1. 项目概述:当AI敌人开始“穿墙术”和“卡墙角”
在Unity3D里做游戏,尤其是带敌人AI的,NavMesh(导航网格)绝对是绕不开的核心系统。它让开发者能快速实现寻路,省去了自己写A*算法的麻烦。但用过的朋友都知道,这东西用起来简单,想用“好”却处处是坑。最典型的就是标题里提到的:敌人走着走着就卡在墙角不动了,或者像幽灵一样直接穿过了本应阻挡它的薄墙、家具。更头疼的是,当场景里有会移动的障碍物,比如被玩家推开的箱子、倒下的柱子,AI要么傻站着等,要么就一头撞上去,逻辑混乱得一塌糊涂。
这些问题,本质上都不是NavMesh的“Bug”,而是我们对这套系统的工作原理和参数配置理解不够深入。NavMesh Agent(导航代理)和Nav Mesh Obstacle(导航网格障碍物)这两个组件,它们各自有一套行为逻辑,如果设置不当或者组合使用有误,就会产生各种诡异现象。我见过不少项目,AI的“智障”表现直接拉低了整个游戏的体验,而开发者往往花费大量时间在调整动画、状态机,却忽略了最底层的导航问题。
这篇指南,就是把我这些年踩过的坑、调试出的经验,系统地梳理一遍。我们会从NavMesh的基本工作原理讲起,重点拆解“卡墙”和“穿模”这两个高频问题的根因,最后深入动态障碍物的配置逻辑,让你不仅能解决问题,更能理解背后的“为什么”。无论你是刚接触Unity AI的新手,还是被这些问题困扰已久的老手,相信都能找到直接的解决方案和配置思路。
2. NavMesh核心机制与问题根源剖析
要解决问题,必须先理解问题从何而来。Unity的NavMesh系统不是一个“魔法黑盒”,它是一套有明确规则和计算周期的寻路方案。
2.1 NavMesh是如何工作的:烘焙、查询与移动
首先,你需要明白NavMesh是一个静态数据。你通过Window > AI > Navigation窗口,设置好参数后点击“Bake”,Unity就会根据场景中的静态碰撞体(Static Collider)生成一张覆盖可行走区域的三角网格图。这个过程叫“烘焙”。Agent(比如你的敌人)就在这张网格图上移动。
Agent的移动分三步:
- 路径查询(Pathfinding):当你调用
agent.SetDestination(target.position)时,系统会在NavMesh网格上,从Agent当前位置到目标位置,计算出一条由一系列拐点(Corners)组成的路径。这个过程相对耗时,所以不会每帧都做。 - 转向与速度控制(Steering & Velocity):Agent根据计算出的路径,结合自身的
Speed、Angular Speed(角速度)、Acceleration(加速度)等参数,计算出一个期望的移动速度向量。 - 避免碰撞(Collision Avoidance):这是防止Agent之间堆叠的关键。Unity使用了一种基于RVO(Reciprocal Velocity Obstacles)的轻量级局部避障算法。Agent会感知周围一定半径(由
Radius和Obstacle Avoidance参数决定)内的其他Agent和障碍物,并微调自己的速度方向来避免相撞。
“卡墙”和“穿模”问题,就潜伏在这三个步骤的细节和交互中。
2.2 “卡墙”问题的三大元凶
敌人走到墙角或复杂地形边缘就卡住不动,通常不是没路,而是路“走不过去”。原因主要有三:
元凶一:Agent的“身体”比想象中大这是最常见的原因。在Navigation烘焙窗口的Agents分页,你可以定义不同“体型”的Agent。这里的Radius和Height决定了NavMesh系统认为这个Agent的物理尺寸。同时,Agent组件上也有一个Radius参数。
关键点:Agent实际用于寻路和避障的半径,取的是这两个地方设置的最大值。如果你在烘焙时定义了半径为0.5米,但Agent组件上的半径是0.7米,那么系统会按0.7米来算。 当路径经过一个狭窄的通道时,系统会检查通道宽度是否大于
Agent Radius * 2。如果通道是直角墙角,可用空间更小。Agent走到那里,计算发现自己的“身体”过不去(即使视觉模型很小),就会在原地不断尝试微调,看起来就是“卡住”。
元凶二:路径拐点精度与采样距离NavMeshAgent有个参数叫Auto Traverse OffMesh Link,通常我们关注的是Stopping Distance(停止距离)和Path End。但Path End的精度会影响拐弯。更关键的是agent.path.corners提供的路径拐点精度是有限的。当目标点非常靠近障碍物或墙角时,最后一个拐点可能就在“卡住”的位置附近。同时,NavMeshAgent.SamplePathPosition这个方法用于查询路径位置,如果采样距离设置不当,也可能导致代理在拐点处“犹豫”。
元凶三:局部避障(Obstacle Avoidance)的副作用局部避障本意是好的,但在墙角这种空间受限的地方,它可能帮倒忙。当Agent试图贴着墙走时,避障系统为了不和墙(被识别为静态障碍)碰撞,会施加一个远离墙的力。同时,如果墙角另一边有另一个Agent或动态障碍物,避障系统又会施加另一个方向的力。多个力的矢量叠加,可能导致合速度接近于零,Agent就“僵住”了。将Obstacle Avoidance Type设置为“None”或“Low Quality”有时反而能解决墙角卡顿,但会失去群体避障效果,需要权衡。
2.3 “穿模”问题的本质:碰撞体与导航体的分离
穿模问题更让人恼火,明明有墙,敌人却视若无睹地穿了过去。这里99%的情况是配置错误:
墙体没有参与NavMesh烘焙:这是最低级的错误。确保你的墙、柱子等障碍物的GameObject在Inspector右上角,导航静态标志(Navigation Static)是勾选的。并且,在Navigation窗口的Object分页,该物体的“Navigation Area”被设置为“Not Walkable”。这样烘焙时,这些区域就不会生成可行走网格,Agent从根本上就无法计算穿过它的路径。
Agent的碰撞体与导航体不匹配:这是高级错误。你的敌人模型可能有一个
Capsule Collider用于物理碰撞,但NavMeshAgent组件有自己的“虚拟身体”。如果NavMeshAgent的Radius设置得远小于视觉模型或物理碰撞体,那么系统认为的“可通行空间”就会大于实际视觉/物理空间。在复杂地形或高速移动下,Agent可能计算出一条从视觉上看会穿墙,但在导航网格上却合法的路径。解决方案是确保NavMeshAgent的Radius和Height与模型主碰撞体大致匹配。OffMesh Link的滥用:OffMesh Link用于连接不相邻的导航网格,比如跳下高台、跳过沟壑。如果你错误地创建了OffMesh Link,连接了两个本应被墙隔开的区域,Agent就会直接“传送”过去,看起来像穿模。检查场景中是否有非预期的OffMesh Link。
3. 静态场景避坑:烘焙参数与Agent设置的黄金法则
理解了原理,我们就可以针对性地调整。对于静态场景(地形、建筑),优化配置可以杜绝大部分问题。
3.1 导航烘焙参数精细化配置
不要使用默认参数直接烘焙。打开Navigation窗口(Window > AI > Navigation),关注这几个面板:
Agents面板:定义Agent类型在这里预设不同体型敌人的导航参数。我强烈建议至少创建两种:一种用于普通小体型敌人(如人类,Radius 0.25-0.3m, Height 1.8m),一种用于大型敌人或Boss(Radius 0.5-1m, Height 2.5-4m)。
- Name: 如“Humanoid”,“LargeMonster”。
- Radius: 这是最重要的参数。它必须大于或等于你模型中用于物理碰撞的主碰撞体半径。对于人形角色,0.25-0.3是安全值。设置太小会穿模,太大会卡门。
- Height: 角色身高。决定Agent能否通过低矮空间。
- Step Height: 可跨越的最大台阶高度。设置合理(如0.3-0.4)可以让角色自然走上楼梯而不是寻路绕开。
- Max Slope: 可爬行的最大坡度。45-60度是常见值。
Areas面板:定义区域成本你可以将不同的表面标记为不同区域(如“道路”、“草地”、“沼泽”),并设置不同的通行成本(Cost)。Agent寻路时会选择总成本最低的路径。这可以用来让AI优先走大路。确保所有不可行走的物体(墙、家具)都被分配到“Not Walkable”区域。
Bake面板:控制网格生成质量
- Agent Radius: 这里要和你上面定义的Agent类型半径一致。它决定了烘焙时网格距离障碍物边界的收缩距离。值越大,生成的可行走区域离墙越远。
- Advanced > Height Mesh:务必勾选。这个选项会为导航网格生成高度信息。对于有楼梯、斜坡、不平整地面的场景,不勾选此选项会导致Agent在上下坡时严重抖动、卡住,甚至从斜坡上“滑”下去。勾选后,Agent会沿着网格表面高度移动,行为自然得多。这是解决复杂地形卡顿的关键。
- Advanced > Voxel Size: 体素大小,影响烘焙精度和速度。值越小精度越高,但烘焙越慢。对于大多数场景,默认值即可。如果场景中有很多细小缝隙,可以适当调小(如0.05),但会显著增加烘焙时间。
3.2 NavMeshAgent组件关键参数详解
为你的敌人GameObject添加NavMeshAgent组件后,需要根据其类型精细调整:
- Agent Type: 选择你在Agents面板中定义的预设(如“Humanoid”)。
- Base Offset: 代理中心相对于模型原点的Y轴偏移。如果你的模型原点在脚底,这个值通常是
Height / 2。 - Speed/ Angular Speed/ Acceleration: 移动参数。
Angular Speed(角速度)过低会导致敌人在拐弯时转身缓慢,显得“卡顿”。适当提高(如120-360)可以让转向更灵敏。 - Stopping Distance: 停止距离。在距离目标多远处开始减速停止。对于攻击敌人的AI,这个值可以设为攻击范围半径,这样敌人走到攻击距离就会停下,而不是试图“贴脸”。
- Auto Braking: 是否自动刹车。如果关闭,Agent到达目标点附近不会减速,会冲过头然后折返,产生“鬼畜”移动。通常保持开启。
- Obstacle Avoidance:
- Radius: 再次强调,这个值应该与物理碰撞体半径、烘焙Agent半径相匹配。它是避障计算的依据。
- Quality: 避障质量。
High质量效果好但耗能高,在大量Agent时建议用Medium或Low。对于墙角卡顿问题,可以尝试降低质量或设为None进行测试。 - Priority: 避障优先级(0-99)。值越低,优先级越高。可以设置玩家控制的角色为高优先级(低数值),让敌人AI主动避让玩家。
一个黄金检查清单:
- 模型物理碰撞体(如CapsuleCollider)的
Radius是多少?记为R_phy。 - Navigation烘焙面板中,所用Agent类型的
Radius是多少?记为R_bake。 - NavMeshAgent组件上的
Radius是多少?记为R_agent。 - 确保R_agent >= R_phy,且R_agent 与 R_bake 相等或接近。最安全的做法是让这三个值保持一致。
4. 动态障碍物的核心:NavMeshObstacle深度解析
静态问题解决了,动态的才是真正的挑战。Unity提供了NavMeshObstacle组件来处理移动的障碍物,但它的两种模式(Carve和Non-Carve)和行为逻辑很容易用错。
4.1 两种模式:障碍(Obstacle)与雕刻(Carve)
NavMeshObstacle组件有两种影响NavMesh的方式,通过Carve复选框切换:
1. 障碍模式(Carve未勾选)
- 行为:此模式下,障碍物不会在NavMesh上挖洞。它仅作为一个“力场”或“排斥体”存在。
- 工作原理:NavMeshAgent的局部避障系统会感知到它,并试图绕开。就像现实中你绕过一个个行走的路人。
- 优点:性能开销极低,因为不需要动态修改导航网格。
- 缺点:避障效果是局部的、反应式的。如果障碍物完全堵死了狭窄的通道,后面的Agent可能会因为无法规划出绕行路径而集体卡住。它只有简单的“排斥”逻辑。
- 适用场景:持续移动的物体,如玩家角色、其他由AI控制的NPC、不断巡逻的敌人。对于这些物体,你希望AI动态避让,而不是在导航网格上永久挖个洞。
2. 雕刻模式(Carve勾选)
- 行为:此模式下,当障碍物静止时,会在NavMesh上“雕刻”出一个不可行走的洞。当它移动时,这个洞会跟着移动(根据设置)。
- 工作原理:它直接修改了导航网格的数据结构。寻路器(Pathfinder)在计算全局路径时,会直接避开这个被挖掉的区域。
- 优点:提供全局的、预测性的避障。AI在很远的地方规划路径时,就知道要绕开这个障碍物所在区域。
- 缺点:性能开销大。每次雕刻(挖洞)和更新洞的位置,都需要重新计算局部导航网格,是CPU密集型操作。
- 适用场景:会移动但最终会停下来的物体,比如被玩家推开后静止的箱子、被炸倒的树干、可破坏的墙壁碎块。你需要AI长期将这个物体的位置视为不可通行区域。
4.2 关键参数详解与配置策略
勾选Carve后,下面几个参数决定了雕刻行为的细节:
- Move Threshold(移动阈值):这是判断障碍物“是否在移动”的阈值。如果障碍物在一帧内的移动距离小于此值,则被视为“静止”;大于此值,则被视为“移动”。这个值不宜设置过小(如0.001),否则轻微的物理抖动都会触发昂贵的网格更新。对于大多数情况,0.1到0.5是一个合理的范围。
- Time To Stationary(静止时间):障碍物需要停止移动多久,才会被系统正式认定为“静止”状态并开始(或重新)雕刻。这避免了障碍物只是短暂停顿(比如箱子被推了一下晃了晃)就立刻挖洞,造成性能浪费和路径抖动。通常设置为0.5秒到1秒。
- Carve Only Stationary(仅雕刻静止物体):这是最重要的一个选项,也是性能优化的关键。
- 勾选时(推荐):障碍物只在被认定为“静止”状态时,才会雕刻NavMesh。在移动过程中,它仅作为障碍物(Obstacle)模式工作,由Agent的局部避障来处理。这是绝大多数场景的最佳实践,兼顾了效果和性能。比如一个被推的箱子,移动时AI会躲开它,等它停稳了,AI的寻路路径就会永久绕过它。
- 取消勾选时:障碍物在移动时也会雕刻NavMesh,并且这个“洞”会随着障碍物移动而更新。这会产生非常精确的路径规避(比如一大群士兵绕过一辆缓慢移动的坦克),但性能代价极高,只适用于少量、关键的大型移动障碍物。
形状(Shape)选择:
- Box:适用于方形物体,如箱子、柜子。
- Capsule:适用于人形或圆柱形物体。
- Size/Center:务必调整这些参数,让形状尽可能紧密地包裹住你的障碍物模型。过大的形状会导致不必要的导航网格浪费,过小则会导致AI规划出的路径过于贴近障碍物,容易发生碰撞。
4.3 动态障碍物配置实战案例
假设我们有一个场景:玩家可以推动场景中的木箱(带有Rigidbody和Box Collider),敌人AI需要避开这些箱子。
正确的配置步骤:
- 为木箱预制体添加组件:为木箱的GameObject添加
NavMeshObstacle组件。 - 设置形状与尺寸:将
Shape设为Box。点击组件上的Edit Collider按钮(一个小圆点),然后直接在Scene视图中拖动Gizmo,使绿色线框的盒子与木箱的视觉模型和物理碰撞体基本重合。或者手动设置Center和Size,使其匹配。 - 启用雕刻并配置参数:
- 勾选
Carve。 - 勾选
Carve Only Stationary。(这是关键!) - 设置
Move Threshold = 0.3(箱子移动超过0.3米才算动)。 - 设置
Time To Stationary = 0.8(箱子停止移动0.8秒后才算真正静止,开始挖洞)。
- 勾选
- 处理物理交互:确保木箱的
Rigidbody的Interpolate设置为Interpolate,以减少物理抖动。这可以防止因微小抖动频繁触发NavMeshObstacle的状态判断。
这样配置后的运行时行为:
- 玩家开始推箱子(箱子移动距离 > 0.3米):
NavMeshObstacle进入“移动”状态,不雕刻网格。附近的敌人AI通过局部避障系统实时躲开箱子。 - 玩家松开箱子,箱子因摩擦力停下:计时开始。
- 箱子静止时间超过0.8秒:
NavMeshObstacle被认定为“静止”,开始在NavMesh上雕刻一个洞。所有敌人AI重新规划路径时,都会永久绕开这个箱子所在的位置。 - 玩家再次推动箱子:一旦移动距离超过0.3米,雕刻的洞立刻消失,障碍物切回“移动”状态,仅通过局部避障影响AI。
这套配置完美平衡了效果与性能,是处理动态障碍物的标准做法。
5. 高级技巧与复合问题解决方案
掌握了基础配置,我们来看一些更复杂的情况和优化技巧。
5.1 处理门、电梯等可开关的通道
对于一扇可以打开和关闭的门,你不能简单地将门本身设置为NavMeshObstacle,因为当门打开时,它不应该阻挡路径。
解决方案:使用两个导航网格区域
- 在门的位置放置一个透明的
GameObject,比如一个Cube,将其缩放调整为门洞大小。 - 将这个Cube的
Navigation Area设置为“Not Walkable”,并标记为Navigation Static。 - 烘焙导航网格。此时门洞是不可通行的。
- 运行时,当门打开时,通过脚本动态修改这个Cube的
Navigation Static状态,并将其Navigation Area改为“Walkable”。 - 调用
NavMeshSurface组件(如果你使用NavMesh Components包)的BuildNavMesh()或UpdateNavMesh()方法,局部更新这个区域的导航网格。对于大型场景,局部更新比全局重建性能好得多。
// 示例代码:控制门的通行区域 public class DoorController : MonoBehaviour { public GameObject navBlocker; // 那个作为导航阻挡的Cube private NavMeshSurface navMeshSurface; // 引用你的NavMeshSurface组件 void Start() { navMeshSurface = FindObjectOfType<NavMeshSurface>(); // 建议在Inspector中赋值 } public void OpenDoor() { // 1. 取消静态标记,并改变区域为可行走 GameObjectUtility.SetStaticEditorFlags(navBlocker, StaticEditorFlags.NavigationStatic, false); // 注意:运行时不能直接设置Area,需要通过NavMeshModifier组件或NavMeshSurface的Modifiers // 更简单的方法:直接禁用或销毁navBlocker物体 navBlocker.SetActive(false); // 2. 更新导航网格(局部更新) // 如果你使用了NavMeshComponents,可以获取navBlocker所在的NavMeshModifierVolume // 然后调用navMeshSurface.UpdateNavMesh(navMeshSurface.navMeshData, new List<NavMeshBuildSource>(){...}); // 简化方案:对于小型场景或门不多的情况,可以每扇门关联一个小型NavMeshSurface,只更新它。 // 或者,如果性能允许,在开门动画结束后简单调用 NavMesh.SamplePosition 让AI重新寻路。 // 最直接(但较重)的方法: // NavMesh.RemoveAllNavMeshData(); // 慎用,清空所有 // NavMeshBuilder.BuildNavMeshAsync(); // 异步重建 } }注意:动态更新NavMesh是相对昂贵的操作,不要每帧进行。确保在状态改变(如门开关)后,有策略地、低频次地更新。
5.2 解决Agent在狭窄门口“排队”或“抖动”
当多个Agent试图通过同一扇门时,即使路径存在,它们也可能在门口挤作一团,不断调整位置导致集体卡顿。
解决方案:结合OffMesh Link与脚本控制
- 在门的两侧创建一对OffMesh Link,将其
Cost Override设置得比正常行走略高(比如2.0)。这样,AI在寻路时,将“穿过门”视为一个需要“额外花费”的特殊链接,而不是默认的最短路径。 - 编写一个简单的“门交通管理”脚本。这个脚本管理一个队列,每次只允许一个或固定数量的Agent“使用”这个OffMesh Link。其他Agent需要在队列中等待。
- 可以通过禁用其他Agent的
NavMeshAgent组件,或临时设置一个等待目标点,来实现排队逻辑。
// 简化版门队列管理思路 public class DoorQueueManager : MonoBehaviour { public Transform waitPoint; // 门前的等待点 private Queue<NavMeshAgent> agentQueue = new Queue<NavMeshAgent>(); private bool isDoorOccupied = false; private void OnTriggerEnter(Collider other) { NavMeshAgent agent = other.GetComponent<NavMeshAgent>(); if (agent != null && agent.gameObject.CompareTag("AI")) { agentQueue.Enqueue(agent); TryProcessQueue(); } } void TryProcessQueue() { if (!isDoorOccupied && agentQueue.Count > 0) { NavMeshAgent nextAgent = agentQueue.Dequeue(); isDoorOccupied = true; // 让这个Agent通过门 // 可以通过动画、传送或设置路径点实现 StartCoroutine(LetAgentPass(nextAgent)); } } IEnumerator LetAgentPass(NavMeshAgent agent) { // 示例:让Agent移动到门内的一个点 agent.SetDestination(passThroughPoint.position); yield return new WaitUntil(() => !agent.pathPending && agent.remainingDistance < 0.1f); // 假设通过需要1秒 yield return new WaitForSeconds(1.0f); isDoorOccupied = false; TryProcessQueue(); // 处理下一个 } }5.3 性能优化:减少NavMesh更新开销
动态障碍物和实时更新的NavMesh是性能杀手。以下是一些优化准则:
- 严格使用
Carve Only Stationary:这是第一道,也是最重要的防线。确保只有真正需要长期阻挡路径的静止物体才雕刻网格。 - 增大
Move Threshold和Time To Stationary:根据你的游戏节奏,适当调高这两个值。避免因物理引擎的微小抖动或物体的轻微晃动就触发昂贵的网格更新。 - 合并更新:如果有多个动态障碍物可能同时改变状态(比如一场爆炸炸飞多个箱子),不要在每个箱子静止时立即更新NavMesh。可以设置一个计时器,在短时间(如0.5秒)内收集所有需要更新的障碍物,然后一次性触发NavMesh更新。
- 使用NavMesh代理层级(Agent Type):如果小型敌人和大型敌人共享同一个精细烘焙的NavMesh,对大型敌人可能是一种浪费。可以考虑为大型敌人烘焙一个更粗糙(Agent Radius更大)的NavMesh,它们使用自己的网格,更新开销更小。
- 限制同时活动的NavMeshAgent数量:对于远离玩家或不在视野内的敌人,可以禁用其
NavMeshAgent组件,并用一个更简单的脚本(如朝玩家方向直线移动)代替,直到进入激活范围。
6. 调试工具与问题排查实战
理论再好,也要能落地排查。Unity提供了一些强大的可视化调试工具。
6.1 使用Navigation Debug可视化
在Game视图左上角,点击“Gizmos”下拉菜单,你可以找到与Navigation相关的调试显示选项:
- Show NavMesh:显示当前场景中烘焙的导航网格。蓝色区域是可行走的。
- Show NavMesh Agents:显示所有NavMeshAgent的Gizmo,包括其半径、当前路径(一条蓝线)和下一个拐点。
- Show NavMesh Obstacles:显示所有NavMeshObstacle的Gizmo。雕刻模式的障碍物会显示一个挖空的区域,障碍模式的则显示为一个简单的形状轮廓。
调试“卡墙”:
- 开启
Show NavMesh和Show NavMesh Agents。 - 控制敌人走到卡住的位置。
- 观察:
- 敌人的蓝色路径线是否在墙角处中断或折返?
- 敌人的代理Gizmo(一个圆柱体)是否与墙壁的导航网格有重叠?如果有,说明Agent半径太大,或者导航网格在墙角处收缩过度。
- 将
NavMeshAgent的Radius临时调小,看是否解决问题。如果是,就需要重新评估并统一你的半径设置。
调试“穿模”:
- 确保
Show NavMesh开启。 - 走到穿模的墙边。
- 观察墙体的位置在导航网格上是否是一片空白(不可行走)?如果墙下有蓝色网格,说明墙体没有正确设置为“Not Walkable”或未标记为
Navigation Static,需要重新烘焙。 - 观察穿模的敌人,其路径线是否是直线穿过空白区域?如果是,那可能是OffMesh Link错误连接,或者Agent的路径查询 somehow 忽略了该区域。检查是否有意外的OffMesh Link。
6.2 脚本调试与信息输出
在代码中,你可以实时获取并打印NavMeshAgent的状态信息,这对于排查复杂问题非常有用。
void DebugAgentInfo(NavMeshAgent agent) { if (agent == null) return; Debug.Log($"Agent: {agent.gameObject.name}"); Debug.Log($" Path Status: {agent.pathStatus}"); // 路径状态 (PathComplete, PathPartial, PathInvalid) Debug.Log($" Has Path: {agent.hasPath}"); Debug.Log($" Path Pending: {agent.pathPending}"); // 路径是否在计算中 Debug.Log($" Remaining Distance: {agent.remainingDistance}"); Debug.Log($" Is Stalled: {agent.isStopped}"); // 是否被手动停止 if (agent.hasPath) { Debug.Log($" Path Corners: {agent.path.corners.Length}"); for (int i = 0; i < agent.path.corners.Length; i++) { Debug.Log($" Corner {i}: {agent.path.corners[i]}"); } } // 检查是否在NavMesh上 NavMeshHit hit; if (!NavMesh.SamplePosition(agent.transform.position, out hit, 0.1f, NavMesh.AllAreas)) { Debug.LogWarning(" Agent is OFF NavMesh!"); } }当敌人卡住时,调用此函数。如果pathStatus是NavMeshPathStatus.PathPartial,意味着路径计算只完成了一部分,目标点可能不可达。如果remainingDistance很小但agent就是不动,可能是遇到了局部避障的死锁。
6.3 常见问题速查表
| 问题现象 | 可能原因 | 排查步骤与解决方案 |
|---|---|---|
| 敌人卡在墙角/门口 | 1. Agent半径过大。 2. 导航网格在拐角处生成不连续。 3. 局部避障冲突。 | 1. 检查并统一Agent Radius(烘焙设置、组件设置、碰撞体)。 2. 开启 Show NavMesh,检查拐角处网格是否完整。可尝试减小烘焙的Agent Radius或增加Advanced > Max Slope。3. 临时将 Obstacle Avoidance设为None测试。 |
| 敌人穿过薄墙/物体 | 1. 墙体未标记为Navigation Static或Not Walkable。2. Agent半径远小于视觉/碰撞模型。 3. 存在错误的OffMesh Link。 | 1. 检查墙体属性,重新烘焙。 2. 确保 NavMeshAgent.radius>= 模型碰撞体半径。3. 在Scene视图开启 Show NavMesh和Show OffMeshLinks检查。 |
| 动态障碍物(箱子)移动时,AI不躲避或反应迟钝 | 1. 未添加NavMeshObstacle组件。2. Carve Only Stationary被勾选,且障碍物在移动。3. Agent的 Obstacle Avoidance质量太低或半径太小。 | 1. 为障碍物添加NavMeshObstacle。2. 确认需求:若需移动时躲避,应依赖局部避障。确保Agent的 Obstacle Avoidance未禁用,且Avoidance Priority合理。3. 提高 Obstacle Avoidance Quality,增大Agent的Radius。 |
| 动态障碍物静止后,AI仍朝其位置走 | 1.NavMeshObstacle未开启Carve。2. Time To Stationary设置过长,障碍物还未被认定为静止。3. Agent的路径未更新,还在用旧路径。 | 1. 勾选Carve。2. 适当减小 Time To Stationary(如0.5秒)。3. 尝试让Agent重新设置目标点 SetDestination,或调用agent.ResetPath()后重设。 |
| 多个AI在门口挤成一团,都不动 | 局部避障在狭窄空间形成死锁。 | 1. 增加门的宽度(如果设计允许)。 2. 使用交通管理脚本进行排队。 3. 为其中一个AI临时提高避障优先级,或禁用其避障让其先过。 |
| AI上下坡时抖动、滑步 | 导航网格未烘焙高度信息。 | 在Navigation Bake面板,勾选Advanced > Height Mesh,然后重新烘焙场景。 |
| 性能问题,大量动态障碍物时帧率下降 | 1. 过多障碍物开启了Carve且未勾选Carve Only Stationary。2. NavMesh更新过于频繁。 | 1. 为所有非必须的移动障碍物勾选Carve Only Stationary。2. 增加 Move Threshold和Time To Stationary。3. 合并NavMesh更新请求,避免每帧更新。 |
7. 实战:构建一个健壮的敌人AI导航系统
最后,我们整合所有知识点,为一个典型的第三人称动作游戏敌人配置一套健壮的导航系统。假设敌人有“巡逻”、“追击”、“攻击”三种状态。
第一步:场景烘焙准备
- 将所有的地形、静态建筑、大型固定装饰物标记为
Navigation Static。 - 将墙壁、栏杆、大型岩石等不可通行物体的
Navigation Area设为Not Walkable。 - 在Agents面板创建“Humanoid”预设,
Radius=0.3,Height=2.0,Step Height=0.4,Max Slope=45。 - 在Bake面板,确认
Agent Radius为0.3,并勾选Height Mesh。点击Bake。
第二步:敌人预制体配置
- 为敌人预制体添加
NavMeshAgent组件。 Agent Type选择 “Humanoid”。- 设置参数:
Speed=3.5,Angular Speed=360,Acceleration=8,Stopping Distance=1.5(假设攻击距离为1.5米)。 Radius设置为0.3(与烘焙设置一致)。Height设置为2.0。Obstacle Avoidance的Quality设为Medium,Priority设为50(默认)。
第三步:动态障碍物配置
- 为场景中所有可移动的物体(如木箱、油桶)创建预制体。
- 在预制体上添加
NavMeshObstacle组件。 - 设置
Shape匹配物体,调整Center和Size。 - 勾选
Carve和Carve Only Stationary。 - 设置
Move Threshold=0.2,Time To Stationary=0.7。
第四步:编写AI状态机(简化示例)
public class EnemyAI : MonoBehaviour { public Transform[] patrolPoints; public float sightRange = 10f; public float attackRange = 1.5f; private NavMeshAgent agent; private Transform player; private int currentPatrolIndex = 0; private enum State { Patrol, Chase, Attack } private State currentState = State.Patrol; void Start() { agent = GetComponent<NavMeshAgent>(); player = GameObject.FindGameObjectWithTag("Player").transform; // 建议通过Manager获取 PatrolToNextPoint(); } void Update() { float distToPlayer = Vector3.Distance(transform.position, player.position); switch (currentState) { case State.Patrol: if (distToPlayer <= sightRange) { currentState = State.Chase; } // 检查是否到达巡逻点 if (!agent.pathPending && agent.remainingDistance < 0.5f) { PatrolToNextPoint(); } break; case State.Chase: agent.SetDestination(player.position); if (distToPlayer <= attackRange) { currentState = State.Attack; agent.isStopped = true; // 停止导航,开始攻击 } else if (distToPlayer > sightRange * 1.2f) // 加入一点滞后,防止在边界抖动 { currentState = State.Patrol; PatrolToNextPoint(); } break; case State.Attack: // 执行攻击逻辑,比如播放动画、造成伤害 FaceTarget(player.position); if (distToPlayer > attackRange) { agent.isStopped = false; currentState = State.Chase; } break; } } void PatrolToNextPoint() { if (patrolPoints.Length == 0) return; agent.SetDestination(patrolPoints[currentPatrolIndex].position); currentPatrolIndex = (currentPatrolIndex + 1) % patrolPoints.Length; } void FaceTarget(Vector3 target) { Vector3 direction = (target - transform.position).normalized; Quaternion lookRotation = Quaternion.LookRotation(new Vector3(direction.x, 0, direction.z)); transform.rotation = Quaternion.Slerp(transform.rotation, lookRotation, Time.deltaTime * 5f); } // 在OnDrawGizmos中可视化侦查和攻击范围,便于调试 void OnDrawGizmosSelected() { Gizmos.color = Color.yellow; Gizmos.DrawWireSphere(transform.position, sightRange); Gizmos.color = Color.red; Gizmos.DrawWireSphere(transform.position, attackRange); } }第五步:持续调试与优化
- 在Game视图开启
Show NavMesh和Show NavMesh Agents,观察敌人行为是否与预期一致。 - 使用
OnDrawGizmosSelected绘制敌人的感知范围,确保逻辑触发正确。 - 在性能分析器(Profiler)中观察
Navigation和NavMesh.Update的开销,确保动态障碍物没有造成性能峰值。 - 进行边界测试:将玩家引到复杂角落、门边、多个动态障碍物中间,观察AI行为是否出现卡顿、穿模或逻辑错误。
这套组合拳下来,你的敌人AI在绝大多数场景下都应该能表现稳定、自然。记住,NavMesh是一个强大的工具,但它不是“设置完就忘”的魔法。理解其原理,善用调试工具,根据你的游戏具体需求进行微调,才是解决所有导航问题的根本之道。