Unity内置了一个比较完善的导航系统,一般称为Nav Mesh(导航网格),用它可以满足大多数游戏中角色自动导航的需求。
Unity的导航系统由以下几个部分组成:
- Nav Mesh。Nav Mesh与具体的场景关联,它定义了场景中可以通过的三角面和非三角面的通路。Unity可以自动构建出Nav Mesh。
- Nav Mesh Agent。Nav Mesh Agent作为组件,要挂载到需要自动导航功能的角色上,它会为角色添加朝目标移动和避开障碍的功能。Nav Mesh Agent必须位于Nav Mesh之上才能发挥作用。
- Nav Mesh Obstacle。该组件将物体标记为障碍物,如阻碍角色行动的木桶或箱子。障碍物有两种起效的途径,一是动态障碍,让Nav Mesh Agent利用规避算法尽可能规避它;二是静态障碍,静态障碍会在Nav Mesh上挖洞,Nav Mesh Agent会围绕着这个洞修改寻路路径。如果障碍完全阻挡了路径,那么Nav Mesh Agent会去寻找一条新的路径。
- Off Mesh Link。某些某些通路用普通方法无法通过,但可能根据游戏设计是可以通过的。例如,一条阻断了Nav Mesh的水沟,在游戏中可以跳过去;从较低的平台无法走上较高平台,但可以通过梯子爬上去。导航系统允许在不连通的区域之间建立链接,这种链接关系通过Off Mesh Link进行标记
在Unity中,静态障碍和动态障碍是指在导航系统中所采用的两种不同类型的障碍物,它们的区别主要体现在障碍物的移动性和对导航系统的影响。
1. 静态障碍(Static Obstacles):静态障碍物是指在游戏场景中位置不会发生改变的障碍物。它们通常是固定的环境元素,如墙壁、建筑物等。静态障碍物在导航系统的计算和路径规划中不会发生变化,只需要在初始化导航网格的时候标记出来即可。静态障碍物对导航系统的影响是恒定的,不会引起额外的计算开销。
2. 动态障碍(Dynamic Obstacles):动态障碍物是指游戏场景中位置可能发生改变的障碍物。它们通常是动态的游戏角色、NPC、移动平台等。动态障碍物在导航系统中需要不断更新其位置信息,以便导航系统能够实时地针对它们进行路径规划。动态障碍物的移动会影响导航网格的可行走区域,导致导航系统需要重新计算路径。为了减少计算开销,Unity的导航系统提供了一些优化技术,如局部更新和动态避障等,以提高动态障碍物的移动效率和导航性能。
总结来说,静态障碍物是固定的,对导航系统没有实时的影响;而动态障碍物是可以移动的,对导航系统的计算和路径规划有实时的影响,需要特殊处理以保证角色可以正确避开它们。
场景中的可移动区域一般包含了地形、平面、楼梯和斜坡等。要让导航系统自动处理这些地形,必须要事先在几何地形上搜集数据,预先计算参数,确定好导航面才可以。从几何关卡创建Nav Mesh的过程称为导航网格Bake。
Unity有一个专门的Navigation窗口用于烘培操作,其基本的烘培操作只需以下两个步骤:
- 将所有场景中与导航有关的物体设置为Navigation Static。一般来说这些物体包含地形、平面、斜坡、楼梯、墙和静态障碍物,但不应包含运动的物体。选中物体后,单机Inspector右上角的小三角,打开菜单,可以看到Navigation Static选项。
- 打开Navigation窗口,选择主菜单的Window→AI→Navigation。然后打开其中的第三个标签页Bake,再单击下方的Bake按钮即可开始烘培。如果场景小,烘培会立即完成。烘培后可以看到但蓝色标记的可移动区域
有淡蓝色覆盖的部分就是可行走区域,而且仔细观察可以看到一些连线,这些连线把区域划分成了凸多边形与三角形。烘培时计算的主要信息正式这些凸多边形或三角形区域,以及每个区域与周围区域的连通关系。
导航系统的工作机制介绍:
在Unity官方文档中介绍了导航系统的工作原理,配合烘培后的网格来看不难理解。导航系统负责的工作从烘培开始,然后到具体的角色移动路线(包括移动中避障),到角色移动到目的地位置,主要分为以下4个步骤:
- 根据设置将地形或模型烘培,形成用很多凸多边形表示的“小平面”。这其中用到的主要算法是三角形剖分算法。
- 运行游戏时,在制定了起点和终点后,计算角色应当通过哪些面。这其中需要用到搜索算法,如常用的A*。
- 只有连通的平面的信息还不够,还需要形成具体的路线。这里的算法主要考虑的是各种拐角,尽可能按照距离最近而且平滑的路线移动,不能让角色绕得太远。
- 在具体移动的过程中,还需要考虑躲避动态障碍物,以及集体排队导航时互相阻挡得问题。
可以明显看到,生成的导航区域在地图边缘、墙壁边缘都有一定收缩。这是因为Nav Mesh Agent(可以理解为自动移动的角色)本身具有一定的半径,换句话说,由于角色有一定体积,角色的中心不可能紧贴在墙边。
不仅是宽度,代理得高度也会影响哪些区域能够通过,哪些不能通过,甚至斜坡的坡度、楼梯的高度也对能否通过有影响。在Bake标签页中也可以对这些参数进行设置。
在导航系统的Bake标签页里,可以看到一个尺寸示意图。
虽然导航代理与场景烘培是两个不同的模块,但是Nav Mesh Agent的尺寸确实对烘培有影响,因此在烘培阶段就应该给出代理的参考尺寸。相关参数及其解释如下:
名称 | 中文名称 | 含义 |
Agent Radius | 角色半径 | 代理的半径,如半径0.5m的角色无法通过宽度小于1m的通道 |
Agent Height | 角色高度 | 代理的高度,代理无法通过低于此高度的通道 |
Max Slope | 最大坡度 | 代理无法登上角度大于此值的坡度 |
Step Height | 台阶高度 | 代理无法踏上高度大于此值的台阶 |
Drop Height | 最大掉落高度 | 与自动生成导航链接有关,最大能从多高的高度跳下 |
Jump Distance | 最远跳跃距离 | 与自动生成的导航链接有关,设定最远跳跃的距离 |
在修改这些参数时,既要考虑到场景尺寸,又要考虑到角色本身的尺寸,包括角色碰撞体的尺寸。通过合理的设置让可导航的路线符合游戏设计的需要,包括楼梯、坡道和通道等。
一般来说,角色的碰撞体积是用胶囊体表示的,这个胶囊体半径应该与代理半径一致或更小一些。例如,碰撞体半径大于代理半径,就可能出现导航系统认为能通过的窄路却被碰撞体阻挡的情况。再例如,角色站立时有1.8m,下蹲后可以通过0.9m的小洞,那么导航参数夜莺岛考虑到角色下蹲移动的情况。
当烘培完成后,再场景所在文件夹下,会多出一个与场景名称相同的文件夹,文件夹中有一个NavMesh.asset文件,里面就是烘培保存的数据了。
烘培好Nav Mesh后,就可以添加Nav Mesh Agent了。Nav Mesh Agent也是一个组件,它可调整的参数比较多。下面先做一个可以随鼠标点击移动的例子。
- 创建一个胶囊体代表玩家,紧贴底面(已经烘培好的导航面)放置。
- 给胶囊体添加Nav Mesh Agent组件,默认参数即可。
- 创建脚本MoveTo,让物体向鼠标点击的位置移动。
- 将脚本挂载到胶囊提上,将摄像机升高并低头,改为半俯视角。
- 运行游戏,通过鼠标左键单击地面,胶囊体会朝着目标寻路移动。
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public class MoveTo : MonoBehaviour
{
UnityEngine.AI.NavMeshAgent agent;
void Start()
{
agent = GetComponent();
}
void Update()
{
if(Input.GetMouseButtonDown(0))
{
Ray ray = Camera.main.ScreenPointToRay(Input.mousePosition);
RaycastHit hit;
if(Physics.Raycast(ray,out hit))
{
agent.destination = hit.point;
}
}
}
}
在运行游戏的同时打开导航烘培窗口,可以一边观察导航网格一边测试游戏,从而方便观察前往不同目的地的导航效果差异。特别是当单机没有被导航网格覆盖的白色区域时,物体依然会尽可能靠近目的地,停留在接近目的地的位置。
以上脚本通过摄像机发射射线,得到鼠标单击的位置。其操作导航代理的关键代码只有一条,如下所示:
agent.destination=hit.point;
只要设置了目的地,导航就会自动朝目标移动。
关于Nav Mesh Agent组件还有很多值得说明的参数和使用方法。下面先介绍Nav Mesh Agent组件的参数,再介绍Nav Mesh Agent组件的常用属性。
说明 | 名称 | 中文名称 | 说明 |
位置 | Base Offset | 中心偏移 | 代理中心与物体中心点的y轴偏差距离。由于代理会自动贴在导航面上移动,如果有偏差就需要调节这个值。 |
Steering (运动和 转向) |
Speed | 移动速度 | 导航移动的速度受到此参数限制 |
Angular Speed | 角速度 | 导航移动的转身速度 | |
Acceleration | 加速度 | 模拟角色由慢到快的加速度 | |
Stopping Distance | 停止距离 | 当离目标的距离小于此值时,就算到达了。此值控制允许误差的范围 | |
Auto Braking | 自动刹车 | 减速效果 | |
Obstacle Avoidance (躲避障碍) |
Radius | 避障半径 | 躲避障碍时的自身半径参考值,较大的值代表尽可能远离障碍 |
Height | 避障高度 | 避障时自身高度的参考值 | |
Quality | 避障品质 | 可调节避障算法的精度。精度越高,效果越好,对性能的影响也就越大 | |
Priority | 避障优先级 | 当多个代理一起避障移动时,数值小的代理优先级更高 |
属性 | 类型 | 用途 |
hasPath | bool | 当前是否有路径。也可以根据它判断是否到达目的地 |
isStopped | bool | 设置为true可以终止寻路 |
path | NavMeshPath | 获得当前路径对象 |
speed | float | Nav Mesh Agent组件设置的速度,其他参数也可以用脚本控制 |
AngularSpeed | float | Nav Mesh Agent组件设置的角速度 |
remainingDistance | float | 当前位置到终点的位置 |
destination | Vector3 | 终点位置,设置它可以启动寻路 |
stoppingDistance | float | Nav Mesh Agent组件的停止距离(即误差距离) |
isOnNavMesh | bool | 是否处于导航面上 |
velocity | Vector3 | 代理当前实际运动的速度向量,非常有用的属性 |
以上属性都可以通过脚本读取或设置
静态障碍物,如墙、水沟可以在烘培时就处理好,而在场景中动态添加的障碍物就无法预先烘培了。Nav Mesh Obstacle组件正是为标记动态障碍物而生的。其属性如图所示。
Nav Mesh Obstacle组件刻修改的参数不多,简单调整后即可使用。
障碍物的形状只有Box与Capsule两种。由于Nav Mesh Agent组件也有一定体积,因此没有必要定义精确的形状。选择形状后可以分别对位置、大小和半径等参数进行提及设置。
障碍物的存在会对导航产生影响,可选两种影响方式:阻碍和打洞。不勾选Carve选项就是阻碍,反之是打洞。
阻碍方式对导航层本身没有影响,就像是地图上的普通障碍物一样,Nav Mesh Agent组件会用规避算法尽量规避障碍物。阻碍方式的优点是不影响导航层,没有重新计算导航层的性能开销;缺点是规避效果取决于Nav Mesh Agent组件的具体移动,当障碍物较多、障碍物体积较大时,避障效果可能不会太理想。
打洞方式则会影响导航层本身的信息,就像是障碍物呗烘培到导航层上一样。Nav Mesh Agent组件在计算路径的阶段就会避开有洞的位置。
可以创建两个Nav Mesh Obetacle,测试勾选或不勾选Carve的效果。如图,上边是勾选的,下面是没有勾选的。
在开启导航窗口的情况下,两个Nav Mesh Obstacle的影响的区别显而易见。如果移动设置为打洞的那个障碍物,导航层会随之变化。
阻碍方式对性能影响较小,适合有一定运动速度、体积不太大的物体。打洞方式会引起导航层局部从新计算,带来一定的性能开销,因此适合于静态的、长期的地形变化。
例如,游戏中道路被掉落的石块堵死,适合用打洞方式实现;而角色过马路时路上的汽车等运动的小障碍,适合用阻碍方式实现。
下面介绍一下自动打洞设置。
在实际游戏中,障碍物的情况可能介于"快速变化"与"静止不变"之间,因此障碍物还支持更细节的控制。通过判断物体移动的距离和静止不动的时间,来确定打洞的时机,这一功能是通过Move Threshold(移动阈值)、Time To Stationary(固定时间)和Carve Only Stationary(仅固定时打洞)3个参数实现的。简单来说,在勾选了Carve Only Stationary的前提下,当物体移动距离超过了Move Threshold的值则取消打洞;当物体的静止时间超过了Time To Stationary的值,就可以被看作暂时固定,切换到固定状态时会进行打洞。
以上参数用文字解释比较抽象,有一个好方法可以快速直观测试这些参数的作用。
- 在一个烘培好的地形上放置两个导航障碍物,其中一个设置为打洞模式并勾选Carve Only Stationary,另一个设置为阻碍模式作为对照。
- 运行游戏,在场景中移动打洞障碍物的位置,当速度高于某个值时,导航的孔洞就会消失;当物体静止以后再经过一点时间,孔洞又会出现。这里的速度阈值和静止时间正是由Move Threshold和Time To Stationary分别控制的。
这些参数与实际游戏设计相关,只有在实际游戏开发中遇到了性能问题再仔细调整。通过这些参数可以控制大东的频率,平衡性呢个与导航效果之间的矛盾。