文章目录
- 1 项目介绍
- 2 模块介绍
- 2.1 BaseState
- 2.2 ...State
- 2.2.1 PatrolState
- 2.2.2 ChaseState / AttackState / BackState
- 2.3 StateMachine
- 2.4 Monster
- 3 其他功能
- 4 类图
项目借鉴 B 站唐老狮 2023年直播内容。 点击前往唐老狮 B 站主页。
1 项目介绍
本项目使用 Unity 2022.3.32f1c1,实现基本的 AI 框架。其中,用 Cube(绿色)代替怪物模型,Cube(红色)代替玩家,即 AI 目标。
项目地址:https://github.com/zheliku/StateMachine_AI。
2 模块介绍
2.1 BaseState
在框架介绍的基础上,添加了两个方法:
-
DistanceOfXZ(Vector3, Vector3)
用于计算 xz 平面上的距离(不考虑 y 轴方向)。
-
DrawGizmos()
用于辅助绘制范围,在 Scene 窗口中显示。
using UnityEngine;
/// <summary>
/// 状态基类
/// </summary>
public abstract class BaseState
{
public virtual EAIState AIState { get; } // 状态类型
protected StateMachine _stateMachine; // 附属的状态机
public BaseState(StateMachine stateMachine) {
_stateMachine = stateMachine;
}
public abstract void OnStateEnter();
public abstract void OnStateUpdate();
public abstract void OnStateExit();
/// <summary>
/// 辅助绘制范围,不强制重写
/// </summary>
public virtual void DrawGizmos() { }
/// <summary>
/// XZ 平面上的距离
/// </summary>
protected float DistanceOfXZ(Vector3 pos1, Vector3 pos2) {
pos1.y = pos2.y = 0;
return Vector3.Distance(pos1, pos2);
}
}
2.2 …State
项目实现了 4 种 AI 状态,包括
- PatrolState(巡逻状态)
- ChaseState(追逐状态)
- AttackState(攻击状态)
- BackState(返回状态)
2.2.1 PatrolState
PatrolState 实现较为详细,其将巡逻方式分为 3 种:
public enum EPatrolType
{
Stay, // 原地播放某个动作(睡觉、放哨等)
CircleMove, // 圆形范围内随机移动
PathMove // 按照路径移动
}
巡逻数据可以直接在 PatrolState 类中声明。本项目选择封装在 PatrolStateData 中。所有 Data 均继承 ScripteObject 类,可以在 Project 窗口中右键直接创建并配置对应数据。
-
PatrolStateData:所有巡逻种类的共有数据。
-
PatrolStateStayData:原地巡逻的数据。
-
PatrolStateMoveData:移动巡逻的数据。
-
PatrolStateCircleMoveData:圆形范围移动巡逻的数据。
-
PatrolStatePathMoveData:路径移动巡逻的数据。
-
Stay:
原地播放某个动作,因此需要 AI 动作枚举。目前只添加 Sleep 动作。
public enum EAction { Sleep, }
-
Move:
范围内随机移动,分为两种:Circle、Path。区别是获取下一次目标位置的方式不同,因此提取出如下逻辑:
public class PatrolState : BaseState { private PatrolStateData _data; // 巡逻数据 ... /// <summary> /// 移动 /// </summary> private void OnMoveUpdate(IAIObject aiObject, EPatrolType moveType) { var data = (PatrolStateMoveData)_data; // 转化数据 ... _data.targetPos = moveType switch { EPatrolType.CircleMove => CalCircleTargetPos((PatrolStateCircleMoveData)data), EPatrolType.PathMove => CalPathTargetPos((PatrolStatePathMoveData)data), _ => throw new ArgumentOutOfRangeException(nameof(moveType), moveType, null) }; ... } /// <summary> /// 更新圆形范围目标位置 /// </summary> private Vector3 CalCircleTargetPos(PatrolStateCircleMoveData data) { ... } /// <summary> /// 更新路径范围目标位置 /// </summary> private Vector3 CalPathTargetPos(PatrolStatePathMoveData data) { ... } ... }
2.2.2 ChaseState / AttackState / BackState
该 3 个状态都遵循以下大致框架:
using UnityEngine;
public class ...State : BaseState
{
private ...StateData _data;
public override EAIState AIState { get => ...; }
public AttackState(StateMachine stateMachine) : base(stateMachine) {
// 加载数据
var data = Resources.Load<...StateData>("StateData/.../...StateData");
_data = Object.Instantiate(data);
}
public override void OnStateEnter() { ... }
public override void OnStateUpdate() { ... }
public override void OnStateExit() { ... }
...
}
2.3 StateMachine
AddState()、ChangeState() 和 UpdateState() 逻辑如下:
/// <summary>
/// 添加 AI 状态
/// </summary>
public void AddState(EAIState state) {
switch (state) {
case EAIState.Patrol:
_stateDic.Add(state, new PatrolState(this));
break;
case EAIState.Back:
_stateDic.Add(state, new BackState(this));
break;
case EAIState.Chase:
_stateDic.Add(state, new ChaseState(this));
break;
case EAIState.Attack:
_stateDic.Add(state, new AttackState(this));
break;
default: throw new ArgumentOutOfRangeException(nameof(state), state, null);
}
}
/// <summary>
/// 切换状态
/// </summary>
/// <param name="state"></param>
public void ChangeState(EAIState state) {
_nowState?.OnStateExit(); // 退出状态
if (_stateDic.TryGetValue(state, out BaseState nowState)) { // 进入状态
_nowState = nowState;
_nowState.OnStateEnter();
}
}
/// <summary>
/// 更新当前状态
/// </summary>
public void UpdateState() {
_nowState?.OnStateUpdate();
_nowState?.DrawGizmos(); // 辅助绘图
}
2.4 Monster
怪物类实现了 IAIObject 接口(详见 2024-07-12 Unity AI状态机1 —— 框架介绍),通过 Unity 导航系统中的 NavMeshAgent 实现基本移动。除了 IAIObject 接口,还包含一些自己的数据。
using System;
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.AI;
public class Monster : MonoBehaviour, IAIObject
{
public Transform targetTransform; // 目标位置
public GameObject bullet; // 子弹
public float attackRange = 3; // 攻击范围
private Quaternion _startRotation; // 记录开始攻击时的角度
private Quaternion _targetRotation; // 记录目标角度
private float _rotateTime; // 旋转计时
private NavMeshAgent _navMeshAgent; // 导航代理
private StateMachine _aiMachine; // AI 状态机
private void Start() {
_navMeshAgent = GetComponent<NavMeshAgent>();
BornPos = transform.position;
_aiMachine = new StateMachine();
_aiMachine.Init(this);
// 为 AI 添加巡逻状态
_aiMachine.AddState(EAIState.Patrol);
_aiMachine.AddState(EAIState.Chase);
_aiMachine.AddState(EAIState.Attack);
_aiMachine.AddState(EAIState.Back);
// 更改初始状态
_aiMachine.ChangeState(EAIState.Patrol);
}
private void Update() {
_aiMachine.UpdateState();
}
#region IAIObject 接口实现
...
#endregion
}
3 其他功能
为了辅助绘图,为 BaseState 添加了 DrawGizmos() 方法(virtual),子类可以实现该方法,在 Scene 窗口中绘制辅助线。同时,在 BaseState 中添加 DistanceOfXZ() 方法,以便所有状态都可使用。以下是 BaseState 的全部逻辑:
using UnityEngine;
/// <summary>
/// 状态基类
/// </summary>
public abstract class BaseState
{
public virtual EAIState AIState { get; } // 状态类型
protected StateMachine _stateMachine; // 附属的状态机
public BaseState(StateMachine stateMachine) {
_stateMachine = stateMachine;
}
public abstract void OnStateEnter();
public abstract void OnStateUpdate();
public abstract void OnStateExit();
/// <summary>
/// 辅助绘制范围,不强制重写
/// </summary>
public virtual void DrawGizmos() { }
/// <summary>
/// XZ 平面上的距离
/// </summary>
protected float DistanceOfXZ(Vector3 pos1, Vector3 pos2) { ... }
}
辅助绘图时,使用 Unity Asset Store 中的插件 DrawXXL 实现,例如 ChaseState 中的 DrawGizmos 如下:
public override void DrawGizmos() {
var aiObject = _stateMachine.AIObject;
// 绘制攻击范围
DrawShapes.Circle(aiObject.Transform.position, aiObject.AttackRange, Color.red,
Vector3.up, lineWidth: 0.05f);
// 绘制脱离范围
DrawShapes.Circle(aiObject.Transform.position, _data.chaseDistance, new Color(1, 0.5f, 0),
Vector3.up, lineWidth: 0.05f, outlineStyle: DrawBasics.LineStyle.dotted);
}