Unity记录5.9-地图-优化与动态加载

汇总:Unity 记录

摘要:地图的动态加载与优化,然而优化效果不是很好。

动态加载-2023/09/16

动态加载与初步优化-2023/09/16

  • 和前面的做法类似,判断角色是否离开某个块,离开块时,判断一下区域内是否要加载,是否要卸载。

    • 加载时从读取文件变成了随机生成,
    • 卸载时不再保存地图文件。
    • 加载/卸载区域的形状从正方形变成了转了45度的正方形,类似菱形,这样可以减少一半的当前大小,不过每次加载和卸载的数量不变。
    • Unity记录4.5-存储-随角色加载的Tilemap
  • 试了三个方案,都很卡,但相对来说快了点。

    • 朴素的代码很卡,协程也卡,最后UniTask也卡。

    • 我看是UniTask比协程更好,在都卡的情况下,我先保留了UniTask的方案。

    • 也试过jobs,但是tilemap没法用jobs执行

    • 我还把地图保存从List<List<String>>改成了int[,],但是实际上这里完全不影响效率。

    • 所有的循环,包括地图表面生成,过渡区域生成,矿物生成,一次7个50x50的循环,都不会卡,1ms都不用。

    • 最卡的是Tilemap绘制。

  • Tilemap我最开始用的是tilemap.settile(),这个卡,我认了,因为是单独渲染。

    • 然后查到了tilemap.settiles()以及tilemap.settileblock(),凭直觉选择了后者,因为前者还需要一个位置数组,也就是能够批量生成分散的tile,而后者刚好是生成一个方形的区域。
    • tilemap.settileblock()相对效率是快了,从200ms+到30ms左右,50x50的块。但是还是会卡顿。
    • 最后是把整个地图块又分成了五组来unitask,但还是卡。
  • 以及一个出生点生成,

    • 这个比较简单,出生点在(0, 0),那么当前块的当前坐标为targets,地势需要过渡到这里为地面。
    • 再设置当前块为左右走向的地势,以令左右两侧的地图正常过渡。
  • 下面是实现效果,gif有点大,所以生成的质量低了点,现在这个也要2M。

    • 可以看到,生成的时候,FPS从200降低至了8,巨大的卡顿,应该是加载7块25x25的地图块。

Unity_023_dynamicGenerate.gif

动态加载代码-2023/09/16

public async UniTaskVoid _balance_tilemap(Vector3Int block_offsets_new) {
    Vector3Int BOffsets = block_offsets_new;
    int loadB = _game_configs.__block_loadBound__;
    int unloadB = _game_configs.__block_unloadBound__;
    List<Vector3Int> loads = new List<Vector3Int>(_tilemap_base._blockLoads_list);
    List<Vector3Int> loads_new = new List<Vector3Int>();
    List<Vector3Int> unloads_new = new List<Vector3Int>();

    for (int r = 0; r < loadB; r++){
        for (int x = -r; x <= r; x++){
            int y = r - Mathf.Abs(x);
            loads_new.Add(new Vector3Int(BOffsets.x + x, BOffsets.y + y));
            if (y != 0) loads_new.Add(new Vector3Int(BOffsets.x + x, BOffsets.y - y));
        }
    }
    List<Vector3Int> loads_wait = loads_new.Except(loads).ToList();
    foreach(Vector3Int block_offsets in loads_wait){
        TilemapBlock block = _tilemap_generate._generate_1DBlock(block_offsets);
        _saveLoad._load_block(block);
        _draw_block(tilemap_modify, block).Forget();
        await UniTask.Yield();
    }

    for (int r = 0; r < unloadB; r++){
        for (int x = -r; x <= r; x++){
            int y = r - Mathf.Abs(x);
            unloads_new.Add(new Vector3Int(BOffsets.x + x, BOffsets.y + y));
            if (y != 0) unloads_new.Add(new Vector3Int(BOffsets.x + x, BOffsets.y - y));
        }
    }
    List<Vector3Int> unloads_wait = loads.Except(unloads_new).ToList();
    foreach(Vector3Int block_offsets in unloads_wait){
        _saveLoad._unload_block(block_offsets);
    }
}

