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

文章首发见博客:https://mwhls.top/4860.html

无图/格式错误/后续更新请见首发页。

更多更新请到mwhls.top查看

欢迎留言提问或批评建议,私信不回。
汇总: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的地图块。
动态加载代码-2023/09/16
c 复制代码
    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
c 复制代码
    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+7110 40050
loop 130+130+146+138+129+186+135+246+188+139+284+152+263 2266
SetTiles() 1099+1205+1459+1221+1058+3827+1463+6475+3834+1565+8324+2320+7214
41064
(b) 每块1000x1000大小,每块分5组加载
SetTilesBlock()
1043+1809+1430+1170+4052+3334+1505+6195+3206+1179+8215+1897+6846 41881
loop 131+140+130+145+186+184+131+239+170+127+278+142+253 2256
SetTiles() 1125+1903+1518+1232+4147+3417+1568+6299+3289+1239+8328+1978+6951
42994
© 每块1000x1000大小,每块分100组加载
SetTilesBlock()
2797+2565+1383+1188+3037+5566+1568+5981+1805+7513+4266+1164+2249 41082
loop 128+127+118+106+121+182+109+166+119+206+144+101+111 2256
SetTiles() 2903+2696+1482+1274+3158+5724+1656+6123+1909+7644+4385+1270+2358
42582
(d) 每块1000x1000大小,每块分1000组加载
SetTilesBlock()
2107+4497+1825+1412+2482+9174+2049+8790+2405+10413+6120+1454+3225 55953
loop 0,每组小于1ms,没正常记录 2256
SetTiles() 2253+4764+1883+1531+2739+9613+2170+9102+2450+10595+6419+1759+3523
58801
同上,重复一次观察
2850+1889+1842+1477+3478+6949+2189+9380+2412+10294+6298+1573+3299 53930
3030+2100+1908+1612+3768+7426+2335+9849+2444+10513+6661+2022+3597 57265
效率测试代码-2023/09/17
c 复制代码
    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
c 复制代码
    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的方式不对?
c 复制代码
    for(int i = 0; i < position_all.Count; i++){
        tilemap.SetTile(position_all[i], tiles_all[i]);
    }
异步修改-2023/09/18
  • 我将SetTIle后加了一个await,好了,现在不卡了。FPS平均在200+。
c 复制代码
    for(int i = 0; i < position_all.Count; i++){
        tilemap.SetTile(position_all[i], tiles_all[i]);
        await UniTask.Yield();
    }
  • 进一步的,我再次进行了分组,在所有Tile上,一次渲染5个,FPS稳定在90+。
c 复制代码
    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代码:

c 复制代码
    // 声明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;
相关推荐
咸鱼永不翻身23 分钟前
Unity视频资源压缩详解
unity·游戏引擎·音视频
在路上看风景29 分钟前
4.2 OverDraw
unity
在路上看风景1 小时前
1.10 CDN缓存
unity
ellis197011 小时前
Unity插件SafeArea Helper适配异形屏详解
unity
nnsix12 小时前
Unity Physics.Raycast的 QueryTriggerInteraction枚举作用
unity·游戏引擎
地狱为王12 小时前
Cesium for Unity叠加行政区划线
unity·gis·cesium
小贺儿开发21 小时前
Unity3D 八大菜系连连看
游戏·unity·互动·传统文化
在路上看风景21 小时前
25. 屏幕像素和纹理像素不匹配
unity
ۓ明哲ڪ1 天前
Unity功能——创建新脚本时自动添加自定义头注释
unity·游戏引擎
熬夜敲代码的小N1 天前
Unity大场景卡顿“急救包”:从诊断到落地的全栈优化方案
java·unity·游戏引擎