如何运行时烘焙局部NavMesh
解读
国内项目普遍采用动态场景+热更新模式,关卡、机关、建筑常在运行时生成或销毁。若每次改动都全图重新烘焙,移动端会瞬间卡死,内存峰值直接触发OOM。面试官真正想听的是:
- 你能否把NavMesh烘焙从编辑器搬到运行时;
- 能否只烘焙玩家周围的一小块,而不是整个世界;
- 能否在低端安卓机上跑得动,且帧率不掉。
答不到“局部、增量、异步”这三个关键词,基本就凉了。
知识点
- NavMeshBuildSource:把任意Mesh、Collider、Terrain打包成烘焙源,可在运行时动态组装。
- NavMeshBuildSettings:与编辑器设置100%对应,重点调
tileSize、voxelSize、minRegionArea,数值越大越快但精度越低,需在低端机精度与性能之间做权衡。 - 异步烘焙接口:
NavMeshBuilder.BuildNavMeshDataAsync+NavMeshBuilder.UpdateNavMeshData,主线程只抛请求,烘焙在子线程,不阻塞渲染循环。 - 局部包围盒裁剪:用
Bounds与现有NavMeshData做相交测试,只把变动区域标记为dirty,再调用NavMeshBuilder.UpdateNavMeshData增量更新,内存峰值降低一个量级。 - 分层NavMesh:把静态地形与动态机关分两份
NavMeshData,运行时只重建机关层,避免反复烘焙大地形。 - Android闪退兜底:烘焙前先用
SystemInfo.systemMemorySize判断低端机,若小于3 GB则把voxelSize放大到0.5以上,并限制maxJobWorkers=1,防止Unity job system把系统内存打满。 - 版本兼容性:2019 LTS以前无官方运行时API,需用
NavMeshComponents开源包;2020.3+才内置,面试时必须说明项目Unity版本,否则会被追问兼容性方案。
答案
“我们项目Unity 2021.3,战斗房间在运行时拼合,玩家脚下10×10米区域会频繁升降。我的做法是四步:
- 收集源:把动态平台、新刷的障碍全部转成
NavMeshBuildSource,缓存进List,只收集变动对象,不碰静态地形。 - 计算脏盒:用变动对象的
Renderer.bounds合并出Bounds,再外扩3米作为安全边,得到dirtyBounds。 - 异步增量烘焙:调
NavMeshBuilder.UpdateNavMeshData(existingData, settings, sources, dirtyBounds),主线程耗时<1 ms;子线程在低端骁龙660上约90 ms完成,玩家无感知。 - 内存与GC优化:烘焙完立即
source.Clear(),并把voxelSize动态调到0.33(精度5 cm),单块NavMeshData<200 KB,GC.Alloc控制在0.8 MB以内,连续跑30分钟Profiler无内存泄漏。
上线后同屏20人在低端机帧率稳定在55 FPS,NavMesh更新峰值内存上涨不超过8 MB,满足腾讯WeTest性能标杆。”
拓展思考
- 多人同步:如果动态障碍由服务器驱动,客户端烘焙完需把
NavMeshData.GetBuildSettings().agentTypeID与dirtyBounds回传服务器,防止不同客户端导航不一致导致“我能过你不能过”的Bug。 - Job System + Burst:把
NavMeshBuildSource的收集与包围盒合并写成IJobParallelForBatch,再配BurstCompile,在麒麟990上可把90 ms压缩到40 ms,但需关闭Burst异常检测,否则安卓包体+2 MB。 - GPU Baking:Unity 2022.2实验版已支持
NavMeshBuilder.BuildNavMeshDataAsync的GPU路径,实测在Mali-G78上再降30%耗时,但OpenGL ES 3.1以下机型会回退CPU,上线前需做A/B分桶。