地图绘制-2023/09/16

async UniTaskVoid _draw_block(Tilemap tilemap, TilemapBlock block){
    int group = 5;
    TileBase[] tiles = new TileBase[block.size.x * block.size.y / group];
    for (int g = 0; g < group; g++){
        Vector3Int block_origin_pos = new Vector3Int(block.offsets.x * block.size.x + g * block.size.x / group, block.offsets.y * block.size.y);
        for (int x = 0; x < block.size.x / group; x++){
            for (int y = 0; y < block.size.y; y++){
                int tile_ID = block.map[x + g * block.size.x / group, y];
                TileBase tile = _tilemap_base._map_ID_to_tile(tile_ID);
                tiles[x + y * block.size.x / group] = tile;
            }
        }
        BoundsInt block_bounds = new (block_origin_pos, new (block.size.x/group, block.size.y, 1));
        tilemap.SetTilesBlock(block_bounds, tiles);
        await UniTask.Yield();
    }
}

效率测试-2023/09/17

实验-2023/09/17

  • 下面是地图块的加载测试,循环是两个方法都需要的,但是SetTiles需要额外多设置一个数组,所以理论上更耗时,不过我没测两者区别。
    • 没有多次测量取均值,麻烦,只有(d)组做了两次,测试是否效率是否正常。
  • 可以看到,组数的增加在100组前并未明显提高渲染总时长,但当(d)对每块分1000组渲染时,时长增加了25%。
    • 时长未提高,可能是因为渲染100个10 000大小(c组,一百组,每组里10 000个tile)的效率和一个1000 000大小的效率一致,并行性能已经到顶了。
    • d组时长提高,可能是因为并行效率未到顶。
  • 同时,注意到同样大小的每块,生成时间变化很大,从最低的1000+ms到最高的10000+ms浮动。
    • 这可能是因为协程或UniTask开的异步过多了。虽然我这方面知识不多,但从GPU跑模型来说,在我电脑上,15G的模型能正常训练,但3个5G的模型会完全卡死。
  • 因此,为提高效率,一次性加载所有地图块或许是个选择。
方法各块速度/ms总时长/ms
(a) 每块1000x1000大小,不分组加载
SetTilesBlock()1022+1130+1400+1170+1004+3738+1396+6364+3752+1505+8221+2238+711040050
loop130+130+146+138+129+186+135+246+188+139+284+152+2632266
SetTiles()1099+1205+1459+1221+1058+3827+1463+6475+3834+1565+8324+2320+721441064
(b) 每块1000x1000大小,每块分5组加载
SetTilesBlock()1043+1809+1430+1170+4052+3334+1505+6195+3206+1179+8215+1897+684641881
loop131+140+130+145+186+184+131+239+170+127+278+142+2532256
SetTiles()1125+1903+1518+1232+4147+3417+1568+6299+3289+1239+8328+1978+695142994
(c) 每块1000x1000大小,每块分100组加载
SetTilesBlock()2797+2565+1383+1188+3037+5566+1568+5981+1805+7513+4266+1164+224941082
loop128+127+118+106+121+182+109+166+119+206+144+101+1112256
SetTiles()2903+2696+1482+1274+3158+5724+1656+6123+1909+7644+4385+1270+235842582
(d) 每块1000x1000大小,每块分1000组加载
SetTilesBlock()2107+4497+1825+1412+2482+9174+2049+8790+2405+10413+6120+1454+322555953
loop0,每组小于1ms,没正常记录2256
SetTiles()2253+4764+1883+1531+2739+9613+2170+9102+2450+10595+6419+1759+352358801
同上,重复一次观察
2850+1889+1842+1477+3478+6949+2189+9380+2412+10294+6298+1573+329953930
3030+2100+1908+1612+3768+7426+2335+9849+2444+10513+6661+2022+359757265

