如何运行时烘焙局部NavMesh

解读

国内项目普遍采用动态场景+热更新模式,关卡、机关、建筑常在运行时生成或销毁。若每次改动都全图重新烘焙,移动端会瞬间卡死,内存峰值直接触发OOM。面试官真正想听的是:

  1. 你能否把NavMesh烘焙从编辑器搬到运行时
  2. 能否只烘焙玩家周围的一小块,而不是整个世界;
  3. 能否在低端安卓机上跑得动,且帧率不掉。
    答不到“局部、增量、异步”这三个关键词,基本就凉了。

知识点

  1. NavMeshBuildSource:把任意Mesh、Collider、Terrain打包成烘焙源,可在运行时动态组装。
  2. NavMeshBuildSettings:与编辑器设置100%对应,重点调tileSizevoxelSizeminRegionArea,数值越大越快但精度越低,需在低端机精度与性能之间做权衡
  3. 异步烘焙接口NavMeshBuilder.BuildNavMeshDataAsync + NavMeshBuilder.UpdateNavMeshData,主线程只抛请求,烘焙在子线程,不阻塞渲染循环
  4. 局部包围盒裁剪:用Bounds与现有NavMeshData做相交测试,只把变动区域标记为dirty,再调用NavMeshBuilder.UpdateNavMeshData增量更新,内存峰值降低一个量级
  5. 分层NavMesh:把静态地形与动态机关分两份NavMeshData,运行时只重建机关层,避免反复烘焙大地形
  6. Android闪退兜底:烘焙前先用SystemInfo.systemMemorySize判断低端机,若小于3 GB则把voxelSize放大到0.5以上,并限制maxJobWorkers=1防止Unity job system把系统内存打满
  7. 版本兼容性:2019 LTS以前无官方运行时API,需用NavMeshComponents开源包;2020.3+才内置,面试时必须说明项目Unity版本,否则会被追问兼容性方案。

答案

“我们项目Unity 2021.3,战斗房间在运行时拼合,玩家脚下10×10米区域会频繁升降。我的做法是四步:

  1. 收集源:把动态平台、新刷的障碍全部转成NavMeshBuildSource,缓存进List,只收集变动对象,不碰静态地形。
  2. 计算脏盒:用变动对象的Renderer.bounds合并出Bounds,再外扩3米作为安全边,得到dirtyBounds
  3. 异步增量烘焙:调NavMeshBuilder.UpdateNavMeshData(existingData, settings, sources, dirtyBounds)主线程耗时<1 ms;子线程在低端骁龙660上约90 ms完成,玩家无感知。
  4. 内存与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性能标杆。”

拓展思考

  1. 多人同步:如果动态障碍由服务器驱动,客户端烘焙完需把NavMeshData.GetBuildSettings().agentTypeIDdirtyBounds回传服务器,防止不同客户端导航不一致导致“我能过你不能过”的Bug。
  2. Job System + Burst:把NavMeshBuildSource的收集与包围盒合并写成IJobParallelForBatch,再配BurstCompile,在麒麟990上可把90 ms压缩到40 ms,但需关闭Burst异常检测,否则安卓包体+2 MB。
  3. GPU Baking:Unity 2022.2实验版已支持NavMeshBuilder.BuildNavMeshDataAsync的GPU路径,实测在Mali-G78上再降30%耗时,但OpenGL ES 3.1以下机型会回退CPU,上线前需做A/B分桶