我接手一个UE5.3项目时,AI角色总在狭长走廊的墙角处原地打转。起初我以为是寻路参数调得不对,连续改了好几种AcceptanceRadius都没见效。后来在编辑器里按了一下P键显示导航网格,问题一下就清楚了——NavMesh边界和场景里墙体的碰撞体组件之间存在明显空隙,准确说就是“碰撞体组件与导航网格结果存在偏移”。这类偏移在UE5项目里非常常见,尤其是从旧版升级上来的项目,或者场景里大量使用自定义碰撞体的项目。它表面上看是AI寻路异常,本质上是NavMesh生成时的几何体输入链路上某个环节出了问题。这篇文章不绕弯子,直接把我定位问题和修复方案的完整链路拆开讲,分机制、排查、修法三层讲透。
1. 现象定级:先把“偏移”拆成四种互不相干的病
很多人一上来就怀疑是导航网格的Agent参数不对,要么是RecastNavMesh的Cell Size太大,要么是Agent Radius太大。但“偏移”这个词太笼统了,实际操作里我至少遇到过四种完全不同的表现,对应的根因和处理方式也完全不同。不先把现象拆开,后面所有操作都是盲猜。
1.1 整体收缩:先怀疑Agent,别急着改碰撞体
最典型的一种偏移是:整个可走区域的边界比碰撞体外表面缩进去一圈,大小基本均匀,像一个等比例缩小的版本。这种“瘦了一圈”的效果,十有八九是Agent Radius造成的。UE5里RecastNavMesh默认的Agent Radius是42厘米,导航网格生成时会把所有阻挡边界朝外部扩张、把可行走区域朝内部收缩,收缩量就等于Agent Radius。所以你在编辑器里看到NavMesh边界离墙一段距离,不是渲染错误,而是导航网格主动给AI留出“身位”。这是正常现象,只有当你觉得收缩量过大、AI离墙太远时,才需要处理。判断方法很简单:看偏移是不是处处均匀,如果墙体厚薄不同导致偏移量不一致,那另有原因。
1.2 局部缺口:多半是某个碰撞体掉队了
第二种表现是局部性的:场景里大部分区域NavMesh边界和碰撞体都贴合得很好,偏偏某一段墙体、某个柱子或者某块地板周围出现了明显的多余空隙,甚至AI会从那里直接穿过去。这种半路掉链子的情况,通常是这个局部物体本身的碰撞体有问题——要么是它的碰撞预设把Navigation通道设成了Ignore,要么是碰撞体组件的位置没跟上Actor的根组件。注意,这里和1.1的最大区别是“不均匀”:别的墙没问题,就这一处有问题。定位时不需要把整张NavMesh的参数翻一遍,只需要找到那个掉队的物体。
1.3 完全消失:碰撞预设或几何体提取模式把物体“屏蔽”了
更严重的情况是某个碰撞体明明在场景里,NavMesh却完全不给它面子,直接穿过去。比如一个门框,碰撞体看得到,但生成的NavMesh直接穿门而过。这已经不是偏移,而是“看不见”了。常见原因就两个:一是碰撞体的Collision Response对Navigation通道不是Block,导航系统在提取几何体时压根不认为它是一堵墙;二是该物体被某种几何体提取模式排除在外了。后面第2章会专门讲这条链路,这里先记住一个结论:能被NavMesh看见的,不是“渲染网格”,而是“参与碰撞的几何体”,而且必须是Navigation通道认可的那个碰撞体。
1.4 边缘锯齿:凸包分解的近似代价
还有一种偏移是靠近曲面、斜面或者圆形柱子时,NavMesh边缘呈阶梯状,和碰撞体表面之间有一小段周期性误差。这不是碰撞体摆歪了,也不是Agent半径统一收缩,而是碰撞体本身在生成凸包时就是近似的。UE5里很多自动碰撞体本质是一组凸多面体,用它们去逼近凹面或弧面时,天然会有一层“贴皮”误差。NavMesh再基于这层凸包表面做边界判断,误差就会叠加一层。所以你在场景里看到一条弧墙,NavMesh边缘却是一段一段直线拼出来的,非常正常,只是误差大到能被人眼看出来的时候,才值得处理。
把现象定级做完之后,我的习惯是立刻做一个几秒钟的实验:把Agent Radius临时改成0,重新生成NavMesh。如果所有偏移瞬间消失,说明整张图就是半径收缩问题;如果依然有不均匀偏移,说明几何体输入本身有问题。这个实验是后面所有排查的前提,所以我把它写在了第3章的第一步里。
2. 碰撞体到NavMesh的提取链路上,谁在动手脚
要搞明白偏移为什么发生,就得先知道NavMesh生成时是怎么从场景里“拿走”几何体的。简单说,UE5的导航系统不是一个通用的物理引擎,它不会照着屏幕上所有网格体去构路,它只认自己能看到的那部分碰撞数据。这个“看到”的过程分为三个环节,任何一个环节出了问题,最终呈现出来的NavMesh边界就会和碰撞体不一致。
2.1 碰撞预设三层开关:Object Type、Collision Enabled、Collision Responses
碰撞体组件上有一整套预设,表面上看就是下拉框里的几个选项,实际背后是三套独立开关在共同决定结果。
第一层是Collision Enabled,表示这个组件是否参与碰撞计算。如果它是No Collision,那导航系统在提取几何体时直接跳过它。第二层是Object Type,就是给碰撞体一个身份标签,比如WorldStatic、Pawn、PhysicsBody等等。第三层是Collision Responses,定义了这个碰撞体对各类通道的响应方式,其中就包括Navigation通道,也就是ECC_Navigation。
对NavMesh来说,最关键的永远是第三层:这个碰撞体对Navigation通道的响应是不是Block。只有当响应是Block时,导航系统才把这个碰撞体的表面当作障碍物边界。如果响应是Overlap或者Ignore,导航系统就会觉得“这里可以走”,NavMesh自然就穿过去了。很多偏移问题,说到底就是碰撞预设里的某一层被改成了Ignore,但开发者对着渲染网格看了半天,觉得“碰撞体明明好好的啊”。
打个比方:碰撞体是酒店的一扇门,Collision Enabled决定这扇门存不存在,Object Type决定门卫按哪类访客标准拦人,Collision Responses就是最终要不要给你放行的门禁。Navigation系统是个借道访客,门禁刷不开,它就绕道走,不会站在门口跟你吵。
2.2 几何体提取模式:谁有资格进入NavMesh的“视野”
第二道关卡是导航系统自身的几何体提取策略。UE5的RecastNavMesh构建时,并不会把场景里所有静态网格体全部拿去跑一遍体素化,而是遵循一套默认的“候选名单”。名单里主要有三类:静态网格体上的简单碰撞体、场景里的Blocking Volume、以及Navigation相关的体积如NavMesh Bounds Volume、NavModifierVolume。
重点注意“简单碰撞体”这四个字。UE5里静态网格体的碰撞体分两类:一类是简单碰撞体,比如Box、Sphere、Capsule、Convex Hull;另一类是复杂碰撞体,本质是渲染网格的三角形面片。NavMesh在默认情况下优先使用简单碰撞体来做体素化输入,因为复杂碰撞体的三角形数量太大,速度慢而且体素化误差反而明显。如果一个网格体没有可用的简单碰撞体,导航系统才会退而求其次去用复杂碰撞体。
这里有一个很容易踩的坑:在StaticMesh编辑器里勾选了“Use Complex Collision as Simple Collision”之后,物理系统会用网格三角形面片当作简化碰撞来用,但同时NavMesh也会把这个复杂碰撞体当作几何输入。这时候你会发现,渲染网格和NavMesh边界之间的误差,是三角形化带来的锯齿误差,而不是Agent半径收缩那种均匀偏移。所以看到锯齿边界,优先去检查StaticMesh的碰撞几何体形态。
2.3 凸包分解在曲面和斜面处制造了多少误差
第三个环节是凸包分解的近似误差。很多模型导入UE5时,自动生成的碰撞体是凸包(Convex Hull)形式。凸包的本质是用一个凸多面体去包裹原始网格的所有顶点,所以它天然无法完美贴合凹面。一个L型房间,用单个凸包包裹,凹进去的那个角就会被填平;一根圆柱,用凸包逼近,侧面就会变成多边形。NavMesh拿到这种凸包后,会在它的表面基础上再做边界判定,误差于是被继承甚至放大。
凸包误差体现在场景里,就是NavMesh边缘贴着碰撞体表面走,但走近看是一条条折线。好消息是,这类误差在大多数情况下不会影响AI的宏观寻路,只有当你做的是AI贴墙走、沿走廊边防撞这种对边界精度敏感的功能时,才会觉得别扭。坏消息是,一旦到了要处理的时候,单纯调NavMesh参数已经没用了,必须回头改StaticMesh里的凸包生成参数或者换成碰撞体类型。
3. 我的定位过程:从改Radius到查凸包,一步都没白走
我处理这个题目时有一整套固定流程,按成本从低到高排列,每一步都能排除一个最可能的变量。这套流程虽然看起来有点“笨”,但不会漏,也特别适合给项目里的美术和策划同事同步排查进度。
3.1 第一步:把Agent Radius调成0做区分实验
这个实验是整个定位过程的分水岭。在Project Settings的Navigation Mesh分类里,找到Supported Agents,把默认Agent的Radius从42改成0,然后回到场景重新Build Paths。注意,改这个参数不影响项目运行时用的物理碰撞,只是让NavMesh生成时不再主动内缩边界。如果改完之后,NavMesh边缘和碰撞体完全贴合,所有偏移消失,那问题就锁定了:根因就是Agent Radius过大,不需要再去碰任何碰撞体。如果改完之后,偏依依然存在,或者只是变好了一部分但没完全贴住,说明几何体输入本身就有问题,继续往下查。
这个实验的好处是零风险、耗时短,而且能做横向对比。我通常会在调完Radius后马上截一张图,记录NavMesh边界和墙的间距,方便后续和修复后的结果做对比。
3.2 第二步:逐个隐藏碰撞体定位元凶
如果问题集中在局部区域,第一步实验往往不能完全消除偏移,这时候就要开始“点名”了。我会在场景中选中可疑Actor,在细节面板最下面找到“Can Ever Affect Navigation”选项,把勾去掉,然后重新Build Paths,观察刚才那个偏移区域有没有变化。
这个操作之所以有效,是因为Can Ever Affect Navigation是导航系统判断一个物体要不要参与NavMesh生成和维护的开关。取消勾选之后,这个物体的几何体就不会再被当作NavMesh的边界来源了。如果去掉这个勾以后偏移区域反而被NavMesh忽略、更不正常了,那就说明这个物体确实参与了边界判定,问题就出在它的碰撞体上;如果去掉之后NavMesh边界完全没变,那这个物体大概率不是元凶,继续查下一个。
这个方法比直接删除Actor安全,因为它不会破坏场景里的其它引用,勾回来也很方便。我在一个大房间里排查时,就靠这个开关,配合Ctrl+Shift+滚轮快速缩放视角,花了不到半小时就把问题锁定在了一个门框的碰撞预设上。
3.3 第三步:用可视化工具直接看导航输入边界
有时候光看绿色NavMesh和墙面之间的空隙,还是判断不了到底是哪一侧出了问题。这时候我会借用一个更直观的工具:临时加一个Blocking Volume到场景里,然后在它的周围生成NavMesh,观察边界是贴着Blocking Volume表面走,还是留着原来的偏移。Blocking Volume是一种纯阻挡体积,它没有任何渲染网格干扰,生成NavMesh时边界应该非常清晰。如果它也有偏移,大概率是Agent参数;如果它没有偏移,说明你在排查的那个原始碰撞体有问题。
另外,UE5里按P键可以直接显示NavMesh,但这个显示方式比较粗糙。要看更细致的边界线,我一般会在Build Paths之后,项目的显示设置里打开Navigation相关的ShowFlags,或者直接指定一个很小的Agent Radius把NavMesh边界物化出来。配合Editor里的Camera Speed调整,能看出偏移到底是几个像素还是肉眼可辨的大裂缝。
3.4 第四步:检查静态网格体的碰撞几何设置
前三步之后,如果还没找到根因,我会直接打开出问题的StaticMesh资产,在它的Collision面板里看一遍。重点关注两件事:一是碰撞体是简单碰撞体还是复杂碰撞体;二是是否勾选了“Use Complex Collision as Simple Collision”。
这一步能解释很多“看着是同一个模型,在A场景正常、在B场景偏移”的灵异现象。因为同一个StaticMesh可以在不同Actor上拥有不同的碰撞覆盖项,而覆盖项的设置可能继承自测试时某个临时改动的数据。我见过一个项目里,某个柱子模型在编辑器里一直用的是Box简单碰撞体,边界很干净;但策划在另一个关卡里手工覆盖成Complex后,NavMesh边缘立刻变得参差不齐,整面墙都像被啃过一样。这种问题不改模型本身,只调整碰撞体设置就能解决。
4. 修法汇总:不同偏移原因对应的参数调整清单
定位到具体原因之后,修复方案基本就是一页纸的事。但这里面的参数取舍和操作顺序都有讲究,我按原因分四类整理成下面的清单,可以直接照用。
4.1 针对Agent半径收缩:折中而不是一刀切
如果你确认是Agent Radius导致的均匀收缩,最简单的做法确实是把它调小,但别直接改成0。Agent Radius的作用是让AI在窄缝面前“知道”自己过不去,如果把它设成0,AI会试图钻进和自己身体一样宽的缝隙里,然后在物理碰撞和寻路之间反复横跳,表现出来就是贴着墙疯狂抖动,比偏移更像bug。
我的经验是:Agent Radius设成角色胶囊体半径的0.8到1.0倍。比如角色的胶囊体半径是40厘米,那Agent Radius就设在32到40之间,这样既不会让AI在走廊里走“太空步”,也不会让它把自己挤死。注意,这里说的是“胶囊体半径”,不是“胶囊体宽度”,很多人把直径当半径填进去,导致收缩量直接翻倍。
4.2 针对碰撞体位置和变换的修正
如果排查下来是某个碰撞体的位置不对,比如碰撞体中心线偏移到了Actor的另一侧,那问题往往不是出在场景里拖拽,而是出在静态网格体的导入阶段。模型在DCC工具里建模时,碰撞体节点可能和渲染网格节点不在同一个原点,导出FBX后UE5会保留这个偏移。遇到这种情况,我一般会回到StaticMesh编辑器里,选中Collision相关的凸包或简单碰撞体,右键选择“Transform”相关选项,把碰撞体的局部坐标归零或者对齐到渲染网格的包围盒。
在场景里硬拖碰撞体是下策。因为场景里看到的位移是相对于Actor的,一旦Actor被合并到另一个Actor底下,或者在蓝图里被重新Attach,偏移就会原形毕露。
4.3 针对几何体提取和预设的方案
如果是碰撞预设导致NavMesh“看不见”某个障碍,我会打开这个Actor的碰撞设置,按下面的清单逐项检查:
- Collision Enabled:必须是Query Only或者Query and Physics,不能是No Collision
- Object Type:保持默认的WorldStatic或可阻挡类型,不要设成Pawn这种角色类型
- Collision Responses:Navigation通道必须为Block
- Can Ever Affect Navigation:必须勾选
这里面最隐蔽的是第四项。有些静态网格体从外部导入时,Can Ever Affect Navigation默认没勾选,地图又大,你根本不会注意到。等Build Paths出来,这个物体的边界就是“消失”的。补上勾选之后,记得重新Build Paths,光标移到Navigation菜单下点Build Paths,或者按快捷键Shift+N。
4.4 针对凸包精度的取舍
如果偏移是凸包近似误差导致的,我通常会在StaticMesh编辑器里重新生成凸包,把参数往高了调。具体来说,在Collision面板里选择生成凸包时,可以看到Max Hull Vertices(最大凸包顶点数)、Max Hulls(最大凸包数量)和Accuracy(精度)这几个参数。曲面越复杂,越需要增加Hull数量和顶点数。但注意,这不是免费的午餐:凸包越精细,物理查询和Navigation构建的开销就越大。我一般控制在Max Hulls 4到16、Max Hull Vertices 8到16之间,只有在个别造型复杂的柱子、雕像上才会上调。
下面这张表是我通常给项目组用的修法速查,你把定位到的现象对进去选方案就行:
| 偏移特征 | 最可能原因 | 首选修复手段 |
|---|---|---|
| 全部边界均匀内缩 | Agent Radius过大 | 按胶囊体半径0.8~1.0倍重设Agent Radius |
| 某个局部区域边界异常 | 该Actor的碰撞预设错误或Can Ever Affect Navigation未勾选 | 检查Navigation通道响应,重新勾选开关并Build Paths |
| 某个模型完全被穿过 | 碰撞体类型不是Block或几何体提取不到 | 检查Collision Enabled、Object Type和Response三层开关 |
| 曲面/弧面处边缘锯齿 | 凸包近似误差 | 调整StacticMesh凸包生成参数或改用Capsule/Box简单碰撞体 |
| 运行时动态生成的障碍物不生效 | 移动组件未开启Can Ever Affect Navigation | 开启该开关,或添加NavModifierComponent |
| 打包后偏移比编辑器里明显 | LOD切换导致碰撞体变化 | 检查StaticMesh各LOD的碰撞体配置,锁定使用同一套碰撞 |
5. 这些坑我都踩过:动态阻挡、关卡合并和打包后的偏移
前四章解决的是静态场景里最常见的偏移问题。但项目一旦跑起来,还会遇到几个特别隐蔽的变种,这些变种不会让你在编辑器里一眼看到,而是等到运行时才暴露,排错成本高很多。我把自己实际踩过的三个坑单独列出来。
5.1 动态运行时阻挡为什么总是漏
很多项目里有可移动的门、临时板条箱、升降平台这类运行时Spawn出来的碰撞体。它们的偏移现象是:在编辑器里生成NavMesh时一切正常,但游戏跑起来,门一开一关,AI路径却依然把“关着的门”当作可穿越区域。问题通常出在移动组件上——CharacterMovementComponent或者简单的MovementComponent里有一个“Can Ever Affect Navigation”选项,如果没勾选,导航系统根本不会因为这个物体的移动而重新计算局部NavMesh。你看着碰撞体在物理世界里挡住了角色,但在导航系统眼里,它就跟不存在一样。
对策有两个方向。一个是给这类动态阻挡提加上NavModifierComponent或者动态障碍组件,让它们在移动时主动通知导航系统刷新局部遮挡;另一个是确保移动组件上那个Can Ever Affect Navigation被勾选,并在蓝图里调用NavigationSystem的相关接口来刷新。二选一即可,别同时用两套,否则局部更新频率会失控,对性能有影响。
5.2 关卡Sublevel合并后NavMesh没跟上
World Partition和Sublevel工作流是目前UE5项目的主流。Sublevel在编辑器里单独打开时,每个碰撞体相对于自己的关卡原点是没有问题的,但一旦把多个Sublevel合并进同一个持久关卡,再统一Build Paths时,某些碰撞体可能出现相对位置变化。这种情况下的偏移特征是:不同区域的NavMesh边界缩放程度不同,有的地方甚至出现大块缺失。
遇到这种情况,我一般不会去细查某一个碰撞体,而是直接把整张NavMesh删掉,重新Build Paths。别想着“只重新生成哪一块”,RecastNavMesh的局部刷新在处理合并关卡时经常会留下陈旧的边界数据,与其跟它较劲,不如全量重建一次,通常10分钟就能解决。重建后,再按第3章的流程排查一遍局部偏移,基本能定位到具体的碰撞体。
5.3 打包后偏移放大的疑案
这个坑我最狼狈。项目在编辑器里怎么验都正常,打包出来一跑,AI全在贴墙转圈。最后发现是LOD的问题:静态网格体在高模LOD下配置了精细碰撞体,但运行时游戏为了性能切到了低模LOD,而低模LOD上的碰撞体配置和高模完全不一样。NavMesh虽然是在编辑器里基于高模碰撞体生成的,但运行时物理和遮挡判定却用了低模碰撞体,于是偏移就“凭空”出现了。
排查思路是:在打包版本里把渲染质量切到最高,强制使用最精细的LOD,看AI行为是否恢复。如果恢复,问题就锁定在LOD切换上。解决方案是在StaticMesh编辑器的LOD设置里,把碰撞体统一成一套,或者关掉LOD对碰撞体的影响。UE5里碰撞体默认是不随LOD切换的,但如果你在某个LOD上手动添加了碰撞体,或者开启了Complex作为Simple,就会打破这个默认行为。
最后再分享一个排查口诀
这个偏移问题看着小,但凡是做AI寻路或者角色自动导航的项目,基本都会在某个版本踩一次。我现在的习惯是把这个问题的排查流程固化成一张自查清单:先改Agent Radius做区分实验,再按Can Ever Affect Navigation逐物体点名,接着查StaticMesh的碰撞几何体和凸包配置,最后才动NavMesh参数。绝大多数项目跑完这套流程,都能在半小时内定位到根因。
最后再给你一个省钱的小技巧:排查偏移前,先在场景里放一个Blocking Volume作为参照物。只要NavMesh边界对这个参照物的贴合程度是正常的,就能快速区分是“全局Agent参数问题”还是“单个碰撞体问题”,省得在项目里绕着墙跑半天还看不出所以然。