AI导航系统让角色能够理解自身需要走楼梯才能到达二楼或跳过沟渠。智能地移动游戏中的角色(或者 AI 行业中所称的代理)时,必须解决两个问题:
- 如何_推断_关卡来寻找目标,
- 然后如何_移动_到该位置。
这两个问题是紧密相关的,但性质却截然不同。关卡推断问题更具全局性和静态性,因为需要考虑整个场景。移动到目标更具局部性和动态性,只考虑移动的方向以及如何防止与其他移动的代理发生碰撞。
1. 导航内容
1.1寻路
要寻找场景中两个位置之间的路径,我们首先需要将起始位置和目标位置映射到各自最近的多边形。然后,我们从起始位置开始搜索,访问所有邻居,直到我们到达目标多边形。通过跟踪被访问的多边形,我们可以找出从起点到目标的多边形序列。一种寻路的常用算法是 A*(发音为“A star”),这也是 Unity 使用的算法。
1.2 跟随路径
描述从起点到目标多边形的路径的多边形序列称为“走廊”(corridor)。代理将始终朝着走廊的下一个可见拐角移动,直至到达目标。如果一个简单游戏只有一个代理在场景中移动,可一次性找出走廊的所有拐角,并推动角色沿着连接拐角的线段移动。
1.3 躲避障碍物
转向逻辑将采用下一个拐角的位置并基于该位置计算出到达目标所需的方向和速度。使用所需的速度移动代理可能会导致与其他代理发生碰撞。障碍躲避系统将选择新的速度,该速度可平衡“代理在所需方向上移动”和“防止未来与其他代理及导航网格边缘发生碰撞”这两个问题。Unity 采用倒数速度障碍物 (RVO) 来预测和防止碰撞
1.4 移动代理
最后在转向和障碍躲避之后计算最终速度。在 Unity 中使用简单的动态模型来模拟代理,该模型还考虑了加速度以实现更自然和平滑的移动。在此阶段,您可以将速度从模拟的代理提供给动画系统,从而使用根运动移动角色,或让导航系统处理该问题。
使用任一方法移动代理后,模拟代理位置将移动并约束到导航网格。最后这一小步对于实现强大的导航功能非常重要
1.5 全局和局部
关于导航需要了解的最重要事项之一是全局和局部导航之间的区别。全局导航用于在整个世界中寻找走廊。在整个世界中寻路是一项代价高昂的操作,需要相当多的处理能力和内存。
描述路径的多边形的线性列表是用于转向的灵活数据结构,并可在代理的位置移动时进行局部调整。局部导航试图确定如何有效移动到下一个拐角而不与其他代理或移动对象发生碰撞。
2. Unity 导航网格 (NavMesh) 系统
可行走区域定义了代理可在场景中站立和移动的位置。在 Unity 中,代理被描述为圆柱体。可行走区域是通过测试代理可站立的位置从场景中的几何体自动构建的。然后,这些位置连接到场景几何体之上覆盖的表面。该表面称为导航网格(简称 NavMesh),包含以下部分:
- 导航网格(即 Navigation Mesh,缩写为 NavMesh)是一种数据结构,用于描述游戏世界的可行走表面,并允许在游戏世界中寻找从一个可行走位置到另一个可行走位置的路径。该数据结构是从关卡几何体自动构建或烘焙的。
- 导航网格代理 (NavMesh Agent) 组件可帮助您创建在朝目标移动时能够彼此避开的角色。代理使用导航网格来推断游戏世界,并知道如何避开彼此以及移动的障碍物。
- 网格外链接 (Off-Mesh Link) 组件允许您合并无法使用可行走表面来表示的导航捷径。例如,跳过沟渠或围栏,或在通过门之前打开门,全都可以描述为网格外链接。
- 导航网格障碍物 (NavMesh Obstacle) 组件可用于描述代理在世界中导航时应避开的移动障碍物。由物理系统控制的木桶或板条箱便是障碍物的典型例子。障碍物正在移动时,代理将尽力避开它,但是障碍物一旦变为静止状态,便会在导航网格中雕刻一个孔,从而使代理能够改变自己的路径来绕过它,或者如果静止的障碍物阻挡了路径,则代理可寻找其他不同的路线。
在 Unity 中,导航网格生成方式是在 Navigation 窗口(菜单:Window > AI > __Navigation__)中进行处理的。
为场景构建导航网格可以通过 4 个快速步骤完成:
- 选择应影响导航的场景几何体:可行走表面和障碍物。
- 选中 Navigation Static 复选框以便在导航网格烘焙过程中包括所选对象。
- 调整烘焙设置以匹配代理大小。 - Agent Radius 定义代理中心与墙壁或窗台的接近程度。- Agent Height 定义代理可以达到的空间有多低。- Max Slope 定义代理走上坡道的陡峭程度。- Step Height 定义代理可以踏上的障碍物的高度。
- 单击 Bake 以构建导航网格。
每当 Navigation 窗口打开且可见时,生成的导航网格便会在场景中显示为底层关卡几何体上的蓝色覆盖层。
您可能已经在上面的图片中注意到,生成的导航网格中的可行走区域显示为缩小状态。导航网格表示代理中心可进行移动的区域。从概念上讲,无论将代理视为缩小的导航网格上的点还是全尺寸的导航网格上的圆都无关紧要,因为这两者是等效的。但是,解释为点有助于提高运行时效率,并可让设计人员立即看到代理是否可以挤过间隙而不用担心代理半径问题。
另外要记住的是导航网格是可行走表面的近似形状。例如,在楼梯中就能看出这一点:楼梯表示为平坦表面,但原始表面是有台阶的。这种表示方式是为了使导航网格数据大小保持较小。这种近似表示方式的副作用是,有时您会希望在关卡几何体中留出一些额外的空间,让代理能够通过一个狭窄位置。
烘焙完成后,您将在一个与导航网格所属场景同名的文件夹中找到导航网格资源文件。例如,如果在 Assets 文件夹中有一个名为 First Level 的场景,则导航网格将位于 Assets > First Level > NavMesh.asset。
3. 导航操作方法
3.1 告诉导航网格代理移动到目标位置
只需将 NavMeshAgent.destination 属性设置为您希望代理移动到的点,即可告诉代理开始计算路径。计算完成后,代理将自动沿路径移动,直至到达目标位置。下面的代码实现了一个简单的类,该类使用一个游戏对象来标记在 Start 函数中分配给 destination 属性的目标点。请注意,该脚本假定您已从 Editor 中添加并配置了导航网格代理 (NavMeshAgent) 组件。
// MoveDestination.cs
using UnityEngine;
public class MoveDestination : MonoBehaviour {
public Transform goal;
void Start () {
NavMeshAgent agent = GetComponent<NavMeshAgent>();
agent.destination = goal.position;
}
}
// MoveDestination.js
var goal: Transform;
function Start() {
var agent: NavMeshAgent = GetComponent.<NavMeshAgent>();
agent.destination = goal.position;
}
3.2 在一组点之间进行代理巡逻
许多游戏都有 NPC 负责在游戏区域周围自动巡逻。使用导航系统可实现此行为,但它比标准寻路方法稍微复杂一些;标准寻路方法仅使用两点之间的最短路径就可以实现有限且可预测的巡逻路线。为获得更有说服力的巡逻模式,可保留一组“有用”的关键点让 NPC 以某种顺序通过并访问它们。例如,在迷宫中,可将关键巡逻点放置在交叉点和拐角处,从而确保代理检查每个走廊。对于办公楼,关键点可能是各个办公室和其他房间。
理想的巡逻点序列将取决于所需的 NPC 行为方式。例如,机器人可能只是按照有条不紊的顺序访问这些点,而人类守卫可能会尝试通过使用更随机的模式来发现玩家。可使用下面显示的代码实现机器人的简单行为。
应使用公共变换数组将巡逻点提供给脚本。可从检视面板分配此数组,并使用游戏对象来标记巡逻点的位置。GotoNextPoint 函数可设置代理的目标点(也开始移动代理),然后选择将在下次调用时使用的新目标。就目前而言,该代码将按照巡逻点在数组中出现的顺序循环遍历巡逻点,但您可以轻松修改这种设置,例如使用 Random.Range 来随机选择数组索引。
在 Update 函数中,该脚本使用 remainingDistance 属性检查代理与目标的接近程度。当此距离非常小时,将调用 GotoNextPoint 来启动巡逻的下一段。
// Patrol.cs
using UnityEngine;
using UnityEngine.AI;
using System.Collections;
public class Patrol : MonoBehaviour {
public Transform[] points;
private int destPoint = 0;
private NavMeshAgent agent;
void Start () {
agent = GetComponent<NavMeshAgent>();
// 禁用自动制动将允许点之间的
// 连续移动(即,代理在接近目标点时
// 不会减速)。
agent.autoBraking = false;
GotoNextPoint();
}
void GotoNextPoint() {
// 如果未设置任何点,则返回
if (points.Length == 0)
return;
//将代理设置为前往当前选定的目标。
agent.destination = points[destPoint].position;
//选择数组中的下一个点作为目标,
// 如有必要,循环到开始。
destPoint = (destPoint + 1) % points.Length;
}
void Update () {
//当代理接近当前目标点时,
// 选择下一个目标点。
if (!agent.pathPending && agent.remainingDistance < 0.5f)
GotoNextPoint();
}
}
4. 项目练习
下载本文置顶的Unity初始项目,解压缩后在Unity Hub中打开,在Unity Editor界面中Scene 场景窗口展示下图:
- 蓝色的墙,包含界外的后补墙和地板作为导航障碍物
- 灰色的地面 -导航网格平面
- 2个(1大1小)绿色的胶囊为导航代理,1个红色的胶囊是 Player
- 1个黄色的长方体是目标物
4.1 创建导航网格平面
- 在Hierarchy窗口选择 Plane 游戏对象,然后在右侧Inspector窗口的底部,点击“Add Component”,输入NavMeshSurface,在列表中显示该内容后,添加 NavMeshSurface 部分到Plane。
- 仔细查看一下NavMeshSurface中的各个属性
- Agent Type 是指导航网格代理(agent)的类别,默认唯一是Humanoid,下面4.2部分将创建新的类别,方便控制不同类别的代理;
- Default Area是网格平面中可解除的区域,默认是可行走区域;
- Collect Objects是指能够收集的游戏对象,即该平面能够影响的游戏对象
- Include Layers是指该平面能够包含的层数,默认是所有层,下面4.2会创建新的平面,方便控制。
- 先不改变上述参数,点击下方的 Bake 按键,在平面游戏对象上烘焙常见的导航网格平面,这样就成功创建了导航网格平面,下图中新增的青色区域就是可行走的区域,限制代理的导航!
4.2 创建导航网格代理
- 在Hierarchy窗口选择Capsule 游戏对象(大的绿色胶囊),然后在右侧Inspector窗口的底部,点击“Add Component”,输入Nav Mesh Agent,在列表中显示该内容后,添加 NavMeshAgent 部分到Capsule 。
- 查看Nav Mesh Agent中的各个属性:
- Agent Type 指当前Agent的类别,默认是Humanoid,如果点击该下拉菜单,有增加 agent 类别的选项;
- Speed 是Agent在场景中移动的速度;Angular Speed是指角速度。。
- Radius是Agent的半径,Height是高度,可以通过设置不同的半径和高度来限制Agent的大小
- 在同一个窗口,点击上方 Layer 右边的下拉菜单,选择 Add Layer, 创建新的层,该层只包含同类型物体
- 在Tags and Layer 窗口中添加两个层: Character 与 Destination,
在Hierarchy窗口再次选择Capsule 游戏对象,在右侧 Inspector 窗口的 Layer 右边下拉菜单选择刚刚新建的 Character 层。
仿照上述步骤4,在Hierarchy窗口选择CapsuleShort 游戏对象(小的绿色胶囊),然后添加 NavMeshAgent 部分到CapsuleShort 。
10 在右侧NavMeshAgent部分第一行 Agent Type右侧点击下拉菜单,选择 Open Agent Settings。
- 在弹出的 Navigation 窗口中,点击Agent Type下方的 “+” 增加一个agent类别,并在下方Name中输入名称 “HumannoidShort”, Height设置为 0.5,然后关闭该窗口
重复上述步骤10,选择类别为 HumannoidShort
在该Inspector窗口顶部选择 Layer 为Character。
点击Unity Editor的 File菜单,选择Save,保存上述修改内容。
4.3 设置目标
- 在Hierarchy窗口选择Cube游戏对象,在右侧 Inspector 窗口的 Layer 右边下拉菜单选择刚刚新建的 Destination层。
- 上图中,点击 Tag 边的下拉菜单,选择 Add Tag,在弹出窗口中点击 Tags下方“+”,增加1个新的tag: Destination
- 返回步骤15中的界面,选择tag为新建立的 Destination。
4.4 自动导航到终点目标
- 在Hierarchy窗口选择Capsule 游戏对象,然后在右侧Inspector 窗口底部点击 “Add Component”,添加1个新的脚本(New Script),取名"AIController", 双击 Script右边的输入框,在visual Studio中打开该脚本
- 在Visual studio中输入下列黑体部分内容, 并保存。
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.AI;
public class AIController : MonoBehaviour
{
** private GameObject destination; //声明目标游戏对象
private NavMeshAgent agent; //声明导航网格代理**
// Start is called before the first frame update
void Start()
{
**// 基于"Destination"tag找到场景中目标游戏对象
destination = GameObject.FindGameObjectWithTag("Destination");
//基于NavMeshAgent属性找到导航网格代理对象
agent = GetComponent<NavMeshAgent>();
//设置该导航网格代理对象的位置为目标游戏对象的位置
agent.SetDestination(destination.transform.position);**
}
}
返回Unity Editor界面,等待编译完成后,点击运行,看游戏窗口中Capsule是否会自动移动到Cube的位置?
没有移动,因为导航网格平面需要解除对Capsule和Cube的包含,在Hierarchy窗口选择Plane对象,在右侧Inspector 中 NavMeshSurface中 Include Layers属性中,将Character和Destination两个新建的层取消勾选,然后再次点击底部的 Bake 按键
再次点击运行,游戏窗口中Capsule将自动移动到Cube的位置。可以尝试将 Capsule初始位置更改,运行后看是否还会自动移动到Cube的位置。
在 Hierarchy选择CapsuleShort 游戏对象,然后在Project 窗口中,Assets目录下,将新建的AIController脚本拖拽到右边的Inspector窗口的底部,这样增添了一个新的脚本文件。
点击运行,游戏窗口中CapsuleShort是否自动移动到Cube的位置?如下错误信息将会显示在Console窗口
这表明该 CapsuleShort 还没有加入到导航网格平面。在 Hierarchy选择 Plane 游戏对象,在右侧 Inspector 窗口的底部,点击“Add Component”,添加1个新的 NavMeshSurface 部分
设置该NavMeshSurface内容如下:
- 再次点击运行,游戏窗口中CapsuleShort 与 Capsule 都会自动移动到Cube的位置,可以细调两者的速度参数,使得一前一后容易辨认。
4.5 创建导航障网格障碍物(Obstacle)
在Hierarchy选择CubeV2-1 游戏对象,然后在右侧的Inspector窗口底部点击 “Add Component”,添加一个 Nav Mesh Obstacle 对象,
在Scene场景窗口移动该CubeV2-1到如下图所示位置,挡住原先Capsule到目标的直线路径。
- 点击Unity 界面正中间上方的 运行按键,运行游戏,有什么发现?
(两个Capsule被挡在上述新增的墙前面,但不会寻找边上已有的路径绕过该墙到达目标物)
- 在选中CubeV2-1情形下,查看右侧Inspector窗口中刚刚新加的 Nav Mesh Obstacle 对象,将 Carve属性右边的方框勾选
- 运行游戏,会发现两个Capsule会按原先定义的路径绕过该墙到达目标物。可以尝试在运行期间,两个Capsule在绕行的时候,实时移动 CubeV2-1 回到原先位置,这是Capsule移动轨迹是否实时改变?
4.6 创建导航网格调整物(Modifier)
- 将CubeV2-1恢复到原先位置,选择 **CubeV2-2 **放到刚才阻断Capsule移动的位置,当做一个门槛挡板。
点击运行按键,运行游戏,可以看到两个Capsule可以直接穿行过去。
在 Hierarchy 窗口选择 Plane,然后在右侧的Inspector窗口找到第一个 NavMeshSurface对象,点击其底部的 Bake 按键,将对应大的Capsule的导航网格平面再次烘焙,可以看到情商的平面在蓝色墙的上面
再次点击运行按键,运行游戏,可以看到大的Capsule在过该挡板的时候有一个向上走再下来的过程,而小的Capsule还是直接穿行。
重复上述步骤35,将第二个NavMeshSurface对象再次烘焙一次。
再次点击运行按键,运行游戏,可以看到两个Capsule在穿过该挡板的时候都有一个向上走再下来的过程。
Hierarchy选择CubeV2-2 游戏对象,然后在右侧的Inspector窗口底部点击 “Add Component”,添加一个 **Nav Mesh Modifier ** 对象,
- 设置该NavMeshModifier内部属性,
- Affected Agents,选择是影响小的Capsule
- 勾选Apply To Childre
- 勾选Override Area,然后在显示的Area Type下拉菜单中选择 Not Workable
这样可以阻止小的Capsule穿过该挡板了。
在 Hierarchy 窗口选择 Plane,然后在右侧的Inspector窗口找到第一个 NavMeshSurface对象,点击其底部的 Bake 按键,再找到第二个 NavMeshSurface对象,点击其底部的 Bake 按键,将两个导航网格平面再次烘焙。
点击运行按键,运行游戏,可以看到大的Capsule 还是按上次方式穿过该挡板,然后小的Capsule则会绕行穿过墙到达目标物。
4.7 跟随Player
设置一个 Player游戏对象,用鼠标控制该Player的移动位置,然后两个Capsule将跟随这个Player移动而变化位置
- 在 Scene 窗口选择红色 CapsulePlayer 游戏对象,将其移到平面上一个任意位置
- 在右侧Inspector窗口的上端,更新tag 为 “Player”。
- 在点击底部 “Add Component”按键,添加一个new Script,命名“PlayerMovement”,双击 script 右边输入框中的文件名,打开visual studio编辑该脚本
- 用如下代码覆盖原有脚本中的内容,保存脚本后回到Unity界面。
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public class PlayerMovement : MonoBehaviour
{
public float moveSpeed = 2.0f; //移动速度
private Vector3 targetPosition; //移动的目标位置
public LayerMask groundLayer;//d地板平面
void Start()
{
targetPosition = transform.position; //初始目标位置为当前的位置
}
void Update()
{
if (Input.GetMouseButton(0)) //响应鼠标左键
{
Ray ray = Camera.main.ScreenPointToRay(Input.mousePosition); //基于鼠标左键点击位置,从当前摄像头看过去的视线
RaycastHit hit;
if (Physics.Raycast(ray, out hit, Mathf.Infinity, groundLayer)) //上述视线与地板平面的交点,即为目标位置
{
targetPosition = hit.point; //更新位置
targetPosition.y = transform.position.y; //保持Y轴不变,还在同一平面上
}
}
float step = moveSpeed * Time.deltaTime; //计算移动的每一步的距离
transform.position = Vector3.MoveTowards(transform.position, targetPosition, step); //开始移动
}
}
- 在Unity界面,看到步骤44中所示图的脚本下面多了一个Ground Layer属性,该属性是在上述代码中加入了,在边上的下拉菜单中选择 “Default”
- 在Hierarchy 窗口选择大的Capsule对象,在右侧Inspector窗口,点击AIController(Script)对象右边的三点,在弹出菜单中选择“Remove Component”,移除该脚本对象
- 再点击该窗口中 “Add Component” 按键,添加一个新的脚本,命名为“AIControllerPlayer”,双击框中的文件名,在visual studio中编辑该脚本
- 在visual studio 用如下代码覆盖原有的代码,保存后回到unity界面。
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.AI;
public class AIControllerPlayer : MonoBehaviour
{
private GameObject destination; // 声明要跟随的目标对象
private NavMeshAgent agent; // 声明当前要移动的capsule
// Start is called before the first frame update
void Start()
{
destination = GameObject.FindGameObjectWithTag("Player"); //找到场景中的tag为player 的红色Capsule
agent = GetComponent<NavMeshAgent>(); //基于NavMeshAgent属性找到导航网格代理对象
}
private void Update()
{
agent.SetDestination(destination.transform.position); //每次场景更新,自动移动网格代理对象
}
}
点击运行按键,运行游戏,在Game游戏窗口中可以看到大的绿色Capsule移动到红色Capsule处不动了, 小的绿色Capsule则会绕行穿过墙到达目标物。
同样在Game 游戏窗口,用鼠标左键点击平面上其他位置,看到红色的Capsule会逐渐移动到鼠标点击的位置,绿色的大Capsule则会跟随红色Capsule移动轨迹而移动。
版权归原作者 m0_66358314 所有, 如有侵权,请联系我们删除。