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;
相关推荐
神洛华43 分钟前
Y3编辑器教程8:资源管理器与存档、防作弊设置
编辑器·游戏引擎·游戏程序
Moweiii1 小时前
SDL3 GPU编程探索
c++·游戏引擎·图形渲染·sdl·vulkan
Artistation Game1 小时前
一、c#基础
游戏·unity·c#·游戏引擎
成都渲染101云渲染66662 小时前
云渲染,Enscape、D5、Lumion渲染提速教程
运维·服务器·unity·电脑·图形渲染·blender·houdini
超龄魔法少女1 天前
[Unity] ShaderGraph动态修改Keyword Enum,实现不同效果一键切换
unity·技术美术·shadergraph
蔗理苦1 天前
2024-12-24 NO1. XR Interaction ToolKit 环境配置
unity·quest3·xr toolkit
花生糖@1 天前
Android XR 应用程序开发 | 从 Unity 6 开发准备到应用程序构建的步骤
android·unity·xr·android xr
向宇it1 天前
【从零开始入门unity游戏开发之——unity篇02】unity6基础入门——软件下载安装、Unity Hub配置、安装unity编辑器、许可证管理
开发语言·unity·c#·编辑器·游戏引擎
虾球xz1 天前
游戏引擎学习第55天
学习·游戏引擎
虾球xz1 天前
游戏引擎学习第58天
学习·游戏引擎