使用NavMeshObstacle切割动态障碍
解读
国内 Unity 面试中,NavMesh 相关题目出现频率极高,尤其“动态障碍”场景是大厂性能优化与玩法实现的必考点。面试官抛出此题,核心想验证三点:
- 你是否真的亲手写过运行时 NavMesh 的局部重烘焙,而不是只会勾选“Carve”;
- 能否在移动端主线程阻塞与GC 压力之间做权衡;
- 对NavMeshBuildSource 与 NavMeshBuilder 的掌握深度,能否做到零 GC、异步、分帧。
一句话:不是问“能不能切”,而是问“怎么切得又快又稳”。
知识点
- NavMeshObstacle.shape:Cylinder 与 Box 两种,决定 Carve 后的孔洞精度;
- Carve Only Stationary / Move Threshold / Time To Stationary:三件套参数决定“何时触发重切”,国内项目通常把 Move Threshold 调到 0.3~0.5 m 兼顾手感与性能;
- NavMeshBuilder.UpdateNavMeshDataAsync(2019.3+)是官方推荐的异步局部重烘焙接口,可指定 Bounds,避免全场景重烘焙;
- NavMeshBuildSource 池化:把 obstacle 的 Mesh/HeightMesh 提前缓存到 List<NavMeshBuildSource>,每帧复用,杜绝 new;
- JobSystem + Burst 可进一步把“CollectSources”阶段放到子线程,在 60 FPS 低端安卓机上将 8 ms 主线程耗时压到 <1 ms;
- LayerMask 与 NavMeshModifier 结合,可实现“仅切割玩家层,不切割怪物层”的差异化导航;
- NavMeshSurface 数量控制:国内经验是单场景 ≤4 个 Surface,否则 Android Vulkan 下会出现 GPU 读回卡顿;
- 版本差异:2021 LTS 以后支持 NavMeshPrefabInstance,可在 Prefab 阶段预烘焙,解决 Addressables 热更新后 NavMesh 丢失的痛点。
答案
(按面试口语化回答,可直接背诵)
“我们在项目中把动态障碍拆成三步:
第一步,统一封装 NavMeshObstacleAgent。自己写了一个轻量级组件挂在障碍物体上,内部监听 NavMeshObstacle.isStationary 的切换事件,把真正耗时的重烘焙逻辑收拢到事件回调,避免每帧判断。
第二步,异步局部重烘焙。拿到障碍的 Bounds 后,先膨胀 0.5 m 做安全边,然后调用 NavMeshBuilder.UpdateNavMeshDataAsync,传入之前缓存的 NavMeshBuildSource 列表,这样主线程只提交命令,烘焙在子线程完成;低端机用 Coroutine 分 3 帧等待,保证帧率不掉。
第三步,池化 + 零 GC。NavMeshBuildSource 结构体在 Awake 阶段就预生成,运行时只做字段赋值,不产生堆内存;同时把 NavMeshData 实例缓存进对象池,障碍销毁时 Data 不清除而是标记为 reusable,下次直接 Replace,实测在 888 关卡场景里减少 40% 的 GC.Alloc。
最后,为了兼容热更新,我们把 NavMeshSurface 做成 Addressable 资产,打包时把 NavMeshData 序列化成 byte[] 随 AssetBundle 下发,运行时通过 NavMesh.AddNavMeshData 挂载,彻底摆脱场景绑定,线上更新零成本。”
拓展思考
- 多人同屏障碍:MOBA 里五名玩家同时推箱子,若五帧内连续触发五次重烘焙,会把子线程队列挤爆。国内解法是做帧合并队列,1/10 秒收集一次变动区域,再统一 UpdateNavMeshDataAsync,把五次合并成一次;
- 超大世界流式 NavMesh:开放世界 4 km×4 km,NavMesh 数据 > 60 MB,无法一次性加载。我们按 256 m 格子切分,每格预烘焙一个 NavMeshData,玩家移动时用 NavMeshQuery 的 PolygonId 跨 Tile 寻路,障碍只影响所在 Tile,内存占用降到 6 MB;
- GPU Driven 替代方案:部分大厂在预研项目里完全抛弃 CPU NavMesh,改用 ComputeShader 做 3D Texture 体素障碍图,寻路用 Jump-Flood 算法在 GPU 并行,1000 个 NPC 寻路耗时 < 0.2 ms,但需自定义管线,目前仅主机平台落地;
- 面试反向提问:如果面试官追问“NavMeshObstacle.Carve 与 NavMeshModifierVolume 区别”,可答“前者是运行时动态孔洞,后者是烘焙时静态剔除”,并补充“ModifierVolume 支持 AgentType 差异化,Carve 不支持”,直接展示底层源码阅读经验,加分效果明显。