
1. 为什么Unity要重写NavMesh旧方案的积弊与新包的核心增量1.1 旧方案在真实项目里的三大痛点先交代一下背景。用过Unity内置NavMesh的老玩家应该都有这种体验烘焙走的是Window AI Navigation这个老面板把静态物体在Inspector里勾上Navigation Static只要模型稍微动一下整个场景重烘一遍烘出来的网格还存在场景的NavMesh.asset里改完场景忘记重新烘焙打包出去NPC就一头撞在墙上——这种尴尬我经历了很多次。旧方案最大的问题不在烘焙操作本身而在它的数据结构是一份NavMesh走天下。一个包含主城、副本、野外地形的大场景你只有一整张NavMesh角色要跨区域切换时没法按需加载导致内存和寻路开销都会堆得很离谱。而且旧系统对动态环境的支持非常弱一座桥断了、一堵临时墙立起来你得在运行时操作Mesh切割或者临时隐藏/显示物体逻辑写得很绕性能和效果都不可控。我在项目里还踩过一个更务实的坑老面板里对可行走区域的识别完全依赖物体是否勾选了Navigation Static而这个标记是一个单独开关很容易漏勾。一个漏勾的角色走不过一面明明看起来能穿过的墙排查半天发现是美术小哥加完模型忘了勾标记。这类问题放到现代项目里越来越多因为场景资产规模、动态物件数量都涨了几十倍旧方案已经扛不住了。1.2 AI Navigation包到底改了什么Unity 2022.3开始把新的AI Navigation作为官方推荐方案推出来其实就是原来GitHub上那个NavMeshComponents实验包转正了。它的核心增量可以概括成三点。第一烘焙从场景内零零碎碎的静态标记变成了组件化Surface。你不再需要满场景去找哪个物体勾没勾Navigation Static而是在需要生成NavMesh的区域放一个NavMeshSurface组件用它来收集范围内的导航数据。Surface的边界是可视化的它有Size属性控制范围有Layer Mask控制哪些层级参与烘焙场景再大也能拆成多块Surface分别处理配合流式加载思路很顺。第二引入了Agent Type体系。之前Agent有Radius和Height等参数但同一时间只有一个全局配置。新系统里你可以创建多套NavMeshAgentSettings比如小型怪物一套参数、大型Boss一套参数、飞行单位甚至可以单独定义一个完全不同的网格烘焙结果。这让同一场景里不同体型单位走不同路径从不可能变成了一等公民。第三NavMeshLink替代了土办法。旧时代做楼梯、跳台、门洞这些网格不连续区域需要在脚本里写Teleport或者自己写路径拼接非常肉疼。新系统的NavMeshLink组件直接在两个Surface之间建立一条可穿越的连接角色走到端点附近会自动瞬移到另一个端点并继续寻路实现起来就是拖一个组件配一下参数。第一节先做到认知对齐后面我们再逐一展开。2. 环境准备与首次烘焙动手前必须搞清楚的三件事2.1 用NavMeshSurface替代Navigation Static烘焙思路彻底变了首次烘焙前要做的第一件事是安装包通过Window Package Manager搜索AI Navigation装完以后菜单栏会出现Window AI Navigation的新面板场景里可以添加NavMeshSurface组件。很多教程会直接让你把Surface往地上一拖就点Bake但我要提醒一句新系统烘焙时默认只收集静态物体也就是物体自身在世界空间没有移动且没有NavMeshModifier的情况下Surface才会把它纳入计算。如果你的关卡里有大量动态生成的地板或者可破坏墙体光靠Surface是不行的得配NavMeshModifier和NavMeshModifierVolume去动态调整。实测下来我建议首次尝试时用一个很小的室内场景地面一块Plane加几堵Cube墙地面只加Surface把Include Layers指向地面和墙体的Layer然后点Bake。烘焙结果会生成一个子物体里面挂NavMeshData相关组件。这时候你可以加一个NavMeshAgent到胶囊体上设置一个NavMeshAgent的destination就能看到它很自然地绕墙走位了。这里有个细节别忽略Surface的Use Geometry字段有三个选项默认是Render Meshes意味着烘焙数据来自被渲染的Mesh。如果你的地面用了复杂Shader或者LOD烘焙结果可能和你视觉上看到的不一致。我现在的习惯是单独做一个只用于导航的简化Mesh层——不加渲染组件只挂MeshCollider然后用Physics Colliders模式去烘焙。这样烘焙结果干净可控还不受模型美术资源精度影响。2.2 Agent Settings不是随便填的四个参数是项目底层的通行规则在AI Navigation面板的Agents选项卡里系统预设了一套默认参数很多新手点了烘焙就直接用结果角色穿不过门缝、跳不上台阶跑来问我怎么回事。实际上这些参数是寻路系统的物理规则每套Agent Type会生成独立的NavMesh数据烘焙时Surface会为每个Agent Type各生成一份网格。先说几个真正影响日常使用的主要参数Radius代理的半径决定角色能通过的最窄缝隙。Height代理高度决定最低的头顶间隙。Step Height台阶高度把小于这个值的垂直落差视为可直接走上去类似角色脚下的视觉脚踝。Max Slope最大坡度大于这个坡度的斜面会被当作不可行走。我常用的经验值是这样人形单位Radius给0.3Height给1.8Step Height给0.5Max Slope在室内给35度室外山体给45度再加额外处理。你可能会问Step Height设大一点不是更省事吗一股脑给到1米确实能让角色跨上很多箱子但寻路结果会变得很假——明明是跳过去的地方看起来却是脚滑瞬移上去的。对需要穿盔甲的写实项目来说这个参数宁小勿大。另外还有一套Generated OffMesh Links相关选项打开后系统会在烘焙时自动检测一定高度差内的可跨越边缘生成自动跳跃连接。这个功能省事但默认的检测逻辑比较粗在复杂场景里容易出现角色翻上桌子的意外几何体我建议初级阶段先手动用NavMeshLink把自动生成关掉后续确定需要了再开。2.3 分层烘焙不要让一张NavMesh承载所有需求一个我强烈建议从第一天就养成的习惯不要把所有地形扔进同一个Surface去烘。新系统最实用的模式是拆层烘。举例来说我在一个开放区域项目里会构建三套Surface地形层Surface只收集地面的地形Mesh负责大范围移动。建筑层Surface室内地板、走廊、桥梁通过NavMeshModifier标记为重要走道。障碍物层路障、碎石、临时墙体运行时动态开关。拆开之后运行时你可以单独控制每一层Surface的启用状态比如“建筑层Surface”配合楼层切换系统角色进入某栋楼时只加载这一层的Surface数据其它楼层直接隐藏索引和寻路压力大幅降低。实际写代码时要注意多Surface模式下每个Surface默认会生成一份独立的NavMeshData它们之间存在重叠区域时Unity会自动做拼接。如果拼接处出现接缝断层最常见的原因是相邻Surface的Agent Type或Layer Mask配置不一致。所以多Surface的前提是参数完全统一只在Size和Center上做区分。3. 让角色会跳、会跨NavMeshLink与OffMeshLink的实战用法3.1 什么时候该用Link什么时候不该用网格不连续的场景我们天天见台阶太高走不上去、两栋楼之间隔着一条街、屋顶之间要跳过去这些地方对NavMesh来说都是缝隙。NavMeshLink就是填补这些缝隙的官方组件。但很多人把Link当万能工具我觉得有必要先划个界限。Link适合两种场景一是路径上必须跨过的低概率缺口比如一个跳台、一座断桥二是设计上要强制角色走的特殊路线比如电梯口、传送门。不适合的场景是大面积断路、需要绕行优化的地方。如果一条路断得太多你与其铺一堆Link不如回去把Surface多包一点。以断桥为例桥两端各有一块NavMesh中间是悬崖。我在断口一侧放一个NavMeshLink组件设置Start Transform和End Transform指向两端的空物体。烘焙完以后Agent走到起点附近会自动朝终点移动跨越时它的isOnOffMeshLink属性会变true你可以监听这个状态在跨越过程中播放跳跃动画。这里有两个参数容易踩雷。第一个是Direction默认是Both双向都可走但如果桥是单向的比如只能跳过去不能跳回来记得改成One Way并指定终点方向。第二个是Width这个值表示Agent可以接入Link的横向范围一定不能比Agent的Radius小否则角色在Link附近会反复横跳看着像卡住了一样。Width我的习惯是给到Agent直径的两倍留足冗余。3.2 Link的坑方向、宽度和代理尺寸的联动NavMeshLink还有一个容易忽视的属性叫Activated默认是开启的。如果你在代码里动态控制桥梁是否可用必须显式地把它关了再开不能直接通过禁用组件实现。因为组件被禁用时烘焙出来的OffMeshLink数据不会从场景中删除Agent依然会把这条路当作可行路径只是到端点后发现无法穿越而产生抖动。这算是一个典型的隐藏开关坑。另一个坑是Link端点位置不在NavMesh表面上。我见过有人把Link的Start和End指向带有碰撞体的物体中心结果物体中心悬空Agent走到端点附近时距离算法判定无法接入路径直接失败。解决办法是让端点空物体尽量贴近NavMesh表面可以加一个调试用的Gizmo线框来可视化端点位置。再说一个偏美术侧的细节Link是“逻辑通道”不是“视觉通道”。断桥上如果只有Link但没放木板、踏板之类的视觉元素角色会在半空中踩着空气走过去穿帮非常严重。我现在做这类功能时都会在Link对应的路径上放一条不可碰撞的绳桥或浮空木板把横跨动作合理化让玩家和策划都能一眼理解这里是能走的。3.3 跑通跳点后的验证方法Link铺完不是点个Bake就完事真正费时间的是调接入点。我的验证流程是三步打开AI Navigation面板的Show NavMesh先肉眼看Link两端是否有网格覆盖。把Agent挂到起点附近用断点跟踪remainingDistance和isOnOffMeshLink确认状态切换链完整寻路中 - 接入Link - 跨越 - 落回网格。在Link跨越状态里播一个跳跃动画同时用CompleteOffMeshLink()手动结束跨越保证落点位置修正到终点表面的最近点。第三步看着简单实际很关键。因为Link跨越结束后Agent的位置是基于终点的如果不做一次NavMesh.SamplePosition修正角色可能从半空坠落一小段才落地物理表现上会有肉眼可见的下沉。这个修正我对任何从A点到B点的非连续移动都会顺手做一遍收益巨大。4. 动态障碍物与运行时更新别让一堵墙卡死全场AI4.1 NavMeshObstacle三种模式的取舍游戏里总有地面模型漂移的情况一扇门开了、一堵墙塌了、一辆车停进道路中央。这些AI不能硬穿、但也没必要重新烘焙全场景的东西新系统用NavMeshObstacle组件来处理。NavMeshObstacle有三种运作模式区分得很清楚模式行为性能开销适合场景默认(不勾选Carve)本地避障时绕开不走寻路网格挖洞极低数量多的可移动物体小石头、短时停留的角色Carve在NavMesh上动态挖出一个洞全局寻路会避开中数量少但需要全局绕行的障碍物混合Carve先本地绕靠近到一定距离再挖洞中高兼顾全局和局部适合会封路的大障碍选型逻辑很简单如果障碍物只影响眼前几个单位的走位默认模式够了如果它在全局层面挡住了整条巷道才上Carve。我在主城项目里就是把城门、大型车驾设成Carve小推车和路障用默认模式。如果你把几十个路障全部设成Carve运行时每帧要做大量多边形挖洞计算哪怕物体一动不动也会白耗CPU。我的经验是Carve模式下尽量少动态改变transform需要移动时用脚本批量更新CarveOnlyHandled开关来控制重建时机。4.2 运行时改变地形后的NavMesh重建动态障碍只是小手术真正的大手术是地形本身变了玩家炸了一面墙、新地图一段区域解锁。这时候必须有运行时重建NavMesh的思路。新系统里最朴素的方法是调用Surface.BuildNavMesh()把场景里所有相关Surface都重建一遍。这个操作是同步的地形复杂时会造成明显卡顿所以实战中我连续踩坑后总结了一个分段重建策略把大场景按区块拆成多个NavMeshSurface。需要重建时先停用目标区Surface调用BuildNavMesh()异步版本或者放到协程里分帧执行。重建完成后再开启Surface让Agent进入新网格。协程伪代码思路大致如下IEnumerator RebuildSurface(NavMeshSurface target) { target.SetActive(false); var job NavMeshSurface.BuildNavMeshAsync(); while (!job.isDone) { yield return null; } target.SetActive(true); }如果你用的是官方包的最新版本还可以直接使用NavMeshDataInstance配合NavMesh.AddNavMeshData来增量提交网格数据这样重建时可以完全不影响其它区域Agent的寻路但复杂度也明显上来了。普通项目我建议先用禁用Surface重建再启用的土办法看清性能瓶颈后再说。4.3 性能与异步逐帧分段烘焙的实际经验一个特别容易被忽视的点Surface的BuildNavMesh()不仅消耗CPU还会占用主线程一大块时间。实测一个中等街区大概200米x200米同步烘焙会在低端移动设备上卡掉1-2帧射击游戏里这是致命的。后来我把烘焙转移到专门的异步协程里做并把烘焙区域缩小成以玩家为中心的3x3块其余块不预生成配合进入区域边缘才触发烘焙的LOD式管理。效果非常显著帧耗时从平均20ms降到8ms左右并且玩家几乎感知不到NavMesh在动态更新。这里有一个小技巧供参考烘焙请求可以加一个延迟合并。比如墙壁倒塌时5秒内连续触发了多次重建请求其实只需要最后一次结果。用一个bool pendingRebuild标记在协程末尾合并处理能省下一半以上的无用烘焙。这样既能保证动态变化及时生效又不会白白烧性能。5. 避障不等于寻路Local Avoidance与全局路径的边界在哪5.1 多角色互相挤在一起的真正原因很多人在项目里会遇到这种怪现象一群单位走同一条路明明有避障参数却还是互相穿透、挤成一团。先把概念拆清楚NavMeshAgent的寻路负责从A到B沿网格走而避障是另外一套系统叫Local Avoidance本地回避。它只处理视野范围内其他Agent和Obstacle对我的短时影响并不尝试重新规划全局路径。默认情况下Agent的Avoidance Priority是相等的两个Agent相遇时谁都不让谁节点计算就会在一条窄路上来回震荡表现就是“原地拉扯”。这时候不是把Radius调小就能解决而是要给不同类型的单位设置不同的优先级。比如玩家角色优先级最低让路中型敌人其次大Boss最高坚决不让。这样设定以后窄路上的NPC会自然给高优先级让出通路观感会好很多。5.2 Priority、Radius和速度曲线怎么配合本地避障系统里有一个我没法省掉的调试项就是Agent组件上的Priority以外还要关注Radius和Stopping Distance之间的配合。拿一个护送NPC的例子来说保镖和主角都是HumanoidRadius都是0.3Priority都是0.1每次主角停下保镖就会不断在他身边绕圈观感很怪。我把保镖改成Priority 50更低优先、Stopping Distance到主角旁边设成1.2米这样它会主动停在离主角一臂远的地方而不是挤到主角身体里。这套避障参数只影响同层问题的理解很重要——避障永远不会帮你重新规划路径它只是在当前路径上“贴着走”。真想解决群体拥挤还得靠寻路层面的Area Cost。你可以在Navigation面板里给某个区域加成本比如NPC默认避开广场中央、或者给泥地区域一个额外成本。成本机制直接影响全局寻路树和Local Avoidance是两套独立系统前者管选路后者管擦肩。我调试拥挤问题时如果发现单位在交叉路口反复横跳第一反应永远是查Area Cost而不是去调避障因为路径选择的错误靠避障根本救不回来。另外遇到单位卡在墙角但路径又正确的情况检查一下AutoTraverseOffMeshLink是否关掉了。关掉以后我们必须自己处理Link逻辑处理不好Agent会在端点前停下来表现就差很多。这里没有万能公式但基本思路是避障管短距离反应寻路管策略两者不要混在一起调。6. 从测试场景到真实项目我总结的烘焙参数与几种典型坑6.1 一张表看完我常用的烘焙参数以我目前做的一个中型开放街区项目为例Agent Type有步兵和车辆两套以下是我反复测过后长期沿用的参数参数项步兵车辆备注Radius0.31.2车辆不能走室内窄道Height1.81.5车辆模型低矮但跨度大Step Height0.450.2车辆基本不能上台阶Max Slope35度20度车辆不爬陡坡Base Offset00一般保持0即可Auto Traverse OffMeshLink开关车辆Link需要单独写逻辑Generated OffMeshLinks关关全部手动Link避免奇怪的自动跳跃注意同一张Surface必须能为多套Agent Type各生成一份网格。在Surface组件里Agent Type下拉列表会列出全部已定义的Agent Type烘焙时一次性生成多份数据。实际项目中这个功能非常重要同一座桥步兵能走、车辆不能走根本不需要额外逻辑烘焙结果就不同。还要强调一个关于Voxel Size的误区很多教程建议“这个值越小烘焙越精细”但它非常吃内存和烘焙时间。默认值是0.1667这是我经过对比后认为的甜点值再低到0.1烘焙时间会指数级上升而寻路精度提升却几乎看不见。如果你在烘焙时报Out of Memory十有八九是Voxel Size被调得太低。6.2 动画、刚体和摄像机AI系统周边环节的连带问题把NavMeshAgent直接挂到带Rigidbody的角色上是我碰到最多的周边坑之一。Agent自己有一套位置驱动逻辑Rigidbody又是物理驱动两套系统同时写Transform就会有拉扯感。好消息是新版Agent和Rigidbody可以共存但必须把Rigidbody的Interpolate打开Collision Detection设为Continuous Dynamic并且在FixedUpdate里做位置同步不要在Update里直接赋值Transform。物理刚体的移动逻辑是固定步长没同步好角色会一跳一跳地走观感特别差。动画方面很多人用Animator的Apply Root Motion来控制角色位移这跟NavMeshAgent完全是两条腿走路Agent要往前走RootMotion又往回拉角色原地踏步或者抖动画面很滑稽。我的做法是寻路时Apply Root Motion关掉由Agent驱动Transform动画只负责播放与速度匹配的移动动画只有在演出段落或摇杆微调时再开启RootMotion。这个切换逻辑维护一个bool isPathFollowing即可。摄像机跟随也有连带关系。Agent接管位置后摄像机如果直接每帧LookAt Agent当前位置镜头会非常死板。我看过有项目用老套的Camera.LookAt(agent.transform.position)结束后镜头猛甩。建议摄像机用平滑阻尼插值跟随配合LateUpdate里锁定一个观察点偏差比如观察点偏移到Agent前方1米处画面稳很多。6.3 调试时最实用的两个可视化工具最后分享两个让我少走很多弯路的调试姿势。第一个是AI Navigation面板里的Show Paths选项打开后每个Agent的当前路径会画出来你能直接看出它选择的路是否合理。第二个是利用NavMesh.CalculatePath自己画一条路径到Scene视图里的Debug.DrawLine不依赖Agent也能复现寻路问题做回归测试很方便。调试接缝或者No path问题时我习惯先在目标点附近调NavMesh.SamplePosition确认落点是否在网格上。很多No path根本不是寻路bug而是你给了一个悬空的终点坐标Sample出来是false原因瞬间就找到了。移动端项目里这套排查方法非常靠谱因为运行时高度动态落点偏移的概率远大于PC机。回到开头说的旧方案问题新NavMesh系统的门槛其实不在功能本身而在于大家习惯了烘焙一次走天下的旧心智。只要你愿意把场景拆成Surface、把Agent Type当回事、把Link和Obstacle放到正确的边界里这个系统完全可以支撑从解谜游戏到大型开放世界的各类寻路需求。我之前用一个周末把项目里所有旧NavMesh迁移到新包迁移完成后同事反馈最明显的变化不是功能而是改场景终于不用全图重烘了。这种正向体验可能才是新系统给到日常开发最大的礼物吧。