前阵子做一个Roguelike塔防原型BOSS出场时要把承重墙撞塌、地图地形跟着变。结果我用Unity自带的Navigation烘焙了一次、两次、三次AI依然无动于衷——墙都倒成一片废墟了小怪还绕着看不见的结界走。就是那几天我把NavMesh Plus动态导航网格这块彻底啃了下来。今天这篇文章就从我当时踩的坑讲起把NavMesh Plus的原理、接入方式、运行时动态烘焙、动态障碍物处理以及那些文档里不会写的注意事项全部梳理一遍。适合正在做动态关卡、沙盒建造、塔防或者任何需要AI实时响应地形变化需求的Unity开发者。1. 为什么你的AI总是撞墙原生NavMesh的静态困局先搞清楚病根。Unity自带的NavMesh工作流非常古典你在编辑器里选中所有场景物件打开Navigation窗口点Bake引擎会把地形、墙体、障碍物的几何信息全部体素化生成一张导航网格快照存成NavMeshData。之后游戏运行时NavMeshAgent就完全靠这张快照做寻路。问题在于这张快照是烤死的。它不会自动感知场景里哪面墙被炸了、哪扇门被打开了、哪座桥升起来了。你想让AI在运行时跟随地形变化基本只有三条路改地面位置后重新NavMesh.CalculateTriangulation扔给NavMeshAgent用这套API在运行时做全量重算数据量一大就卡帧。用OffMeshLink手动维护连接点但地形断裂、重建这种细粒度变化根本管不过来。利用Unity 2022之后官方Navigation包里的NavMeshSurface做运行时重烘焙早期版本能力有限很多关键API和动态收集机制一直在迭代社区里真正用得顺手且资料详细的还是NavMesh Plus。NavMesh Plus本质上把Recast算法那套收集几何体→体素化→生成区域→轮廓→多边形栅格的完整烘焙管线从编辑器搬到了运行时。结合它的Source收集系统你可以让NavMesh在整个游戏过程中持续更新——墙塌了AI绕路门开了AI直穿平台降下来AI走新桥。这对塔防、沙盒建造、Roguelike这类关卡会变形的项目来说几乎是刚需。2. NavMesh Plus两个关键设计和原生方案的差异2.1 第一个设计把烘焙管线完整暴露到运行时原生的NavMesh烘焙在编辑器里是一套不可见的流程Unity拿到场景里的静态几何体按导航设置里的Agent Radius、Height、Max Slope等参数把它们转成导航网格文件。到了运行时这些文件只是被加载的数据没有任何增量更新能力。NavMesh Plus把这一整套流程变成了可以在运行时调用的对象方法。核心是NavMeshSurface组件它直接持有NavMeshBuildSettings然后提供几个关键方法BuildNavMesh()从收集到的Source全集里完整重建一份NavMeshData。数据量大的时候开销高适合初始化阶段或大规模地形变动的场景。UpdateNavMesh(NavMeshData data)只重算出该份NavMeshData对应的Tiles。你拆了一面墙、移开一个箱子用这个方法就能局部更新不需要全图重算。这两个API是动态导航的关键分水岭。原生方案是运行时只能读不能改NavMesh Plus是运行时可局部改可增量改。实际项目里动态烘焙的开销大头不在算法本身而在Source收集和体素化这两个API刚好把这两个环节拆开了你可以在策划需要的时候精准触发。2.2 第二个设计Source收集机制把动态物体纳入烘焙列表动态导航的第二根支柱是怎么让烘焙系统找到哪些物体参与了导航网格计算。NavMesh Plus的Source收集机制支持三种类型的收集对象NavMeshSourceTag挂在动态物体上。只要物体有MeshFilter MeshRenderer或者MeshCollider挂上这个Tag后Surface在收集Source时就会把它纳入计算。程序化生成的地板、运行时加载的模型、被拆掉的墙体都靠它进入烘焙流程。NavMeshModifier挂在物体上用来调整它参与烘焙时的行为。你可以指定它Override某个Area比如把某些装饰物设成Not Walkable或者把它踢出某个Agent类型。NavMeshModifierVolume一个3D体积框。放在场景里可以把某一块区域强制设为Walkable/Not Walkable或者改成任意自定义Area。这个组件做逻辑锁门和区域禁行极其好用后面我会专门讲。原生NavMesh在编辑器里是勾选Navigation Static来决定参与烘焙到了运行时这套标记就变成了一堆不可变的静态数据。NavMesh Plus用Source Tag和Modifier组件把动态参与权交给了开发者这是它能做动态导航的底层底气。3. 接入NavMesh PlusPackage安装到场景改造3.1 安装方式和版本选择NavMesh Plus可以直接用Unity Package Manager安装。打开Window → Package Manager点击左上角选择Add package from git URL...”然后粘贴仓库地址等待解析导入即可。推荐用2020.3或2021.3以上的LTS版本开发环境越稳越好。如果你是从Asset Store或旧项目拿的早期版本注意看组件命名空间——老版本是UnityEngine.AI新版逐渐迁移到Unity.AI.Navigation下面代码我会按新版写老版本自行替换命名空间。导入后Project窗口会多出NavMeshPlus目录里面有ComponentsNavMeshSurface、NavMeshLink、NavMeshModifier、NavMeshModifierVolume、NavMeshSourceTag和Examples示例场景。我的习惯是先打开Examples里的RuntimeBaked场景跑一遍确认链路通再开始改造自己的场景。3.2 把老场景从原生NavMesh迁移到NavMeshSurface迁移过程不复杂但有几个顺序不能乱。我当时就是直接加组件没先删旧数据结果编辑器里同时存在两份NavMeshAgent的寻路结果完全莫名其妙。先清理旧导航数据看看Hierarchy里有没有挂着旧NavMesh组件的物体有就删掉组件Assets目录下单独生成的.asset导航网格文件也一并删除。选中场景里代表地面/主地形的物体挂上NavMeshSurface组件。Surface的Collect Objects参数建议先选All——它会收集场景里所有满足条件的Source。后面项目规模大了再改成Volume限定区域。Use Geometry有Physics Colliders和Render Meshes两种选择。我的建议是优先Physics Colliders。原因很直接AI寻路必须和物理碰撞体一致不然会出现网格能走但角色被碰撞体挡死的精神分裂情况。关键一步把场景里所有不可行走但参与阻挡的物体在Navigation窗口或Object窗口里勾选Navigation Static。这一步在NavMesh Plus里同样有效它会把物体标记为可收集的Source。给场景里所有AI所在物体上的NavMeshAgent看一眼Agent Type确保和NavMeshSurface上的Agent Type一致。这些都做完后回到Surface组件点一下Bake在Scene视图里应该能看到完整的绿色可走区域。如果绿色区域明显偏多/偏少再回Step 4去调整Use Geometry的模式。3.3 agentTypeID最容易被忽略的隐形杀手这一节我必须单独拎出来讲因为这是我在项目里卡得最久的一个问题。NavMesh Plus的Surface上有一个Agent Type下拉框默认是Humanoid而NavMeshAgent上也有一个Agent Type。两者必须严格一致寻路才成立。怎么理解Agent Type不是简简单单的名字它背后是一组完整的NavMeshBuildSettings——Agent Radius半径、Agent Height高度、Max Slope最大坡度、Step Height台阶高度。Unity内部给每套Settings分配了一个整数agentTypeIDNavMeshSurface按这个ID烘焙出来的Tile数据只会被同样ID的NavMeshAgent使用。所以如果你的Surface用的是HumanoidAgent却因为某些代码改动变成了自定义的RogueType那等你运行时会看到Scene里导航网格明明存在Agent却永远找不到路Debug.Log出来的路径是空的。我见过不少人的处理方式是在Inspector里凭感觉选一个Agent Type然后再去调Agent参数这样反而更乱。我的习惯是项目初期统一用一个默认类型要调就只调Agent身上的Radius、Height不改Agent Type下拉框。只有在做小体型比如老鼠、无人机和人体型AI共存的项目时才考虑创建第二套Agent Type同时给对应Surface和对应Agent都用同一套设置。4. 运行时动态烘焙实操让世界在帧内变形4.1 一个最小可复现的动态烘焙场景先搭一个最基础的测试场景确保你完全理解链路再上复杂逻辑。场景内容很简单一块地面上面放两个箱子当障碍物一只AI小兵一个目标点。初始状态下AI从地面一端走向目标点路径会因为箱子而有绕行。玩法逻辑是按空格键随机销毁其中一个箱子然后动态更新导航网格AI必须立刻走新的直线路线。脚本我贴出来using UnityEngine; using UnityEngine.AI; // NavMeshAgent using Unity.AI.Navigation; // 新版NavMeshSurface命名空间 public class RuntimeBakeDemo : MonoBehaviour { public NavMeshSurface groundSurface; public NavMeshAgent agent; public Transform targetPoint; public GameObject[] breakableBoxes; private void Start() { // 初始烘焙一次保证场景开始时有完整网格 groundSurface.BuildNavMesh(); // 给AI安排初始导航目标 agent.SetDestination(targetPoint.position); } private void Update() { if (Input.GetKeyDown(KeyCode.Space)) { int index Random.Range(0, breakableBoxes.Length); if (breakableBoxes[index] ! null) { // 销毁障碍物 Destroy(breakableBoxes[index]); // 关键局部更新导航数据让该区域的Tile重新计算 groundSurface.UpdateNavMesh(groundSurface.navMeshData); // 导航变了必须让Agent重新寻路否则它还会按旧路径走 agent.SetDestination(targetPoint.position); } } } }这里有一个我从实际项目中悟出来的细节UpdateNavMesh调用之后给AI重新设一次目标很多新手会漏掉。导航网格变了但Agent内部的路径数据还是旧的你不重置目标它的表现就是到了原来箱子位置的旁边后突然停下来或者原地打转仿佛撞到了空气。SetDestination会触发寻路重新计算让路径回到正轨。4.2 UpdateNavMesh和BuildNavMesh的取舍原则理解了最小示例之后你得学会在不同业务阶段用不同的方法。我的判断条件很简单场景开始、存档读档、大规模地形生成比如开局把整张地图铺开用BuildNavMesh()全量重建保证所有区域都有数据。游戏中途的局部变化拆墙、封路、开门、移动平台用UpdateNavMesh()局部更新开销小很多。但要注意UpdateNavMesh和BuildNavMesh一样都是同步操作会在当前帧阻塞。如果你的动态地图区域特别大切一块100x100的地图哪怕只更新其中一小块如果这块Tile切分不合理卡顿感也会非常明显。所以接下去一个问题就是怎么让区块切分更科学。4.3 多块Surface与多份NavMeshData的管理一开始我的做法是在整个大地图上挂一个Surface然后每次动态变化都调UpdateNavMesh。结果测试时发现更新一张400x400的地图哪怕只是中间一小条河道变化也要卡上几百毫秒。后来我把Surface拆成了静态区和动态区地面、山体这些永远不会变的单独烘焙成一份只读的NavMeshData建筑物、门、桥这些高频变化的区域单独挂Surface。关键API是// 给每块区域单独生成NavMeshData并注册到同一套NavMesh NavMeshData staticData staticSurface.BuildNavMesh(); NavMesh.AddNavMeshData(staticData); NavMeshData dynamicData dynamicSurface.BuildNavMesh(); NavMesh.AddNavMeshData(dynamicData);之后动态区域要变化时只对dynamicSurface调用运 UpdateNavMesh静态数据完全不受影响。这样拆分之后烘焙的压力被限制在一个小区域里卡顿感降了一个数量级。如果你做的是沙盒建造游戏玩家随手砌墙拆墙我强烈建议把玩家可编辑区域和世界基础地形从一开始就分成两套Surface不要混在一起。5. 动态障碍物实战三种处理思路与选型对比5.1 方案一物理拆除式障碍物用动态烘焙最直观的场景就是可破坏掩体。玩家丢了一个手雷把一堆沙袋炸飞了AI小怪原本绕路走现在直冲过来。这种几何物体真的消失/真的出现的玩法就是要触发上面那套动态烘焙流程。注意两大要点被炸掉的物体要么挂NavMeshSourceTag要么在烘焙前处于被Surface收集的状态。如果你用的是Destroy()即时删除建议删除后立刻调UpdateNavMesh不要等下一帧否则AI会短暂地看穿新路径然后卡住。如果物体是被物理交互从A点搬到B点比如用Rigidbody推箱子每帧更新开销太大正确做法是在箱子最终停稳落地后只做一次UpdateNavMesh。频繁调用动态烘焙是性能毒药。5.2 方案二逻辑锁门用NavMeshModifierVolume有一类需求不是真拆几何体而是这扇门开着AI能走关着AI必须绕路。用动态烘焙当然可以做但更聪明的做法是放一个NavMeshModifierVolume。用法很直白在门口处拉一个Volume尽量贴合门框宽度Volume的Area设置为Not Walkable。初始烘焙时Surface会把这块体积所覆盖的Mesh区域标记为不可走。运行时你只需要控制Volume的enabledpublic NavMeshModifierVolume gateVolume; public void SetGateOpen(bool open) { gateVolume.enabled !open; }enabled关闭后原本Not Walkable的区域变回基础AreaAI会瞬间把它当可行走区域处理完全不用重新烘焙。这个方案性能极高适合门、闸机、任务阶段性封锁、NPC禁入区等场景。但注意前提Volume必须在初始烘焙时已经存在Surface才能把这个覆盖关系写进NavMeshData。如果你是运行时才动态New一个Volume出来那就还得配合一次UpdateNavMesh才能让它生效。5.3 方案三断桥和升降平台用NavMeshLink做动态连接如果地图上有两片物理上不相连的平台AI要跨过去正常烘焙是走不了的——中间没有可走网格。传统做法是放OffMeshLinkNavMesh Plus里看NavMeshLink组件它能设置start和end两个Transform在网格之间建立一条虚拟连接。AI只要走到起点附近就能瞬间传送式通过这条连接。我项目里用NavMeshLink做了一个升降桥桥升起来之后两片平台之间的Link必须断开桥降下来时Link再开启。脚本核心就几行public NavMeshLink bridgeLink; public void RaiseBridge() { bridgeLink.enabled false; } public void LowerBridge() { bridgeLink.enabled true; }还有一类更灵活的用法动态更新Link位置。某些游戏里NPC会周期性改变巡逻路径索桥、藤蔓这类交互物件位置可能跟着变化。只要把bridgeLink.startTransform和endTransform指向新的挂点Agent的跨网格连接就会自动更新。这个方案的好处是完全不碰烘焙开关、挪位置都是纯逻辑操作适合跳跃点、滑索、电梯到达层这类玩法。5.4 三种方案横向对比方案适用场景性能开销实现复杂度是否需要动态烘焙动态烘焙UpdateNavMesh可破坏掩体、地形几何真实变化中等局部Tile可控中必须NavMeshModifierVolume逻辑锁门、禁行区域、任务阶段变化极低仅改enabled低只需初始烘焙NavMeshLink开关断桥、跳跃点、升降电梯极低低两端网格需要烘焙6. 踩坑复盘agentTypeID、卡顿与隐形墙6.1 agentTypeID错配烘焙有网格Agent却永远没路径这个坑我前面提过但还是值得用一整节复盘。现象很迷惑Scene视图里绿色导航网格清清楚楚AI就在网格上站着但你让它寻路它永远报Failed to find path。排查步骤选中NavMeshSurface看Agent Type下拉框当前的值。选中NavMeshAgent看它自己的Agent Type。如果两者不一致就是错配了。把Agent的Agent Type改成和Surface一致问题立刻消失。如果项目里出现了多套Agent Type可以用代码获取对应的agentTypeID做校验int id NavMesh.GetSettingsByIndex(0).agentTypeID;注意GetSettingsByIndex(0)返回的是第一套配置文件对应的NavMeshBuildSettings它的agentTypeID默认是0。如果你创建过自定义Agentindex会变一定要用和Surface面板上名字一致的那一套。我的终极建议是不要轻易创建自定义Agent Type除非你有充分的体积差异化需求。6.2 烘焙卡顿大数据量的主线程阻塞问题动态烘焙本质是CPU密集操作放在主线程里卡顿无法避免。先用Profiler确认卡顿点是不是在NavMeshSurface.UpdateNavMesh或BuildNavMesh内部。如果是你按这个优先级优化第一优先级把大片区域拆成多个Surface见4.3让每次更新的数据量落到可控范围。第二优先级调低Surface上NavMeshBuildSettings的voxelSize不对这个调低只会让烘焙更慢。实践里反而是看要不要关闭Build HeightMesh选项。HeightMesh是辅助层很多寻路不需要它关掉能省掉一截耗时。第三优先级把动态烘焙操作放到目标点可见之后的一帧再做比如玩家触发事件后延迟0.2秒再调UpdateNavMesh分担瞬时开销。另外移动端机型差异很大同一套参数在PC上跑3毫秒在低端手机上可能30毫秒。有条件的话真机上用Profiler跑一次场景变化最密集的时段再把Surface的采集范围和烘焙参数针对移动端收敛一轮。6.3 隐形墙Source收集不全导致的能看见却走不通这类问题比agentTypeID还阴间——场景里明明没有障碍物导航网格也是通的但AI走到某个位置就是绕弯。最后定位发现原来是某个模型上挂着MeshCollider但该物体没被收集进NavMesh计算还有一类是物体的Scale特别大烘焙时体素化细节不够导致狭窄缝隙被识别成封闭区域。处理方法检查所有带碰撞体的物体是否都正确处理了Navigation Static或NavMeshSourceTag。烘焙时用Physics Colliders模式后要再检查一次Collider的尺寸和MeshRenderer是否匹配。有时美术给的碰撞体过度简化寻路路径会和视觉表现严重不符。在Scene视图里打开NavMesh的Debug可视化确认绿色可走区域的边界和你预期的物理可用空间一致。6.4 动态更新后的路径重置这里回到一个细节网格更新后Agent不会自己刷新当前路径。你得显式调用SetDestination或者ResetPath否则AI会沿着旧路径的缓存数据走表现就是该转弯不转弯该直走偏绕远。这句话我说三遍都不嫌多因为我自己就在这个细节上翻车过两次。尤其是用NavMeshModifierVolume开关门时很多人觉得逻辑禁行区开关应该自动更新Agent路径但实际上Agent不会自己重算必须代码里也触发一次重规划。public void ForceRepath(NavMeshAgent agent, Vector3 target) { agent.ResetPath(); agent.SetDestination(target); }7. 最后再分享几个动态导航的小技巧写完复盘再说几个我项目落地时实际用顺手的小技巧。第一给动态Surface的更新操作做一个简单的队列管理。如果有多个障碍物同一帧被破坏把它们集中到一次UpdateNavMesh里处理避免逐帧多次烘焙。第二烘焙线程和寻路线程的时序问题在UpdateNavMesh执行完的那一帧别立刻让所有Agent调用SetDestination。我的经验是至少等一帧或者用yield return null做一个协程延迟不然极端情况下Agent会把目标判定到旧Tile缝隙里去导致一次瞬间丢目标的抖动。第三如果项目里大量用到运行时的地形生成比如程序化生成的地下城或无限地形NavMesh Plus的SourceTag配合Surface的一小段脚本可以做成以玩家为中心的分块生成玩家走到新区块边缘动态生成地形并把对应SourceTag交给Surface触发一次局部烘焙。这样既保证了导航网格持续可用又不会一次性烘焙整张无限地图。坦白讲NavMesh Plus不是银弹它仍然需要你对烘焙参数、数据分块和刷新时机有清晰的把控。但对于需要动态关卡、沙盒建造、塔防、AI实时避障改路这类玩法它确实是目前Unity生态里最省心、最成熟的一套开源解决方案。我做完一个30天可玩原型再回头看最深刻的体会是动态导航的架构设计最好在项目初期就定下来——哪些区域静态、哪些区域动态、动态更新的触发点在哪里想清楚这几点后面所有AI寻路相关的功能都会顺很多。
阅读完成 · 觉得有帮助?