1. 项目概述:为什么2D寻路需要NavMeshPlus?
在Unity里做2D游戏,尤其是RTS、塔防、俯视角ARPG这类需要大量单位智能移动的项目,寻路是个绕不开的核心功能。很多开发者,尤其是刚接触Unity的朋友,第一反应可能是用Unity自带的NavMesh系统。但上手一用就发现不对劲:Unity的NavMesh是专为3D场景设计的,它的整个工作流,从烘焙(Bake)到代理(Agent)移动,都深度绑定在三维空间里。当你试图在2D的XY平面上使用时,会遇到各种水土不服的问题,比如代理会尝试在Z轴上寻找路径,或者烘焙出的网格无法正确贴合2D碰撞体。
这时候,一个经过社区验证的解决方案就显得尤为重要。NavMeshPlus(也称为NavMesh Components for 2D)正是为此而生。它不是一个全新的寻路引擎,而是对Unity原生NavMesh系统的一个“2D化”扩展。其核心思路非常巧妙:利用Unity 3D NavMesh的强大算法,但通过一系列组件和工具,将2D的“地面”投射或映射到3D空间中进行计算,最终再将结果转换回2D坐标。这样一来,我们既享受了Unity官方寻路系统成熟、稳定、性能优化的红利,又获得了在2D项目中开箱即用的便捷性。
我接手过好几个2D项目,从早期的自己手写A*算法,到尝试各种第三方Asset Store的寻路插件,最后稳定在NavMeshPlus上。原因很简单:它足够轻量、与引擎集成度高、且完全免费开源。对于中小型团队或个人开发者来说,它避免了重复造轮子,也省去了学习和维护一套全新寻路API的成本。简单来说,NavMeshPlus让你能用处理3D寻路的思维和方式,来高效地解决2D游戏的寻路需求。
2. 核心思路与架构拆解
NavMeshPlus的运作机制可以概括为“二维问题,三维求解,二维呈现”。理解这个流程,对于后续的调试和问题排查至关重要。
2.1 核心工作流程
整个系统的工作流可以分为四个核心阶段:
- 数据准备与场景映射:这是最基础的一步。在2D场景中,我们通过
NavMeshSurface2d组件来定义哪些游戏对象(通常是带有Collider2D的Tilemap或Sprite)是“可行走”的地面。这个组件的作用是收集这些2D碰撞体信息。 - 空间转换与网格烘焙:
NavMeshSurface2d内部会将收集到的2D多边形(从2D碰撞体获取)进行三角剖分,并“抬升”到一个固定的3D高度(例如Z=0)上,生成一个3D的网格数据。然后,它调用Unity底层的NavMesh烘焙接口,对这个3D网格进行烘焙,计算出可行走区域(Walkable Area)和障碍物。 - 代理移动与路径查询:我们使用
NavMeshAgent(注意,是Unity原生的3D组件)来控制角色移动。但这里有个关键技巧:我们会将这个Agent的baseOffset(基准偏移)设置为一个正值,让它“悬浮”在刚刚烘焙出的3D NavMesh表面之上。当我们在2D空间(XY平面)设置目标点时,系统会将此目标点也映射到3D NavMesh表面进行路径计算。 - 位置同步与渲染:计算出的路径是3D空间中的一系列点。
NavMeshAgent会沿着这条3D路径移动。我们需要在每帧更新时,只取其X和Y坐标同步到2D游戏对象(如Transform)的位置上,而忽略或固定其Z坐标。这样,从玩家的视角看,角色就是在2D平面上平滑移动了。
注意:这里最容易混淆的概念是
NavMeshAgent。在NavMeshPlus方案中,你必须使用UnityEngine.AI.NavMeshAgent,而不是去寻找一个2D版本。它的移动逻辑是3D的,但通过我们对其位置的控制,可以实现完美的2D移动效果。
2.2 关键组件角色解析
- NavMeshSurface2d:这是整个方案的入口和核心管理器。它挂载在包含2D地面(如Tilemap)的游戏对象上。其主要职责是:
- 定义哪些
Collider2D参与导航网格的生成。 - 管理NavMesh的烘焙过程(可以运行时动态烘焙)。
- 持有烘焙产生的NavMesh数据。
- 定义哪些
- NavMeshAgent (UnityEngine.AI):路径跟随和执行者。它挂载在需要寻路的角色(如敌人、NPC)上。你需要像在3D中一样配置它的速度、角速度、加速度、停止距离等参数。它的
destination属性就是你在2D空间中设置的目标点。 - NavMeshModifier 与 NavMeshModifierVolume:这两个是用于微调导航区域的工具。例如,你可以用
NavMeshModifier标记某个区域为“成本更高”(如沼泽地),让AI优先选择其他路径;或者用NavMeshModifierVolume在运行时动态定义一个立方体区域为不可行走(如临时放置的障碍物)。
这套架构的优势在于,它复用了Unity经过多年打磨的NavMesh寻路算法(基于Recast Navigation),该算法在路径平滑、拐角处理、动态障碍物回避等方面都非常成熟。我们只是在其上做了一层“翻译”,让2D世界能与之对话。
3. 环境搭建与项目配置实操
理论清晰后,我们进入实战环节。首先是把NavMeshPlus集成到你的项目中。
3.1 安装NavMeshPlus
最推荐的方式是通过Unity的Package Manager进行安装,这便于版本管理和更新。
- 打开Unity,进入
Window -> Package Manager。 - 点击左上角的“+”号,选择
Add package from git URL...。 - 在弹出的输入框中,填入NavMeshPlus的Git仓库地址:
https://github.com/h8man/NavMeshPlus.git。你也可以使用更稳定的版本URL,如https://github.com/h8man/NavMeshPlus.git#upm,后者是专门为UPM准备的分支。 - 点击“Add”。Unity会开始下载并导入包。完成后,在Package Manager的“My Registries”或“In Project”列表中应该能看到
NavMesh Components。
实操心得:如果从Git URL安装失败(可能由于网络问题),可以退而求其次,直接从GitHub仓库下载
.zip文件,解压后将其中的Assets/NavMeshComponents文件夹拷贝到你项目的Assets目录下。但UPM方式是首选,它能更好地处理依赖。
3.2 基础场景设置
假设我们有一个简单的2D俯视角场景,地面由Unity的Tilemap绘制。
- 创建导航地面:选中你的Tilemap游戏对象(或任何包含
Collider2D并代表地面的对象)。 - 添加NavMeshSurface2d组件:在Inspector窗口中,点击
Add Component,搜索并添加NavMeshSurface2d。 - 配置Surface:
Agent Type:选择一个预定义的Agent类型,它定义了通行能力(如高度、坡度、半径)。对于纯2D,通常使用默认的“Humanoid”即可,但最好根据你的角色大小在Navigation窗口(Window -> AI -> Navigation)的Agents页签下新建一个更适合的2D Agent类型,比如设置半径为0.5,高度为1。Collect Objects:选择Volume。这是关键设置,意味着组件会收集其自身BoxCollider2D(或CompositeCollider2D)所定义区域内的所有Collider2D。Use Geometry:选择Physics Colliders。这告诉系统使用物理碰撞体来生成导航网格,而不是渲染网格。Default Area:设置为Walkable。
- 烘焙导航网格:在
NavMeshSurface2d组件上,点击Bake按钮。稍等片刻,你会在Scene视图中看到一层蓝色的半透明网格覆盖在可行走区域上。这就是生成的2D导航网格在3D空间中的可视化。
3.3 创建可寻路的角色(Agent)
- 创建一个2D精灵(Sprite)作为你的角色,为其添加
Rigidbody2D(根据游戏类型选择Dynamic或Kinematic)和Collider2D。 - 关键步骤:为这个角色添加
UnityEngine.AI.NavMeshAgent组件。是的,直接从Component菜单的Navigation下添加这个3D组件。 - 配置Agent参数:
Speed:最大移动速度。Angular Speed:旋转速度。在纯2D锁定视角游戏中,如果角色不需要旋转模型,这个可以设很高或保持默认。Acceleration:加速度。Stopping Distance:在距离目标点多远时停止。Auto Braking:接近目标时是否自动减速。Radius:代理的半径,用于路径宽度计算。应略大于角色碰撞体半径。Height:代理的高度。在2D中这个值不太重要,但必须大于0(例如设为1)。Base Offset:这是实现2D化的核心参数之一。将其设置为一个正数,例如0.5或1。这相当于让代理“飘”在导航网格的上方。确保这个值大于0,否则代理可能会与导航网格表面相交,导致寻路失败。
- 编写移动脚本:创建一个脚本(如
PlayerController或AIController)挂载到角色上。脚本的核心就是设置NavMeshAgent的destination。
using UnityEngine; using UnityEngine.AI; // 注意引用AI命名空间 public class SimpleAIMover : MonoBehaviour { private NavMeshAgent agent; void Start() { agent = GetComponent<NavMeshAgent>(); // 确保代理在正确的层上,并且不会在物理上掉下去 agent.updatePosition = false; // 重要:我们自己控制2D位置更新 agent.updateRotation = false; // 在2D中通常不需要代理控制旋转 } void Update() { // 示例:点击鼠标设置目标 if (Input.GetMouseButtonDown(0)) { // 将鼠标点击的屏幕坐标转换为世界坐标 Vector3 mouseWorldPos = Camera.main.ScreenToWorldPoint(Input.mousePosition); mouseWorldPos.z = 0; // 确保Z坐标为0,我们的2D平面 // 设置代理的目标点 agent.SetDestination(mouseWorldPos); } // 关键:将NavMeshAgent计算出的3D位置,同步到2D Transform的XY位置 if (agent.remainingDistance > agent.stoppingDistance) { Vector3 agent3DPos = agent.nextPosition; transform.position = new Vector3(agent3DPos.x, agent3DPos.y, transform.position.z); } } }注意事项:代码中
agent.updatePosition = false;和手动同步transform.position是保证2D移动精确控制的关键。如果设为true,NavMeshAgent会直接修改Transform.position的XYZ,可能导致Z轴变化。手动同步让我们只取XY。
4. 核心功能实现与高级用法
基础寻路跑通后,我们会遇到更复杂的需求。NavMeshPlus结合Unity NavMesh的原生功能,可以很好地处理这些情况。
4.1 处理动态障碍物
游戏中的障碍物常常不是静态的,比如可移动的箱子、开关的门。Unity NavMesh原生支持NavMeshObstacle组件。
- 为你的动态障碍物(一个带有
Collider2D的游戏对象)添加NavMeshObstacle组件(同样是3D组件)。 - 配置参数:
Shape:选择Box或Capsule,尽量匹配你2D碰撞体的形状。Center/Size:调整其3D形状,使其在XY平面上覆盖你的2D碰撞体,并有一个小的高度(如Z方向高度为1)。Carve:勾选此选项。这是关键,它使得障碍物能在导航网格上“挖”出一个洞。Move Threshold:当障碍物移动距离超过此值时,才重新计算被挖掉的区域。适当设置可以优化性能。Time To Stationary:障碍物停止移动后,等待多久才将其视为静态并重新烘焙其周围网格。
当带有NavMeshObstacle的物体移动时,它会实时地影响导航网格,其他NavMeshAgent会自动绕开它。这是实现动态避障最省心的方式。
4.2 区域成本与区域掩码
游戏地图通常有不同的地形,比如平地、草地、沼泽、道路,移动速度或代价不同。
- 定义导航区域:在
Navigation窗口的Areas页签,你可以创建自定义区域,如Grass、Road、Mud,并为每个区域设置一个Cost(成本)。成本越高,Agent在寻路时越会优先避免走该区域。 - 标记游戏对象区域:有两种方式:
- NavMeshModifier:挂载到游戏对象上。将该对象的
Area Type设置为自定义区域(如Mud)。在烘焙时,该对象所在的导航面就会被标记为该区域。 - 图层(Layer):在
NavMeshSurface2d组件的Object Collection设置中,可以指定从特定图层收集物体。你可以提前将“沼泽”Tilemap放在名为Mud的图层上,然后在Surface中只收集这个图层,并设置其Default Area为Mud。
- NavMeshModifier:挂载到游戏对象上。将该对象的
- Agent区域通行设置:在
NavMeshAgent组件上,有一个Area Mask属性。这是一个位掩码,用于指定该Agent可以行走在哪些区域。例如,一个高级士兵可以走所有地形,而一个平民可能只能走Walkable和Road区域。通过代码可以动态修改:agent.areaMask = NavMesh.AllAreas;或agent.areaMask = 1 << NavMesh.GetAreaFromName(“Road”);。
4.3 运行时动态烘焙与更新
对于完全动态生成的地图,或者需要大量修改地形的游戏(如沙盒游戏),需要运行时烘焙。
// 获取场景中的NavMeshSurface2d组件 NavMeshSurface2d surface = FindObjectOfType<NavMeshSurface2d>(); // 方式1:完全重新烘焙(耗时,可能卡顿) surface.BuildNavMesh(); // 方式2:异步烘焙(推荐,避免帧率卡顿) StartCoroutine(BakeNavMeshAsync(surface)); IEnumerator BakeNavMeshAsync(NavMeshSurface2d surface) { AsyncOperation operation = surface.UpdateNavMesh(surface.navMeshData); while (!operation.isDone) { yield return null; } // 烘焙完成 Debug.Log("NavMesh baked async."); } // 方式3:局部更新(NavMeshPlus扩展功能) // 当只有小部分地形变化时,可以只更新受影响的Tile // 这需要你管理一个脏矩形(Dirty Rect)列表,然后调用 // surface.UpdateNavMesh(tilesBounds); // tilesBounds是一个Bounds结构体实操心得:动态烘焙是性能敏感操作。务必在编辑器下进行性能测试。对于Tilemap游戏,如果只是局部格子变化,强烈建议研究并使用NavMeshPlus提供的局部更新API (
NavMeshBuilder.UpdateNavMeshDataAsync配合脏区域管理),这比全量烘焙高效得多。通常的策略是,在玩家修改地形后的几帧内,延迟执行局部更新。
4.4 多Surface管理与连接
大型地图可能需要分块烘焙导航网格,或者不同区域有完全独立的导航网格(如室内、室外)。
- 创建多个
NavMeshSurface2d组件,分别管理不同区域的地形。 - 分别烘焙每个Surface。
- 连接(Links):如果两个区域之间有通道(如门、楼梯),你需要使用
NavMeshLink组件来连接它们。NavMeshLink定义了一个起点和终点,Agent在寻路时会将其视为一条可通行的“通道”,即使两点间没有连续的导航网格。- 在门口放置一个空物体,添加
NavMeshLink。 - 设置
Start Point和End Point(局部坐标),分别指向门的两侧。 - 调整
Width和Cost Override(链接成本)。 - 在代码中,可以通过
NavMesh.CalculatePath来验证路径是否通过了Link。
- 在门口放置一个空物体,添加
5. 性能优化与调试技巧
当场景中有数十上百个Agent同时寻路时,性能问题就会凸显。以下是一些关键的优化点和调试方法。
5.1 性能优化策略
| 优化方向 | 具体措施 | 说明 |
|---|---|---|
| 代理(Agent)数量 | 1.分帧更新:不要所有Agent都在同一帧调用SetDestination或查询路径。将Agent分组,每帧只更新一部分。2.休眠机制:对于到达目标或长时间静止的Agent,设置 agent.isStopped = true,甚至禁用其NavMeshAgent组件。当需要时再激活。 | 寻路计算是CPU密集型操作。减少同时活动的Agent数量是首要优化手段。 |
| 导航网格(NavMesh) | 1.简化碰撞体:用于生成NavMesh的2D碰撞体应尽量使用简单的形状(如BoxCollider2D)。避免使用复杂多边形或过多的小碰撞体。 2.合理设置烘焙参数:在 NavMeshSurface2d的Advanced设置中,调整Cell Size(体素大小)。值越大,烘焙速度越快,网格越粗糙,性能越好,但精度越低。需要在精度和性能间权衡。3.使用代理半径(Agent Radius):设置合理的Agent半径。半径越大,烘焙出的可行走区域越“窄”,但也能避免Agent挤在一起。 | NavMesh的数据量和复杂度直接影响寻路查询的速度和内存占用。 |
| 寻路查询 | 1.降低查询频率:AI不需要每帧都重新寻路。可以每N帧(如0.5-1秒一次)检查一次是否需要更新路径。 2.使用协程(Coroutine):将耗时的路径计算(如 NavMesh.CalculatePath)放在协程中,避免阻塞主线程。3.路径缓存:对于固定点之间的路径(如巡逻点),可以预先计算并缓存起来,重复使用。 | 减少不必要的寻路计算调用。 |
| 动态障碍物 | 合理设置NavMeshObstacle的Carve属性和Move Threshold。对于频繁移动但形状不变的障碍物,Move Threshold可以设大一些,减少重新“雕刻”导航网格的次数。 | 动态障碍物的实时雕刻开销较大。 |
5.2 调试与可视化
清晰的调试信息能极大提升开发效率。
- Scene视图可视化:
- 在Scene视图的
Gizmos下拉菜单中,确保Navigation相关选项已开启。 - 选中
NavMeshAgent,你可以在Scene视图中看到其当前路径(一条绿线)、下一个路点(黄色菱形)和停止距离(红色圆圈)。 - 选中
NavMeshSurface2d,可以看到烘焙出的导航网格(蓝色区域)。 - 选中
NavMeshObstacle,可以看到其雕刻出的区域(红色区域)。
- 在Scene视图的
- 绘制调试图形:在代码中使用
Debug.DrawLine和Debug.DrawRay来绘制自定义的调试信息,比如Agent的视线、感知范围、当前路径等。 - 检查NavMesh数据:可以通过
NavMesh.CalculatePath获取路径点数组,然后遍历并打印或绘制出来,验证路径是否正确。 - 使用NavMeshQuery:对于高级需求,如采样随机点、射线检测等,可以使用
NavMeshQuery类进行更底层的查询和调试。
// 示例:在OnDrawGizmos中绘制Agent的路径 void OnDrawGizmos() { if (agent != null && agent.hasPath) { var path = agent.path; if (path.corners.Length < 2) return; Gizmos.color = Color.green; for (int i = 0; i < path.corners.Length - 1; i++) { Gizmos.DrawLine(path.corners[i], path.corners[i + 1]); Gizmos.DrawSphere(path.corners[i], 0.1f); } Gizmos.DrawSphere(path.corners[path.corners.Length - 1], 0.1f); } }6. 常见问题与解决方案实录
在实际项目中踩坑是免不了的。这里记录了几个最典型的问题和我的解决思路。
6.1 Agent不移动或原地抖动
- 问题现象:
NavMeshAgent的destination已设置,pathStatus显示为PathComplete,但角色不动,或者在小范围内快速抖动。 - 排查步骤:
- 检查导航网格:确保角色起始位置在蓝色的导航网格区域内。如果不在,Agent无法找到路径。可以用
NavMesh.SamplePosition来检测和纠正位置。 - 检查Base Offset:确认
NavMeshAgent的Base Offset设置为一个大于0的值(如0.5)。如果为0或负数,Agent可能嵌在导航网格里。 - 检查Update Position/Rotation:如之前所述,在2D项目中,通常需要设置
agent.updatePosition = false;并在Update中手动同步位置。如果设为true,且角色的初始Z坐标与导航网格的Z坐标不匹配,会导致计算混乱。 - 检查是否有碰撞体阻挡:
NavMeshAgent只负责寻路,不处理物理碰撞。如果角色有Rigidbody2D和Collider2D,并且与其他静态碰撞体发生了物理阻挡,它依然会被卡住。你需要确保导航网格覆盖的区域,在物理上也是可通行的,或者通过代码/物理层设置忽略某些碰撞。
- 检查导航网格:确保角色起始位置在蓝色的导航网格区域内。如果不在,Agent无法找到路径。可以用
- 解决方案:在
Start或Awake中,使用NavMesh.SamplePosition将Agent“吸附”到最近的导航网格点上。
void Start() { agent = GetComponent<NavMeshAgent>(); agent.updatePosition = false; agent.updateRotation = false; // 确保Agent初始位置在NavMesh上 NavMeshHit hit; if (NavMesh.SamplePosition(transform.position, out hit, 1.0f, NavMesh.AllAreas)) { agent.Warp(hit.position); // Warp是瞬间移动Agent到指定位置 // 同时同步2D位置 transform.position = new Vector3(hit.position.x, hit.position.y, transform.position.z); } else { Debug.LogError("Agent starting position is not on NavMesh!"); } }6.2 烘焙后导航网格缺失或不正确
- 问题现象:点击Bake后,Scene视图没有蓝色网格,或者网格形状与预期不符(有洞、形状奇怪)。
- 排查步骤:
- 检查Collider2D:确保用于生成导航网格的Tilemap或Sprite确实有
Collider2D组件(TilemapCollider2D或BoxCollider2D等),并且已启用。 - 检查NavMeshSurface2d设置:
Collect Objects是否设置为Volume?并且该游戏对象自身是否有Collider2D来定义收集范围?Use Geometry是否设置为Physics Colliders?Agent Type的半径和高度是否设置得过于极端,导致没有有效区域?尝试使用默认的“Humanoid”先测试。
- 检查图层(Layer)过滤:如果
NavMeshSurface2d设置了特定的Layer Mask,请确保你的地面碰撞体在那些图层上。 - 检查场景缩放:如果场景或父物体的缩放(Scale)不是(1,1,1),可能会导致碰撞体数据转换出错。尽量保持用于导航的地面根节点缩放为1。
- 检查Collider2D:确保用于生成导航网格的Tilemap或Sprite确实有
- 解决方案:从一个最简单的场景开始测试:一个只有
BoxCollider2D的平面,加上NavMeshSurface2d,确保能烘焙出网格。然后逐步添加复杂元素,定位问题。
6.3 动态障碍物(NavMeshObstacle)不生效
- 问题现象:移动的障碍物没有让Agent绕行,Agent直接穿过去或撞上去。
- 排查步骤:
- 检查Carve选项:确保
NavMeshObstacle组件的Carve复选框被勾选。 - 检查形状和位置:在Scene视图中检查
NavMeshObstacle的Gizmo(一个红色线框),确保它在XY平面上完全覆盖了障碍物的2D碰撞体,并且有足够的高度(Z方向)与导航网格相交。 - 检查NavMeshSurface的烘焙类型:
NavMeshSurface2d有一个Collect Objects模式叫All,它会收集场景中所有物体。如果你的障碍物也被收集进去并烘焙成了静态可行走区域,那么动态的NavMeshObstacle可能无法覆盖它。确保用于烘焙的Surface只收集静态地面。 - 等待雕刻生效:
NavMeshObstacle的雕刻不是瞬间完成的,有一个很小的延迟。对于快速移动的物体,效果可能不理想。
- 检查Carve选项:确保
- 解决方案:对于需要精确实时避障的快速移动障碍物(如其他玩家),
NavMeshObstacle可能不是最佳选择。可以考虑使用更传统的“潜在场”(Potential Fields)或“流场”(Flow Field)算法,或者将障碍物视为一个高成本的区域,并让Agent定期重新规划路径。
6.4 跨平台构建问题
- 问题现象:在编辑器里运行正常,但打包(如WebGL、Android)后寻路失效。
- 排查步骤:
- 检查NavMesh数据是否被打包:导航网格数据是一种资源。确保包含
NavMeshSurface2d组件的场景被正确添加到构建设置(Build Settings)中。 - 检查脚本执行顺序:在打包版本中,Awake/Start的调用顺序可能与编辑器不同。确保在Agent开始寻路前,
NavMeshSurface2d的烘焙已经完成(如果是运行时烘焙)。可以考虑在场景加载完成后,用一个协程等待几帧再初始化AI。 - 检查Unity版本与NavMeshPlus兼容性:不同版本的Unity其NavMesh API可能有细微差别。确认你使用的NavMeshPlus版本与你的Unity版本兼容。通常GitHub仓库的Release页面或README会有说明。
- 查看Player Log:打包后运行时出现任何错误,查看Player Log是首要任务。可能会暴露出数据加载失败或API不兼容的错误信息。
- 检查NavMesh数据是否被打包:导航网格数据是一种资源。确保包含
- 解决方案:始终在目标平台进行测试。对于WebGL这类平台,由于其单线程特性,复杂的动态烘焙操作可能导致主线程卡死,应尽量避免或使用非常简化的网格。
7. 进阶应用与扩展思路
当基础功能满足后,可以探索一些更高级的用法,让AI行为更加智能和自然。
7.1 群体移动与局部避障(Local Avoidance)
Unity原生的NavMeshAgent自带基础的局部避障功能(通过RVO),但在2D中默认可能不工作或效果不佳。对于大量单位(如人群模拟),需要更专业的方案。
- 使用第三方RVO库:例如
RVO2库的Unity移植版。这些库专门处理密集群体的局部碰撞避免,计算效率高,效果自然。 - 混合方法:用
NavMeshAgent处理宏观的、长距离的路径规划(房间到房间),用简单的物理力或向量场处理微观的、短距离的避障(避免与其他Agent相撞)。例如,每个Agent除了跟随路径,还施加一个远离附近其他Agent的排斥力。
7.2 与行为树(Behavior Tree)或状态机集成
NavMeshAgent只负责移动,AI的决策(如“追击玩家”、“逃跑”、“巡逻”)需要更上层的逻辑来控制。
- 状态机模式:在
Update中根据当前状态(State)调用不同的方法。例如,在ChaseState中,设置destination为玩家位置;在PatrolState中,在几个预设点之间循环移动。 - 行为树:使用如
NodeCanvas、Behavior Designer等插件,或者自己实现一个轻量级行为树。将“移动到某点”(使用NavMeshAgent)作为一个叶子节点(Action Node),由行为树根据条件选择执行。这样可以将复杂的AI逻辑清晰地模块化。
7.3 自定义路径后处理
有时NavMeshAgent计算出的路径可能不够平滑,或者你想让角色沿着一条更特定的曲线移动(如沿道路中心线)。
- 路径平滑:获取
agent.path.corners(路径拐点数组),然后使用样条插值(如Catmull-Rom)在这些点之间生成更平滑的曲线,让Agent沿着这条曲线移动。 - 路径修饰:在路径点之间插入额外的逻辑。例如,在经过某个点时触发一个动画(如跳跃),或者强制让角色在某个区域减速。
// 简单的路径平滑示例(需在协程中执行) IEnumerator SmoothPathFollow(Vector3[] waypoints) { for (int i = 0; i < waypoints.Length - 1; i++) { Vector3 start = waypoints[i]; Vector3 end = waypoints[i + 1]; float duration = Vector3.Distance(start, end) / agent.speed; float elapsed = 0; while (elapsed < duration) { elapsed += Time.deltaTime; float t = elapsed / duration; // 使用简单的线性插值,可以替换为更复杂的曲线函数 Vector3 smoothedPosition = Vector3.Lerp(start, end, t); agent.nextPosition = smoothedPosition; // 直接设置nextPosition来“引导”Agent transform.position = new Vector3(smoothedPosition.x, smoothedPosition.y, transform.position.z); yield return null; } } }这套基于NavMeshPlus的2D寻路方案,经过多个项目的实战检验,在性能、稳定性和开发效率上取得了很好的平衡。它可能不是所有2D寻路问题的最优解(例如对于极大量、超密集单位的实时战略游戏,专门的流场算法可能更优),但对于绝大多数需要可靠、智能且易于集成的2D导航需求的游戏项目来说,它无疑是一个强大而优雅的解决方案。关键在于理解其“2D转3D再转2D”的核心思想,并熟练掌握其组件的工作流程和配置细节,这样就能游刃有余地应对开发中遇到的各种挑战。