本文简单介绍如何基于FSM有限状态机实现Enemies AI,首先定义敌人的AI逻辑:默认状态下Enemy为巡逻状态,有若干巡逻点位,Enemy在这些点位之间来回巡逻走动,同时检测Player的位置,当Player进入一定范围内时,Enemy进入寻路状态,寻路到Player位置前,进入Attacking攻击状态,当Player离开一定距离时,Enemy重回巡逻状态进行巡逻。
Patrol State
:巡逻状态Path Finding State
:寻路状态Attacking State
:攻击状态如图所示,我们预设了三个巡逻点,Enemy会在这三个巡逻点之间来回移动巡逻,并且在到达一个巡逻点时,会随机休息几秒,首先在OnDrawGizmos
函数中绘制出三个点的Position Handle
,方便我们调试:
//巡逻点集合
[SerializeField] private Transform[] patrolPoints;
private void OnDrawGizmos()
{
for (int i = 0; i < patrolPoints.Length; i++)
{
Handles.PositionHandle(patrolPoints[i].position, Quaternion.identity);
Handles.Label(patrolPoints[i].position, string.Format("Patrol Point {0}", i + 1));
}
}
动画相关变量与参数如下:
//动画组件
[SerializeField] private Animator animator;
private class AnimatorParams
{
public static readonly int Idle = Animator.StringToHash("Idle");
public static readonly int Walk = Animator.StringToHash("Walk");
public static readonly int Run = Animator.StringToHash("Run");
public static readonly int Action = Animator.StringToHash("Action");
}
寻路功能使用Unity内部功能NavMeshAgent
:
//寻路代理
[SerializeField] private NavMeshAgent agent;
定义Patrol State
:
private class PatrolState : State
{
//当前巡逻点的索引值
public int index;
//休息计时
public float timer;
}
创建状态机并构建状态:
private void Start()
{
var machine = StateMachine.Create("Enemy AI")
.Build<PatrolState>("巡逻状态")
.OnEnter(s =>
{
agent.isStopped = false;
//StopDistance设为0
agent.stoppingDistance = 0f;
//设置速度
agent.speed = 1f;
//进入巡逻状态时 设置第一个巡逻点
s.index = 0;
agent.SetDestination(patrolPoints[s.index].position);
//设置动画参数 进入Walk
animator.SetBool(AnimatorParams.Idle, false);
animator.SetBool(AnimatorParams.Walk, true);
})
.OnStay(s =>
{
//判断是否到达目标巡逻点
if (Vector3.Distance(transform.position, patrolPoints[s.index].position) <= .1f)
{
//设置动画参数 进入Idle
animator.SetBool(AnimatorParams.Walk, false);
animator.SetBool(AnimatorParams.Idle, true);
//到达后随机休息若干秒
s.timer += Time.deltaTime;
if (s.timer >= Random.Range(3f, 5f))
{
//重置计时器
s.timer = 0f;
//设置下一个巡逻点
s.index++;
s.index = s.index == patrolPoints.Length ? 0 : s.index;
agent.SetDestination(patrolPoints[s.index].position);
//设置动画参数 进入Walk
animator.SetBool(AnimatorParams.Idle, false);
animator.SetBool(AnimatorParams.Walk, true);
}
}
})
.OnExit(s =>
{
agent.isStopped = true;
animator.SetBool(AnimatorParams.Idle, false);
animator.SetBool(AnimatorParams.Walk, false);
})
.Complete();
//进入第一个状态
machine.Switch2Next();
}
巡逻状态下,当Player进入到5米检测范围内时,进入寻路状态:
//当Player进入5米范围内时 Enemy进入寻路状态
SwitchWhen(() => Vector3.Distance(player.position, transform.position) <= 5f, "寻路状态")
通过Handles
类中的DrawWireArc
方法将该范围绘制出来,方便调试:
Handles.color = Color.red;
Handles.DrawWireArc(transform.position, transform.up, transform.right, 360f, 5f);
如图所示,红色圈范围即为检测范围:
寻路状态表示已经检测到Player,追击Player,不断寻路到Player前,设置Agent
的Stop Distance
属性为1.5,该寻路过程中的移动速度比巡逻状态时要快,因此调整Speed
属性为2,当距离Player大于10时,重新回到巡逻状态,不再追击。
.Build<State>("寻路状态")
.OnEnter(s =>
{
agent.isStopped = false;
//StopDistance设为1
agent.stoppingDistance = 1.5f;
//加速移动
agent.speed = 2f;
//设置动画参数 进入Run
animator.SetBool(AnimatorParams.Run, true);
})
.OnStay(s =>
{
//未到达Player前指定距离时 不断寻路
if (Vector3.Distance(transform.position, player.position) > 1.5f)
{
agent.SetDestination(player.position);
}
else
{
//到达Player前指定距离 进入攻击状态
s.machine.Switch("攻击状态");
}
})
.OnExit(s =>
{
animator.SetBool(AnimatorParams.Run, false);
})
//距离Player大于指定值时 重回巡逻状态
.SwitchWhen(() => Vector3.Distance(transform.position, player.position) > 10f, "巡逻状态")
.Complete()
同样使用Handles
类中的DrawWireArc
方法绘制出追击范围:
private void OnDrawGizmos()
{
for (int i = 0; i < patrolPoints.Length; i++)
{
Handles.PositionHandle(patrolPoints[i].position, Quaternion.identity);
Handles.Label(patrolPoints[i].position, string.Format("Patrol Point {0}", i + 1));
}
Handles.color = Color.red;
Handles.DrawWireArc(transform.position, transform.up, transform.right, 360f, 5f);
Handles.color = Color.cyan;
Handles.DrawWireArc(transform.position, transform.up, transform.right, 360f, 10f);
}
如图所示,青色圈范围即为追击范围:
定义攻击状态:
private class AttackState : State
{
//攻击CD
public float attackCD = 2f;
}
构建攻击状态:
.Build<AttackState>("攻击状态")
.OnEnter(s => agent.isStopped = true)
.OnStay(s =>
{
//朝向Player
transform.rotation = Quaternion.LookRotation(player.position - transform.position);
//Attack Action
if (s.attackCD == 2f) animator.SetInteger(AnimatorParams.Action, 1);
//攻击CD
else
{
s.attackCD -= Time.deltaTime;
if (s.attackCD <= 0f) s.attackCD = 2f;
}
})
.OnExit(s => animator.SetInteger(AnimatorParams.Action, 0))
.SwitchWhen(() => Vector3.Distance(transform.position, player.position) >= 2f, "寻路状态")
.Complete();
这里使用一个Wolf的模型当做Player:
Player进入巡逻检测范围:
Player离开追击范围:
using UnityEngine;
using UnityEngine.AI;
using SK.Framework.FSM;
#if UNITY_EDITOR
using UnityEditor;
#endif
///
/// 敌人单位
///
public class EnemyUnit : MonoBehaviour
{
//Player位置
[SerializeField] private Transform player;
//寻路代理
[SerializeField] private NavMeshAgent agent;
//动画组件
[SerializeField] private Animator animator;
//巡逻点集合
[SerializeField] private Transform[] patrolPoints;
private class PatrolState : State
{
//当前巡逻点的索引值
public int index;
//休息计时
public float timer;
}
private class AttackState : State
{
public float attackCD = 2f;
}
private class AnimatorParams
{
public static readonly int Idle = Animator.StringToHash("Idle");
public static readonly int Walk = Animator.StringToHash("Walk");
public static readonly int Run = Animator.StringToHash("Run");
public static readonly int Action = Animator.StringToHash("Action");
}
private void Start()
{
var machine = StateMachine.Create("Enemy AI")
.Build<PatrolState>("巡逻状态")
.OnEnter(s =>
{
agent.isStopped = false;
//StopDistance设为0
agent.stoppingDistance = 0f;
//设置速度
agent.speed = 1f;
//进入巡逻状态时 设置第一个巡逻点
s.index = 0;
agent.SetDestination(patrolPoints[s.index].position);
//设置动画参数 进入Walk
animator.SetBool(AnimatorParams.Idle, false);
animator.SetBool(AnimatorParams.Walk, true);
})
.OnStay(s =>
{
//判断是否到达目标巡逻点
if (Vector3.Distance(transform.position, patrolPoints[s.index].position) <= .1f)
{
//设置动画参数 进入Idle
animator.SetBool(AnimatorParams.Walk, false);
animator.SetBool(AnimatorParams.Idle, true);
//到达后随机休息若干秒
s.timer += Time.deltaTime;
if (s.timer >= Random.Range(3f, 5f))
{
//重置计时器
s.timer = 0f;
//设置下一个巡逻点
s.index++;
s.index = s.index == patrolPoints.Length ? 0 : s.index;
agent.SetDestination(patrolPoints[s.index].position);
//设置动画参数 进入Walk
animator.SetBool(AnimatorParams.Idle, false);
animator.SetBool(AnimatorParams.Walk, true);
}
}
})
.OnExit(s =>
{
agent.isStopped = true;
animator.SetBool(AnimatorParams.Idle, false);
animator.SetBool(AnimatorParams.Walk, false);
})
//当Player进入5米范围内时 Enemy进入寻路状态
.SwitchWhen(() => Vector3.Distance(player.position, transform.position) <= 5f, "寻路状态")
.Complete()
.Build<State>("寻路状态")
.OnEnter(s =>
{
agent.isStopped = false;
//StopDistance设为1
agent.stoppingDistance = 1.5f;
//加速移动
agent.speed = 2f;
//设置动画参数 进入Run
animator.SetBool(AnimatorParams.Run, true);
})
.OnStay(s =>
{
//未到达Player前指定距离时 不断寻路
if (Vector3.Distance(transform.position, player.position) > 1.5f)
{
agent.SetDestination(player.position);
}
else
{
//到达Player前指定距离 进入攻击状态
s.machine.Switch("攻击状态");
}
})
.OnExit(s =>
{
animator.SetBool(AnimatorParams.Run, false);
})
//距离Player大于指定值时 重回巡逻状态
.SwitchWhen(() => Vector3.Distance(transform.position, player.position) > 10f, "巡逻状态")
.Complete()
.Build<AttackState>("攻击状态")
.OnEnter(s => agent.isStopped = true)
.OnStay(s =>
{
//朝向Player
transform.rotation = Quaternion.LookRotation(player.position - transform.position);
//Attack Action
if (s.attackCD == 2f) animator.SetInteger(AnimatorParams.Action, 1);
//攻击CD
else
{
s.attackCD -= Time.deltaTime;
if (s.attackCD <= 0f) s.attackCD = 2f;
}
})
.OnExit(s => animator.SetInteger(AnimatorParams.Action, 0))
.SwitchWhen(() => Vector3.Distance(transform.position, player.position) >= 2f, "寻路状态")
.Complete();
//进入第一个状态
machine.Switch2Next();
}
#if UNITY_EDITOR
private void OnDrawGizmos()
{
for (int i = 0; i < patrolPoints.Length; i++)
{
Handles.PositionHandle(patrolPoints[i].position, Quaternion.identity);
Handles.Label(patrolPoints[i].position, string.Format("Patrol Point {0}", i + 1));
}
Handles.color = Color.red;
Handles.DrawWireArc(transform.position, transform.up, transform.right, 360f, 5f);
Handles.color = Color.cyan;
Handles.DrawWireArc(transform.position, transform.up, transform.right, 360f, 10f);
}
#endif
}