1. 项目概述:为什么Unity 2D游戏需要智能寻路?
在开发2D游戏,尤其是俯视角、RPG、塔防或者策略类游戏时,一个最核心也最让人头疼的问题就是:如何让游戏里的角色自己找到路?你肯定不想让玩家去手动点击屏幕上的每一个格子来指挥角色移动,那太笨拙了。更不想看到你的NPC或者敌人像个没头苍蝇一样撞墙,或者卡在某个角落出不来。
Unity自带的NavMesh系统非常强大,但它是为3D世界设计的。在3D里,寻路是在一个三维的“地面”网格上计算的。但到了2D世界,事情就变了。我们的游戏世界是一个平面,角色在X和Y轴上移动,Z轴可能只用来处理渲染层级。直接把3D的NavMesh用在2D场景上,会遇到坐标轴不对、碰撞体不匹配、烘焙(Bake)出来的导航区域错位等一系列问题。
这时候,NavMeshPlus就登场了。它不是一个全新的寻路引擎,而是一个基于Unity官方NavMeshComponents的扩展(Extension)系统。简单说,它把3D的导航网格“拍扁”并适配到了2D坐标系里,同时提供了专门用于收集2D场景元素(如Tilemap瓦片地图、Sprite精灵、Collider2D碰撞体)作为障碍物的工具。这样一来,我们就能在2D游戏里,享受到和3D游戏一样成熟、高效的A*寻路算法带来的便利——自动绕过障碍、寻找最短路径、动态更新导航区域。
我最近在一个2D俯视角的生存建造游戏里深度使用了NavMeshPlus,让村民可以自动去砍树、采矿、搬运资源回仓库,整个过程完全无需玩家微操。踩过不少坑,也总结出了一套从原理到落地的完整流程。这篇文章,我就来详细拆解如何用NavMeshPlus实现2D智能寻路,让你也能轻松搞定游戏里的“自动驾驶”。
2. 核心原理与架构拆解:NavMeshPlus是如何工作的?
在动手写代码和摆弄组件之前,理解NavMeshPlus底层是怎么转起来的,能帮你避开后面90%的坑。它并不是魔改Unity的物理引擎或者重写寻路算法,而是巧妙地“桥接”和“转换”。
2.1 Unity NavMesh系统基础回顾
首先,我们要明白Unity原生的NavMesh系统工作流:
- 烘焙(Bake):在编辑模式下,系统会扫描场景中所有标记为“Navigation Static”的物体及其Mesh Renderer,根据它们的形状和位置,在“可行走”的表面(通常是Y轴朝上的平面)生成一个由三角形构成的导航网格(NavMesh)。这个网格定义了AI可以行走的区域。
- 导航代理(NavMeshAgent):这是一个挂在游戏对象(如你的角色)上的组件。它包含了速度、加速度、转向、半径等参数。你给Agent一个目标位置,它就会利用A*算法在导航网格上计算出一条路径,并控制角色移动。
- 障碍物(NavMeshObstacle):动态障碍物,Agent在寻路时会实时避开它们。
这个流程的核心是“3D网格”。而在2D中,我们的“地面”是XY平面,障碍物是Sprite或Tilemap,它们没有3D Mesh,只有2D Collider。
2.2 NavMeshPlus的“二维化”魔法
NavMeshPlus的核心思路是:在3D NavMesh系统的框架内,模拟一个2D的“世界”。
坐标转换与表面旋转:这是最关键的一步。NavMeshSurface组件(来自NavMeshComponents)默认认为可行走面是XZ平面(Y轴向上)。在2D游戏中,我们的相机通常是正交投影(Orthographic),看向XY平面(Z轴指向屏幕内)。NavMeshPlus通过一个“Rotate Surface to XY”的操作,本质上是将NavMeshSurface组件所在的GameObject旋转-90度绕X轴。这样,原本的XZ平面就变成了XY平面,Z轴变成了“高度”(在纯2D中我们通常忽略它)。后续所有的烘焙和寻路计算,都基于这个旋转后的新坐标系。
2D数据收集器(NavMeshCollectSources2d):这是NavMeshPlus的明星组件。Unity原生的烘焙器只认识3D Mesh。NavMeshCollectSources2d组件的工作就是遍历场景,找到所有的Tilemap、带有SpriteRenderer和Collider2D的物体,将这些2D几何信息“转换”成NavMesh系统能理解的3D Mesh数据(Sources)。
- 对于Tilemap:它会读取每个有碰撞体的瓦片(Tile),将其矩形轮廓转换为3D多边形。
- 对于Sprite/Collider2D:它会根据Collider2D的形状(Box, Circle, Polygon等),生成对应的3D碰撞体形状。 这些被转换后的“源数据”,会被喂给Unity的NavMeshBuilder去生成真正的导航网格。
世界边界与行走区域:在3D中,导航网格烘焙在场景几何体上。在2D中,我们需要先定义一个“可行走的矩形区域”。NavMeshPlus会根据你场景中2D碰撞体的边界,自动计算出一个包围盒(World Bounds),或者你也可以手动指定。烘焙时,会先在这个矩形区域内生成一个完整的“行走面”网格,然后再根据收集到的2D障碍物源,在这个网格上“挖洞”,形成不可行走区域。
注意:这里有一个非常重要的概念区分。NavMeshPlus生成的不是一个“点阵”或“网格图”,它生成的依然是三角形网格。只是这个网格是铺在XY平面上的。Agent的寻路是在这个三角形网格上进行的,因此路径是平滑的曲线,而不是生硬的网格拐点。这对于追求自然移动感的游戏至关重要。
2.3 核心组件一览
了解这几个组件,你就掌握了NavMeshPlus的命脉:
- NavMeshSurface (来自Unity):导航网格表面的容器和烘焙执行者。挂载它并旋转后,它定义了2D导航网格所在的平面。
- NavMeshCollectSources2d (NavMeshPlus):2D源收集器。必须和NavMeshSurface挂在同一GameObject上。负责搜集Tilemap、Sprite等并转换为导航数据。
- NavMeshModifier (来自Unity):区域修改器。挂在具体的障碍物物体上,可以覆盖该障碍物所在区域的导航网格类型(例如,标记为“不可行走”、“沼泽(减速)”、“水域(不可通行)”等)。
- NavMeshAgent (来自Unity):导航代理。挂在需要自动寻路的角色上,是寻路的执行者。
- NavMeshLink (来自Unity):导航链接。用于连接两个被断开(如沟壑、楼梯)的导航网格区域,实现“跳跃”或“传送”效果。在2D中可用于实现上下楼梯、跳平台等。
3. 环境准备与项目集成:三种安装方式详解
理论懂了,接下来就是动手。把NavMeshPlus弄进你的Unity项目,有三种主流方法,各有利弊。
3.1 方式一:使用Package Manager(Git URL)—— 推荐
这是最干净、最便于管理的方式,尤其适合使用Git进行版本控制的项目。
- 在Unity编辑器中,打开Window > Package Manager。
- 点击左上角的“+”按钮,选择“Add package from git URL...”。
- 在弹出的输入框中,粘贴NavMeshPlus的Git仓库地址:
https://github.com/h8man/NavMeshPlus.git - 点击Add。Unity会自动从GitHub克隆仓库并作为包导入。
优点:集成在Package Manager中,与项目Assets分离,干净整洁。更新相对方便(可指定分支或标签)。缺点:需要网络,且依赖Git。如果公司内网限制Git,可能无法使用。
3.2 方式二:修改manifest.json文件—— 灵活控制版本
如果你需要锁定某个特定版本,或者项目结构有特殊要求,可以直接修改包清单文件。
- 用文本编辑器打开你项目根目录下的
Packages/manifest.json文件。 - 在
"dependencies": { ... }部分,添加一行:
如果你想使用某个稳定版本(查看GitHub的Release或Tags),可以将"com.h8man.2d.navmeshplus": "https://github.com/h8man/NavMeshPlus.git#master"#master替换为#v1.0.0这样的标签名。 - 保存文件,回到Unity,它会自动开始导入包。
优点:版本控制精确,配置一次即可。适合团队协作,确保所有人环境一致。缺点:对不熟悉json和包管理的开发者稍有门槛。
3.3 方式三:直接下载源码到Assets—— 传统直接
如果你不想碰Package Manager,或者项目非常老,这是最直接的方法。
- 访问 NavMeshPlus的GitHub页面 。
- 点击Code > Download ZIP,将源码下载到本地并解压。
- 在你的Unity项目Assets文件夹下,创建一个合适的子文件夹,例如
Plugins/NavMeshPlus。 - 将解压后文件夹内的
Assets/NavMeshComponents和Assets/Gizmos等内容,复制到你刚刚创建的Plugins/NavMeshPlus文件夹内。
优点:无需网络和Git,最直观。可以直接查看和修改源码(但需注意许可证)。缺点:污染Assets目录,更新麻烦,需要手动替换文件。不利于包管理。
实操心得:对于新项目,我强烈推荐方式一(Package Manager)。它代表了Unity现代的资源管理方式。如果方式一失败,再退而求其次用方式二。方式三仅作为备用方案。集成成功后,你可以在Package Manager的“My Registries”或“In Project”列表里看到
com.h8man.2d.navmeshplus。
4. 场景配置与导航网格烘焙:一步步构建可行走世界
安装好插件,我们开始实战。假设我们有一个简单的2D俯视角场景,地面由Tilemap铺成,上面散落着一些树木(Sprite)和石头(Sprite)作为障碍物。
4.1 基础场景设置
- 创建地面:使用Unity的Tilemap系统创建一个地面层。确保你的Tile Palette中的瓦片都设置了合适的Collider 2D(通常是
Tilemap Collider 2D组件,并勾选Used By Composite,然后添加Composite Collider 2D组件以优化碰撞体)。地面瓦片本身不应该被标记为障碍物,它只是背景。 - 放置障碍物:创建一些精灵(Sprite)作为树木和石头,为它们添加
Box Collider 2D或Polygon Collider 2D。这些将是寻路时需要绕开的物体。
4.2 配置NavMeshSurface与2D收集器
这是核心步骤。
- 在场景层级(Hierarchy)的根目录下,创建一个空的GameObject,命名为
Navigation。 - 选中这个
Navigation对象,在检查器(Inspector)中点击Add Component。 - 搜索并添加NavMesh Surface组件。这个组件来自Unity的AI Navigation包,如果你还没导入,Unity会提示你导入,确认即可。
- 继续在
Navigation对象上,点击Add Component,搜索并添加NavMesh Collect Sources 2d组件。这是NavMeshPlus提供的。 - 现在,点击NavMesh Surface组件上的Rotate Surface to XY按钮。你会看到该GameObject的Rotation值变为
(-90, 0, 0)。这一步至关重要,它把导航表面从XZ平面旋转到了XY平面。
4.3 设置障碍物区域类型
我们不想让角色穿过树木和石头,所以需要将它们标记为“不可行走”。
- 选中场景中的一棵“树”(Sprite物体)。
- 点击Add Component,搜索并添加NavMesh Modifier组件。
- 在该组件中:
- 勾选Override Area。
- 在Area Type下拉菜单中,选择Not Walkable。Unity内置了多种区域类型,
Not Walkable表示完全不可通行。 - 确保Apply To Children也勾选(如果你的树是一个有子物体的预制件)。
- 对所有的树木、石头重复此操作。你也可以为河流、沼泽等地形设置不同的区域类型(如自定义的“Slow”慢速区域)。
4.4 烘焙导航网格
一切就绪,开始生成导航网格。
- 选中之前创建的
Navigation游戏对象。 - 在其NavMesh Surface组件上,找到Agent Type。通常使用默认的
Humanoid即可,你也可以在Window > AI > Navigation的Agents标签页自定义代理尺寸。 - 点击Bake按钮。
- 烘焙完成后,切换到Scene视图,在顶部工具栏的
Gizmos下拉菜单中,确保Navigation是开启状态。你就能看到场景中覆盖了一层蓝色的网格——这就是生成的2D导航网格!蓝色的区域是可行走的,障碍物所在的地方则是“镂空”的。
烘焙参数详解:
- Agent Radius:代理的半径。路径会与此半径的障碍物保持距离。如果你的角色体积小,可以设小一点(如0.1)以获得更贴近障碍物的路径。
- Agent Height:在2D中通常不重要,但保留一个较小值(如0.5)即可。
- Max Slope和Step Height:在纯2D俯视角游戏中,这两个参数基本无关,因为不存在斜坡和台阶。可以忽略。
- Drop Height和Jump Distance:用于生成Off-Mesh Links(导航链接),在2D平台跳跃游戏中可能有用,俯视角游戏一般不用。
踩坑记录:第一次烘焙后如果看不到蓝色网格,请检查:1.
NavigationGameObject的Rotation是否为(-90,0,0)。2. Scene视图的Gizmos是否开启了Navigation显示。3. 是否在Window > AI > Navigation的Bake标签页中,将Generated World Size设置得足够大以覆盖你的场景。烘焙后,如果角色仍然穿墙,请检查障碍物的NavMesh Modifier是否应用正确,以及障碍物的2D碰撞体是否确实存在且形状正确。
5. 让角色动起来:NavMeshAgent的配置与脚本控制
导航网格有了,现在需要让角色在上面智能移动。这通过NavMeshAgent组件和简单的脚本控制来实现。
5.1 为角色添加NavMeshAgent
- 选中你的角色预制件或场景中的角色对象。
- 点击Add Component,搜索并添加NavMesh Agent。
- 配置Agent参数,这些参数直接影响移动手感和性能:
- Speed:最大移动速度。
- Angular Speed:转向速度。在2D中,角色通常是瞬间转向或使用插值旋转,这个值可以设高(如500)。
- Acceleration:加速度。值越大,提速越快。
- Stopping Distance:在距离目标多远处开始减速停止。设为0会尝试精确到达目标点,但可能导致在目标点附近抖动。
- Auto Braking:是否自动刹车。如果勾选,接近目标时会减速;不勾选,则会以恒定速度移动直到到达,然后瞬间停止。根据游戏手感需求调整。
- Radius, Height, Base Offset:这些是代理的物理尺寸,用于避障和空间查询。确保
Radius小于导航网格中通道的宽度。Height在2D中不重要,Base Offset通常为0。
5.2 编写寻路控制脚本
为角色创建一个C#脚本,例如PlayerController或AIController。
using UnityEngine; using UnityEngine.AI; // 引入AI命名空间 public class AIController : MonoBehaviour { private NavMeshAgent agent; private Camera mainCamera; void Start() { // 获取组件引用 agent = GetComponent<NavMeshAgent>(); mainCamera = Camera.main; // 关键!告诉Agent我们是在2D XY平面上工作 // 将代理的更新位置和旋转模式设置为2D兼容 agent.updatePosition = true; agent.updateRotation = false; // 关闭Agent的3D旋转控制,我们自己控制2D朝向 agent.updateUpAxis = false; // 在NavMeshPlus的特定配置下,可能需要关注此参数 } void Update() { // 示例1:鼠标点击移动(适用于玩家控制或RTS) if (Input.GetMouseButtonDown(0)) // 左键点击 { // 将鼠标屏幕坐标转换为世界坐标(2D) Vector2 mousePos = mainCamera.ScreenToWorldPoint(Input.mousePosition); // 设置Agent的目的地 agent.SetDestination(mousePos); } // 示例2:自动面向移动方向(2D) if (agent.velocity.sqrMagnitude > Mathf.Epsilon) // 如果有速度 { // 计算朝向角度 Vector3 direction = agent.velocity.normalized; float angle = Mathf.Atan2(direction.y, direction.x) * Mathf.Rad2Deg; // 应用旋转(假设角色朝右是0度) transform.rotation = Quaternion.AngleAxis(angle, Vector3.forward); } } // 一个公共方法,供其他脚本调用,让AI移动到指定位置 public void MoveToPosition(Vector2 targetPosition) { if (agent != null && agent.isActiveAndEnabled) { agent.SetDestination(targetPosition); } } // 检查是否到达目的地 public bool HasReachedDestination() { return !agent.pathPending && agent.remainingDistance <= agent.stoppingDistance && (!agent.hasPath || agent.velocity.sqrMagnitude == 0f); } }脚本关键点解析:
agent.updateRotation = false;:这是2D寻路的关键设置。NavMeshAgent默认会控制3D物体的Y轴旋转,这在2D中会导致不可预料的行为(物体可能倾斜)。我们禁用它,然后手动用代码控制2D平面(Z轴)的旋转。agent.SetDestination():核心方法,告诉Agent目标点。Agent会自动在后台计算路径并开始移动。agent.velocity:可以获取Agent当前的速度向量,用于计算面向方向。remainingDistance和pathPending:用于判断移动状态,非常有用。
5.3 处理动态障碍物
如果场景中有会移动的障碍物(比如其他NPC、可推动的箱子),你需要用到NavMeshObstacle组件。
- 为动态障碍物添加NavMesh Obstacle组件。
- 设置其形状(Carve)和大小。关键参数是Carve,当勾选时,它会在导航网格上“挖”出一个动态的洞,Agent会实时避开这个区域。
- 对于形状复杂的物体,可以勾选Carve Only Stationary,这样只有当障碍物停止移动时,它才会被“雕刻”进导航网格,避免因障碍物频繁移动导致导航网格反复重新计算(性能开销大)。
实操心得:对于大量动态障碍物,要谨慎使用
Carve功能,尤其是它们频繁移动时。一种优化策略是:对于移动的敌人或NPC,不将其作为NavMeshObstacle,而是通过代码让AI Agent之间互相避让(例如使用RVO或简单的排斥力)。NavMeshObstacle更适合相对静止或缓慢移动的环境物体。
6. 高级应用与性能优化:应对复杂场景
当你的游戏场景变大、角色变多时,基础的寻路可能会遇到性能瓶颈或逻辑问题。下面分享一些进阶技巧。
6.1 分层烘焙与区域代价
不同的地形应该有不同的移动代价。比如,在草地上行走正常,在沼泽里速度减半。
- 自定义区域:打开
Window > AI > Navigation,切换到Areas标签页。你可以看到默认的Walkable,Not Walkable等。点击+号,可以创建自定义区域,例如命名为Swamp,并设置其Cost为2(代价为2,意味着走过该区域1米,相当于在普通区域走2米)。 - 应用区域:在场景中,选中沼泽地对应的Tilemap或Sprite,为其添加
NavMesh Modifier组件,Override Area选择你刚创建的Swamp。 - 重新烘焙。现在,当Agent寻路时,它会自动计算代价,可能会选择绕开沼泽,即使那条路更长,但总“代价”更低。
- 在Agent上设置区域掩码:在NavMeshAgent组件上,有一个
Area Mask属性。它决定了Agent可以在哪些类型的区域上行走。如果你的Agent是船,你可以只勾选Water区域;如果是陆地单位,则不要勾选Water。这可以防止单位走到不该去的地方。
6.2 局部网格更新与异步烘焙
如果你的游戏地图是动态生成的,或者有可破坏的地形(如炸毁一堵墙),每次变化都全图重新烘焙(Rebake)是灾难性的性能开销。
- 使用NavMeshSurface的AddData和RemoveData:NavMeshSurface组件提供了
AddData和RemoveData方法,可以动态地添加或移除一部分导航网格数据。但这需要你手动管理这些数据块,逻辑较复杂。 - 局部烘焙(Partial Bake):NavMeshPlus的
NavMeshCollectSources2d组件支持指定收集源的范围。你可以创建多个小的NavMeshSurface,每个负责场景的一个区域。当某个区域发生变化时,只重新烘焙那个小Surface。这需要你设计好场景的分区逻辑。 - 异步烘焙:
NavMeshSurface.BuildNavMesh()是一个同步方法,在大型地图上会卡住主线程。你可以使用NavMeshBuilder.UpdateNavMeshDataAsync()这个静态方法进行异步更新。你需要先创建一份NavMeshData,然后异步更新它,最后赋值给NavMeshSurface.navMeshData。具体实现可以参考Unity官方文档关于异步烘焙的示例。这是处理大型动态地图的终极方案,但实现难度较高。
6.3 多Agent管理与避障
当屏幕上同时有几十上百个单位在寻路时,即使每个Agent的路径计算是高效的,它们也可能互相卡住。
- RVO(Reciprocal Velocity Obstacles):Unity的High-Level AI系统包含了RVO避障功能,但它在2D中的支持并不完美。NavMeshAgent本身有简单的彼此避让,但对于高密度单位效果有限。
- 自定义局部避障:一种实用的方法是,在
Update中,为每个Agent检测前方一定距离内(Physics2D.OverlapCircle)的其他Agent。如果检测到,则给当前Agent一个垂直于连线方向的轻微侧向力,或者临时微调其速度。这需要精细的调参来避免“抖动”。 - 分层寻路(Flow Field):对于大规模群体移动(如RTS中的军队),NavMesh可能不是最高效的。业界常用Flow Field(流场寻路)。但这超出了NavMeshPlus的范围,你需要自己实现或寻找其他插件。对于中小规模(<100个Agent)的2D游戏,优化好的NavMeshAgent完全够用。
6.4 常见性能问题排查表
| 问题现象 | 可能原因 | 解决方案 |
|---|---|---|
| 烘焙时间极长 | 场景中2D碰撞体过多、过于复杂;Tilemap使用了大量多边形碰撞体。 | 1. 简化碰撞体形状,用Box代替复杂Polygon。2. 为Tilemap使用Composite Collider 2D合并碰撞体。3. 在NavMeshCollectSources2d组件中,尝试调整Collect Mode,可能不需要收集所有类型的源。 |
| 游戏运行时卡顿 | 每帧有大量Agent在同时计算路径;动态障碍物频繁触发网格雕刻(Carve)。 | 1. 限制同时进行路径请求的Agent数量(使用队列或分帧处理)。2. 对非紧急的移动,降低寻路更新频率(如每0.5秒寻路一次)。3. 为动态NavMeshObstacle设置合理的Carve Only Stationary和Carving Move Threshold。 |
| Agent在狭窄处卡住 | Agent的Radius设置过大,超过了导航网格通道的实际宽度。 | 1. 减小Agent的Radius。2. 在烘焙时增加Agent Radius参数,生成更宽的通道。3. 检查烘焙结果,确保关键通道没有被错误地标记为不可行走。 |
| 路径看起来“绕远” | 区域代价(Area Cost)设置不合理;或者存在多个相似代价的路径,Agent选择了第一条计算的。 | 1. 检查并调整不同地形区域的Cost值。2. A*算法本身会找到最短“代价”路径,而非绝对最短几何路径,这是正常的。 |
7. 实战案例:构建一个2D塔防游戏的敌人寻路系统
让我们用一个具体的例子来串联所有知识。假设我们要做一个经典的2D塔防游戏,敌人从地图入口沿着固定路径走向终点。
7.1 场景搭建与静态烘焙
- 设计地图:使用Tilemap绘制一条蜿蜒的路径,路径两侧是障碍物(如岩石、树木)。路径本身的地面瓦片使用一种Tile,障碍物使用另一种Tile并为其添加2D碰撞体。
- 设置导航:
- 创建
Navigation空物体,添加NavMeshSurface和NavMeshCollectSources2d。 - 点击
Rotate Surface to XY。 - 为所有作为障碍物的Tilemap瓦片(注意是瓦片,不是整个Tilemap)添加
NavMesh Modifier,区域类型设为Not Walkable。这里有个技巧:你可以创建一个只包含障碍物瓦片的独立Tilemap图层,然后给这个Tilemap物体整体添加一个NavMesh Modifier,并勾选Override Area为Not Walkable,同时勾选Apply To Children。这样这个图层上的所有瓦片都会自动成为障碍物。 - 烘焙导航网格。你应该看到一条蓝色的、与你的路径形状一致的导航网格生成。
- 创建
7.2 敌人AI脚本
敌人预制件需要NavMeshAgent组件和一个控制脚本。
using UnityEngine; using UnityEngine.AI; public class TDEnemy : MonoBehaviour { public Transform targetWaypoint; // 当前目标路点(由路径管理器分配) private NavMeshAgent agent; private float originalSpeed; public int health = 100; void Start() { agent = GetComponent<NavMeshAgent>(); originalSpeed = agent.speed; agent.updateRotation = false; // 关键:禁用3D旋转 if (targetWaypoint != null) { MoveToWaypoint(targetWaypoint.position); } } void Update() { // 控制2D朝向,面向移动方向 if (agent.velocity != Vector3.zero) { Vector3 dir = agent.velocity.normalized; float angle = Mathf.Atan2(dir.y, dir.x) * Mathf.Rad2Deg; transform.rotation = Quaternion.Euler(0, 0, angle); } // 检查是否到达当前路点 if (targetWaypoint != null && HasReachedDestination()) { // 通知路径管理器,请求下一个路点 PathManager.Instance.EnemyReachedWaypoint(this); } } public void MoveToWaypoint(Vector2 position) { agent.SetDestination(position); } public void ApplySlowEffect(float slowFactor, float duration) { // 被塔攻击时减速 agent.speed = originalSpeed * slowFactor; CancelInvoke(nameof(ResetSpeed)); // 取消之前的重置 Invoke(nameof(ResetSpeed), duration); // duration秒后重置速度 } private void ResetSpeed() { agent.speed = originalSpeed; } private bool HasReachedDestination() { return !agent.pathPending && agent.remainingDistance <= agent.stoppingDistance; } public void TakeDamage(int damage) { health -= damage; if (health <= 0) { Die(); } } private void Die() { // 死亡逻辑,例如播放动画、产生金币、从管理器列表中移除等 PathManager.Instance.RemoveEnemy(this); Destroy(gameObject); } }7.3 路径管理器(Waypoint System)
虽然NavMesh可以自动寻路,但塔防游戏通常需要敌人严格沿着设计好的路径走,即使有更短的“野路子”也不行。我们可以用路点(Waypoint)来引导。
- 在场景中创建一系列空物体作为路点(Waypoint),按顺序放置在路径上。
- 创建一个
PathManager单例脚本,管理所有路点和敌人。
using System.Collections.Generic; using UnityEngine; public class PathManager : MonoBehaviour { public static PathManager Instance; public List<Transform> waypoints = new List<Transform>(); private Dictionary<TDEnemy, int> enemyWaypointIndex = new Dictionary<TDEnemy, int>(); void Awake() { if (Instance == null) Instance = this; else Destroy(gameObject); } // 敌人出生时调用,分配起始路点 public void RegisterEnemy(TDEnemy enemy) { enemyWaypointIndex[enemy] = 0; // 从第一个路点开始 enemy.MoveToWaypoint(waypoints[0].position); } // 敌人到达当前路点后调用 public void EnemyReachedWaypoint(TDEnemy enemy) { if (enemyWaypointIndex.ContainsKey(enemy)) { int currentIndex = enemyWaypointIndex[enemy]; int nextIndex = currentIndex + 1; if (nextIndex < waypoints.Count) { // 分配下一个路点 enemyWaypointIndex[enemy] = nextIndex; enemy.MoveToWaypoint(waypoints[nextIndex].position); } else { // 到达终点,敌人漏了! EnemyReachedEnd(enemy); } } } private void EnemyReachedEnd(TDEnemy enemy) { // 扣玩家生命值等逻辑 GameManager.Instance.PlayerLoseLife(); RemoveEnemy(enemy); } public void RemoveEnemy(TDEnemy enemy) { if (enemyWaypointIndex.ContainsKey(enemy)) { enemyWaypointIndex.Remove(enemy); } // 从其他管理列表中移除... } }这个方案的精妙之处在于:我们结合了NavMesh的智能避障和路点系统的路径约束。敌人通过NavMeshAgent.SetDestination()前往下一个路点,但寻路过程是由NavMesh完成的,所以如果两个路点之间临时出现一个障碍物(比如玩家建造了一个临时路障),敌人会自动绕行,而不会傻傻地撞墙。到达路点后,再获取下一个目标。这样既保证了敌人沿预定路线前进,又拥有了动态避障的灵活性。
7.4 塔的攻击与减速区域
塔防游戏中,减速塔(Glue Tower)的效果可以通过NavMesh的区域代价系统优雅实现。
- 在Navigation窗口的Areas页,创建一个名为
Slow的区域,设置Cost为3。 - 在减速塔的攻击范围内,放置一个带有
Collider 2D(如Circle Collider 2D)的触发器物体。为其添加NavMesh Modifier组件,Override Area为Slow。 - 当敌人进入这个区域,由于该区域移动代价变高,它的寻路算法可能会尝试离开这个区域(如果路径可选)。同时,我们也可以在敌人进入触发器时,调用上面
TDEnemy脚本中的ApplySlowEffect方法,直接降低其agent.speed,实现更直接、可视的减速效果。两种方式可以叠加使用,Cost影响路径规划(长远决策),speed影响实时移动(即时效果)。
通过这个完整的塔防案例,你应该能体会到NavMeshPlus在2D游戏寻路中的强大与灵活。它不是一个死板的路径点系统,而是一个真正的、基于环境的智能导航解决方案。从简单的点击移动到复杂的动态策略游戏,它都能提供坚实的底层支持。关键在于理解其原理,并根据你的游戏类型进行恰当的配置和扩展。