文章首发见博客: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;