效率测试代码-2023/09/17

public TilemapRegion4Draw _get_draw_region(Tilemap tilemap, TilemapBlock block){
    int group = 1000;        
    TilemapRegion4Draw region = new TilemapRegion4Draw(){
        tiles = new TileBase[block.size.x * block.size.y / group],
        positions = new Vector3Int[block.size.x * block.size.y / group]
    };
    long[] times1 = new long[group];
    long[] times2 = new long[group];
    long[] times3 = new long[group];

    for (int g = 0; g < group; g++){ 
        System.Diagnostics.Stopwatch stopwatch = new System.Diagnostics.Stopwatch();
        stopwatch.Start();
        Vector3Int block_origin_pos = new Vector3Int(block.offsets.x * block.size.x + g * block.size.x / group, block.offsets.y * block.size.y);
        for (int x = 0; x < block.size.x / group; x++){
            for (int y = 0; y < block.size.y; y++){
                int tile_ID = block.map[x + g * block.size.x / group, y];
                TileBase tile = _tilemap_base._map_ID_to_tile(tile_ID);
                region.tiles[x + y * block.size.x / group] = tile;
                region.positions[x + y * block.size.x / group] = new Vector3Int(block_origin_pos.x + x, block_origin_pos.y + y, 0);
            }
        }
        BoundsInt block_bounds = new (block_origin_pos, new (block.size.x/group, block.size.y, 1));
        stopwatch.Stop();
        times1[g] = stopwatch.ElapsedMilliseconds;

        stopwatch.Start();
        tilemap.SetTilesBlock(block_bounds, region.tiles);
        stopwatch.Stop();
        times2[g] = stopwatch.ElapsedMilliseconds;

        stopwatch.Start();
        tilemap.SetTiles(region.positions, region.tiles);
        stopwatch.Stop();
        times3[g] = stopwatch.ElapsedMilliseconds;
    }
    Debug.Log("Time1: " + times1.Sum() + " Time2: " + times2.Sum() + " Time3: " + times3.Sum());
    // await UniTask.Yield();
    return region;
}

第二次优化-2023/09/18

说明-2023/09/18

  • 我觉得我昨天蛮奇葩的,为什么要跑1000x1000的块,我都不知道几年后才会写成这种情况。
  • 我改写了绘制代码,从SetTilesBlock分别渲染每个块,改为用SetTiles同时渲染所有块。
    • 顺带一提,使用SetTiles同时渲染所有tile,1个1000x1000用了4032,12x1000x1000用了53723,
    • 此外,我感觉Stopwatch不准,昨天1000x1000不论怎么跑,最低都能碰到1000+,今天就4032,太奇怪了。
  • 在改为同时加载后,卡顿明显降低了,应该除了渲染效率提升外,还减少了unitask的损耗把。
    • GPT:过多的并发任务: 如果你创建大量的 UniTask 实例并同时运行它们,可能会导致线程资源竞争和上下文切换,从而影响性能。
  • 但目前还是一步一卡的状态,只是暂时能接受了,以后再优化把,指不定我换引擎了是吧。
  • 题外话,UniTask的2.4.0好像有问题,没有Delay等函数(从NuGet安装),但2.3.3正常(从GitHub release安装)。

代码-2023/09/18

public void _draw_region(Tilemap tilemap, List<TilemapRegion4Draw> regions){
    List<Vector3Int> position_all = new();
    List<TileBase> tiles_all = new();
    foreach(TilemapRegion4Draw region in regions){
        position_all.AddRange(region.positions);
        tiles_all.AddRange(region.tiles);
    }
    Vector3Int[] position_array = position_all.ToArray();
    TileBase[] tiles_array = tiles_all.ToArray();
    tilemap.SetTiles(position_array, tiles_array);
}

