Unity 内的敌人AI

2016年01月08日 11:48 0 点赞 0 评论 更新于 2025-11-21 19:40

作者:孙广东

一、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,将巡逻点对象标记为 WandarPointFSMUpdate() 方法会在子类中重写,并在每个 Update() 中执行。通过 switch 语句,脚本可以根据当前状态执行相应的操作,因此扩展 AI 非常简单,只需添加新的状态即可。Initialize() 方法也会在 Start() 方法中被调用执行。

结论

有限状态机(FSM)易于理解和实现,可用于执行复杂的 AI。它可以用图来表示,方便开发人员理解,开发人员可以轻松调整、改变和优化最终结果。有限状态机使用函数或方法来表示状态执行,简单、强大且易于扩展。使用基于堆栈的状态机可以确保执行流易于管理和稳定,即使在实现更复杂的 AI 时也不会对代码产生负面影响。因此,使用有限状态机可以让您的敌人更聪明,助力您的游戏取得成功。

作者信息

洞悉

洞悉

共发布了 3994 篇文章