☰
Unity动态导航实战:NavMesh Plus运行时烘焙与动态障碍物处理
2026/10/1 3:36:40 网站建设 项目流程

前阵子做一个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 Plus:Package安装到场景改造

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目录,里面有Components(NavMeshSurface、NavMeshLink、NavMeshModifier、NavMeshModifierVolume、NavMeshSourceTag)和Examples示例场景。我的习惯是先打开Examples里的RuntimeBaked场景跑一遍,确认链路通,再开始改造自己的场景。

3.2 把老场景从原生NavMesh迁移到NavMeshSurface

迁移过程不复杂,但有几个顺序不能乱。我当时就是直接加组件没先删旧数据,结果编辑器里同时存在两份NavMesh,Agent的寻路结果完全莫名其妙。

  1. 先清理旧导航数据:看看Hierarchy里有没有挂着旧NavMesh组件的物体,有就删掉组件;Assets目录下单独生成的.asset导航网格文件,也一并删除。
  2. 选中场景里代表"地面/主地形"的物体,挂上NavMeshSurface组件。
  3. Surface的Collect Objects参数,建议先选All——它会收集场景里所有满足条件的Source。后面项目规模大了,再改成Volume限定区域。
  4. Use Geometry有Physics Colliders和Render Meshes两种选择。我的建议是优先Physics Colliders。原因很直接:AI寻路必须和物理碰撞体一致,不然会出现"网格能走但角色被碰撞体挡死"的精神分裂情况。
  5. 关键一步:把场景里所有"不可行走但参与阻挡"的物体,在Navigation窗口或Object窗口里勾选Navigation Static。这一步在NavMesh Plus里同样有效,它会把物体标记为可收集的Source。
  6. 给场景里所有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分配了一个整数agentTypeID,NavMeshSurface按这个ID烘焙出来的Tile数据,只会被同样ID的NavMeshAgent使用。所以如果你的Surface用的是Humanoid,Agent却因为某些代码改动变成了自定义的"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的enabled:

public NavMeshModifierVolume gateVolume; public void SetGateOpen(bool open) { gateVolume.enabled = !open; }

enabled关闭后,原本Not Walkable的区域变回基础Area,AI会瞬间把它当可行走区域处理,完全不用重新烘焙。这个方案性能极高,适合门、闸机、任务阶段性封锁、NPC禁入区等场景。但注意前提:Volume必须在初始烘焙时已经存在,Surface才能把这个覆盖关系写进NavMeshData。如果你是运行时才动态New一个Volume出来,那就还得配合一次UpdateNavMesh才能让它生效。

5.3 方案三:断桥和升降平台用NavMeshLink做动态连接

如果地图上有两片物理上不相连的平台,AI要跨过去,正常烘焙是走不了的——中间没有可走网格。传统做法是放OffMeshLink,NavMesh 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"。

排查步骤:

  1. 选中NavMeshSurface,看Agent Type下拉框当前的值。
  2. 选中NavMeshAgent,看它自己的Agent Type。
  3. 如果两者不一致,就是错配了。把Agent的Agent Type改成和Surface一致,问题立刻消失。

如果项目里出现了多套Agent Type,可以用代码获取对应的agentTypeID做校验:

int id = NavMesh.GetSettingsByIndex(0).agentTypeID;

注意:GetSettingsByIndex(0)返回的是第一套配置文件对应的NavMeshBuildSettings,它的agentTypeID默认是0。如果你创建过自定义Agent,index会变,一定要用和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寻路相关的功能都会顺很多。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询