第三次优化-2023/09/18

单独渲染每个Tile-2023/09/18

  • 上面的结论很奇怪,一个异步操作让程序卡了。
    • 我看到一篇关于Tilemap如何多线程渲染的讨论(讨论结果是不能多线程)中,有人提到一个个Tile单独渲染,慢但是不卡,这很匹配我的知识点。
    • 但是在我的实验中又变成了单独渲染造成卡顿。
    • 我的实验应该是,在不卡顿的前提下,每次渲染多一点。而不是怎么都卡顿,但通过渲染多一点,让卡顿少一点。
  • 所以我还在上面tilemap.SetTiles(position_array, tiles_array);加了一个单独渲染每块的方法,但大大降低了渲染速度。
    • 难道是我协程/UniTask的方式不对?
for(int i = 0; i < position_all.Count; i++){
    tilemap.SetTile(position_all[i], tiles_all[i]);
}

异步修改-2023/09/18

  • 我将SetTIle后加了一个await,好了,现在不卡了。FPS平均在200+。
for(int i = 0; i < position_all.Count; i++){
    tilemap.SetTile(position_all[i], tiles_all[i]);
    await UniTask.Yield();
}
  • 进一步的,我再次进行了分组,在所有Tile上,一次渲染5个,FPS稳定在90+。
int tile_per_group = 5;
int group_count = position_all.Count / tile_per_group;
for(int i = 0; i < group_count; i++){
    Vector3Int[] tmp_positions = position_all.GetRange(i * tile_per_group, tile_per_group).ToArray();
    TileBase[] tmp_tiles = tiles_all.GetRange(i * tile_per_group, tile_per_group).ToArray();
    tilemap.SetTiles(tmp_positions, tmp_tiles);
    await UniTask.Yield();
}
  • 那么结论正确了,前面怎么降都卡,是因为,数量还是太多,无语住。

关闭不用的UniTask-2023/09/18

  • 每次角色所处地图块变化时,都会触发动态加载,因此会导致移动时触发多个动态加载。

    • 但之前的动态加载实际上已经不用了,因此需要取消之前的UniTask。
  • 但是我暂时不想做了,以后再专开一个优化阶段。记录一下现在的思路。

    • 方案1:

    • 执行前先声明一个CancellationTokenSource cancel_token,用于关闭子UniTask。

    • 子UniTask通过判断cancel_token来提前退出if (cancel_token.IsCancellationRequested) return;

    • 这个方案的缺陷在于逻辑需要修改,因为多个动态加载的内容并不重复,只是同时运行了而已,因此提前退出会导致新的区域被渲染,旧的区域渲染一半就停止了

    • 方案2:

    • 不在_balance_tilemap内渲染,而是添加地图块至某个队列,然后由另一个函数加载队列。

    • 这个方案不改变现有逻辑,看起来更可靠。

  • 目前用的是第二次优化的结果,一次性渲染所有tile,虽然一步一卡,但是至少加载的块。

  • 方案1代码:

// 声明Token,传给UniTask()
if (_cancel_balanceTilemap != null) _cancel_balanceTilemap.Cancel();
_cancel_balanceTilemap = new CancellationTokenSource();
_tilemap_system._balance_tilemap(_tilemapBlock_offsets, _cancel_balanceTilemap).Forget();

// 在_balance_tilemap内传入cancel_token.Token
_tilemap_draw._draw_region(tilemap_modify, regions, cancel_token.Token).Forget();

// 子UniTask内判断
public async UniTaskVoid _draw_region(Tilemap tilemap, List<TilemapRegion4Draw> regions, CancellationToken cancel_token){
     if (cancel_token.IsCancellationRequested) return;

版权声明:
作者:MWHLS
链接:https://mwhls.top/4860.html
来源:无镣之涯
文章版权归作者所有,未经允许请勿转载。

THE END
分享
二维码
打赏
< <上一篇
下一篇>>
文章目录
关闭
目 录