Unity 内的敌人AI
作者:孙广东
一、Enemy Aim AI
目的
本文旨在让您了解如何使用 Enemy Aim AI。通过学习,您将掌握让敌人持续监视玩家的方法。
原理概述
Enemy aim AI 在需要敌人持续监视玩家的场景中非常实用。在真实世界场景里,物体获取目标需要时间,因此敌人在锁定目标系统前也会有一定的反应时间。这种效果可以通过对敌人朝向玩家的旋转角度进行插值(Lerping)来实现,这在动作游戏中,敌人跟随、瞄准并射击玩家的场景中尤为有用。此前发布的博客中已讨论过敌人跟随的概念,在实现游戏中的相关功能时,理解四元数的概念至关重要。
四元数用于存储对象的旋转信息,还能计算方向。虽然可以直接使用欧拉角,但可能会出现“万向锁”的问题。在本地坐标系中,如果先旋转模型的 X 轴,其 Y 轴和 Z 轴会因万向节锁而“锁定”在一起。
示例步骤
1. 创建代表玩家的 Cube
- 创建一个 Cube,将其作为玩家控制的对象。
- 为其网格渲染器应用适当的材质。
- 将
TargetMovementScript脚本应用到该游戏对象上,使 Cube 能根据玩家的指令移动。
以下是 TargetMovementScript.cs 的代码:
public class TargetMovementScript : MonoBehaviour
{
public float targetSpeed = 9.0f; // 物体移动的速度
void Update()
{
transform.Translate(Input.GetAxis("Horizontal") * Time.deltaTime * targetSpeed,
Input.GetAxis("Vertical") * Time.deltaTime * targetSpeed, 0);
}
}
2. 创建敌人对象
敌人对象由箭头和 Cube 组成,具体步骤如下:
- 为 Cube 应用适当的材质。
- 将箭头 Sprite 作为一个独立的对象。
- Cube 指示箭头的方向,箭头充当敌人的枪。
- 箭头将指向目标对象,模拟敌人试图锁定目标的效果。
- 可以操纵敌人锁定目标的速度,不同的敌人应具有不同的难度和功能。例如,坦克锁定目标的时间应比持枪士兵长。
以下是 EnemyAimScript.cs 的代码:
public class EnemyAimScript : MonoBehaviour
{
public Transform target; // 目标对象
public float enemyAimSpeed = 5.0f; // 敌人锁定目标的速度
Quaternion newRotation;
float orientTransform;
float orientTarget;
void Update()
{
orientTransform = transform.position.x;
orientTarget = target.position.x;
// 检查目标在物体的哪一侧(右侧或左侧)
if (orientTransform > orientTarget)
{
// 计算旋转角度,使箭头指向目标
newRotation = Quaternion.LookRotation(transform.position - target.position, -Vector3.up);
}
else
{
newRotation = Quaternion.LookRotation(transform.position - target.position, Vector3.up);
}
// 冻结 X 和 Y 轴的旋转,确保箭头正确移动
newRotation.x = 0.0f;
newRotation.y = 0.0f;
// 最终旋转并瞄准目标方向
transform.rotation = Quaternion.Lerp(transform.rotation, newRotation, Time.deltaTime * enemyAimSpeed);
// 另一种实现方式
// transform.rotation = Quaternion.RotateTowards(transform.rotation, newRotation, Time.deltaTime * enemyAimSpeed);
}
}
备注
- 可以改变敌人的目标并设置锁定目标的速度。
- 可以通过允许 X 或 Y 轴旋转的脚本来深入理解相关概念。
- 可以为敌人添加 Follow 脚本,使其能够跟随并瞄准玩家。
- 除了使用
Quaternion.Lerp,还可以使用Quaternion.RotateTowards达到相同的效果。
二、通过有限状态机实现 AI
目的
本文将介绍如何使用有限状态机模型在 Unity 中实现 AI。
FSM 基础知识
在游戏中,使用有限状态机框架(Finite State Machine Framework)实现 AI 是一种理想的选择,它能在不使用复杂代码的情况下产生出色的效果。
有限状态机是一个由一个或多个状态组成的模型,在某一时刻只有一个状态处于活动状态。为了执行不同的操作,机器必须在不同状态之间进行转换。该框架常用于管理、组织和表示游戏中的不同状态和执行流,在实现人工智能方面非常有用。例如,敌人的“大脑”可以使用有限状态机来实现,每个状态代表一个动作,如巡逻、追逐、逃避或射击等。
AI FSMs 的工作方式与 Unity 的动画 FSMs 类似,动画状态会根据要求进行切换。可以通过使用第三方插件(如行为树)来实现 AI FSM,也可以直接通过脚本实现。
为了获取有限状态机框架的基本概念,我们将通过一个简单的教程,使用 switch 语句来实现。后续还将学习如何使用框架使 AI 实现更易于管理和扩展。
示例步骤
我们将创建两个 Box,一个由玩家控制,另一个由 AI 控制。AI 控制的 Box 有追逐(chasing)和巡逻(patrolling)两种状态。当玩家控制的 Box 接近 AI 控制的 Box 时,AI Box 将进入追逐状态;当玩家 Box 远离到一定距离时,AI Box 将切换回巡逻状态。
Step - 1: 场景设置
在场景中设置一个平面和两个 Box,布局如下图所示。
Step - 2: 创建并放置游戏对象
- 创建空的游戏对象,并将其命名为 Wanderer Points,将这些空对象放置在平面周围。
- 蓝色的 Cube 为 AI Cube,红色的 Cube 为玩家控制的 Cube,将它们放置在适当的距离。
Step - 3: 实现 BoxMovement 脚本
实现 BoxMovementScript 脚本来控制玩家的 Cube 的移动,代码如下:
public class BoxMovementScript : MonoBehaviour
{
public float speed = 0.1f;
private Vector3 positionVector3;
void Update()
{
InitializePosition();
if (Input.GetKey(KeyCode.LeftArrow))
{
GoLeft();
}
if (Input.GetKey(KeyCode.RightArrow))
{
GoRight();
}
if (Input.GetKey(KeyCode.UpArrow))
{
GoTop();
}
if (Input.GetKey(KeyCode.DownArrow))
{
GoDown();
}
RotateNow();
}
private void InitializePosition()
{
positionVector3 = transform.position;
}
private void RotateNow()
{
Quaternion targetRotation = Quaternion.LookRotation(transform.position - positionVector3);
transform.rotation = targetRotation;
}
private void GoLeft()
{
transform.position = transform.position + new Vector3(-speed, 0, 0);
}
private void GoRight()
{
transform.position = transform.position + new Vector3(speed, 0, 0);
}
private void GoTop()
{
transform.position = transform.position + new Vector3(0, 0, speed);
}
private void GoDown()
{
transform.position = transform.position + new Vector3(0, 0, -speed);
}
}
Step - 4: 构建 FSM 模型脚本
以下是 FSM.cs 的代码:
public class FSM : MonoBehaviour
{
// 玩家的 Transform
protected Transform playerTransform;
// Box 的下一个目标位置
protected Vector3 destPos;
// 巡逻点列表
protected GameObject[] pointList;
protected virtual void Initialize()
{
}
protected virtual void FSMUpdate()
{
}
protected virtual void FSMFixedUpdate()
{
}
void Start()
{
Initialize();
}
void Update()
{
FSMUpdate();
}
void FixedUpdate()
{
FSMFixedUpdate();
}
}
Step - 5: 构建 Box 的 AI 脚本
以下是 BoxFSM.cs 的代码:
public class BoxFSM : FSM
{
public enum FSMState
{
None,
Patrol,
Chase
}
// 当前 Box 所处的状态
public FSMState curState;
// Box 的移动速度
private float curSpeed;
// Box 的旋转速度
private float curRotSpeed;
// 初始化 AI 驱动的 Box 的有限状态机
protected override void Initialize()
{
curState = FSMState.Patrol;
curSpeed = 5.0f;
curRotSpeed = 1.5f;
// 获取巡逻点列表
pointList = GameObject.FindGameObjectsWithTag("WandarPoint");
// 为巡逻状态设置随机目标点
FindNextPoint();
// 获取目标敌人(玩家)
GameObject objPlayer = GameObject.FindGameObjectWithTag("Player");
playerTransform = objPlayer.transform;
if (!playerTransform)
{
print("Player doesn't exist.. Please add one with Tag named 'Player'");
}
}
// 每帧更新
protected override void FSMUpdate()
{
switch (curState)
{
case FSMState.Patrol:
UpdatePatrolState();
break;
case FSMState.Chase:
UpdateChaseState();
break;
}
}
protected void UpdatePatrolState()
{
// 到达当前目标点后,寻找下一个随机巡逻点
if (Vector3.Distance(transform.position, destPos) <= 2.5f)
{
print("Reached to the destination point\ncalculating the next point");
FindNextPoint();
}
// 检查与玩家 Box 的距离,接近时切换到追逐状态
else if (Vector3.Distance(transform.position, playerTransform.position) <= 15.0f)
{
print("Switch to Chase State");
curState = FSMState.Chase;
}
// 旋转到目标点
Quaternion targetRotation = Quaternion.LookRotation(destPos - transform.position);
transform.rotation = Quaternion.Slerp(transform.rotation, targetRotation, Time.deltaTime * curRotSpeed);
// 向前移动
transform.Translate(Vector3.forward * Time.deltaTime * curSpeed);
}
protected void FindNextPoint()
{
print("Finding next point");
int rndIndex = Random.Range(0, pointList.Length);
float rndRadius = 5.0f;
Vector3 rndPosition = Vector3.zero;
destPos = pointList[rndIndex].transform.position + rndPosition;
// 检查移动范围,若不合适则重新确定随机点
if (IsInCurrentRange(destPos))
{
rndPosition = new Vector3(Random.Range(-rndRadius, rndRadius), 0.0f, Random.Range(-rndRadius, rndRadius));
destPos = pointList[rndIndex].transform.position + rndPosition;
}
}
protected bool IsInCurrentRange(Vector3 pos)
{
float xPos = Mathf.Abs(pos.x - transform.position.x);
float zPos = Mathf.Abs(pos.z - transform.position.z);
if (xPos <= 8 && zPos <= 8)
{
return true;
}
return false;
}
protected void UpdateChaseState()
{
// 将目标位置设置为玩家的位置
destPos = playerTransform.position;
// 检查与玩家 Box 的距离,若玩家太远则切换回巡逻状态
float dist = Vector3.Distance(transform.position, playerTransform.position);
if (dist >= 15.0f)
{
curState = FSMState.Patrol;
FindNextPoint();
}
// 旋转到目标点
Quaternion targetRotation = Quaternion.LookRotation(destPos - transform.position);
transform.rotation = Quaternion.Slerp(transform.rotation, targetRotation, Time.deltaTime * curRotSpeed);
// 向前移动
transform.Translate(Vector3.forward * Time.deltaTime * curSpeed);
}
}
注意事项
此脚本适用于需要跟随玩家的 Cube,不要忘记将玩家对象标记为 Player,将巡逻点对象标记为 WandarPoint。FSMUpdate() 方法会在子类中重写,并在每个 Update() 中执行。通过 switch 语句,脚本可以根据当前状态执行相应的操作,因此扩展 AI 非常简单,只需添加新的状态即可。Initialize() 方法也会在 Start() 方法中被调用执行。
结论
有限状态机(FSM)易于理解和实现,可用于执行复杂的 AI。它可以用图来表示,方便开发人员理解,开发人员可以轻松调整、改变和优化最终结果。有限状态机使用函数或方法来表示状态执行,简单、强大且易于扩展。使用基于堆栈的状态机可以确保执行流易于管理和稳定,即使在实现更复杂的 AI 时也不会对代码产生负面影响。因此,使用有限状态机可以让您的敌人更聪明,助力您的游戏取得成功。