Unity状态机FSM
一:状态机介绍
有限状态机,也称为 FSM(Finite State Machine) ,这些状态是有限的、不重叠的,其在任意时刻都处于有限状态集合中的某一状态。当其获得特定输入时,将从当前状态转换到另一个状态 ,或者仍然保持在当前状态。
状态机的应用领域
--- 玩家动作控制:比如一个玩家动作较多,我们可以使用状态机进行管理
--- UI界面的切换与管理
--- 怪物AI的设计
二:状态机设计实例
我们以案例为基础,设计一个人物有三种状态,在三种状态之间自由切换。
我先把里面的几个类描述一下
StateBase:所有状态基类
StateMachine:状态机类,负责管理所有的状态以及切换
PlayerCtrl:玩家控制类,包含一个状态机以及自身控制逻辑
IdleState,RunState,AttackState三种状态
StateTemplate<T>:泛型类,为了解决人物和怪物都存在状态机的时候,可以指定对应的所有者
2.1 设计状态机基类
一个状态机里面,有很多种状态
,每一种状态都有很多相似的特征,这里我们需要一个状态基类。基类里面包含所有状态的基本信息。
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public abstract class StateBase
{
/// <summary>
/// 每个状态对应不同的ID号
/// </summary>
public int ID { get; private set; }
/// <summary>
/// 状态机
/// </summary>
public StateMachine machine;
/// <summary>
/// Construtor
/// </summary>
/// <param name="id">状态的id号(id号有对应的枚举)</param>
public StateBase(int id)
{
ID = id;
}
//进入状态
public virtual void OnEnter(params object[] args) { }
//状态停留
public virtual void OnStay(params object[] args) { }
//状态退出
public virtual void OnExit(params object[] args) { }
//检查状态
public virtual void OnCheck(params object[] args) { }
}
public class StateTemplate<T> : StateBase
{
/// <summary>
/// 状态的拥有者
/// </summary>
public T m_owner;
/// <summary>
/// Constructor
/// </summary>
/// <param name="id">状态的id号</param>
/// <param name="owner">状态的拥有者</param>
public StateTemplate(int id, T owner) : base(id)
{
m_owner = owner;
}
}
2.2 状态机类
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
/// 状态机
/// </summary>
public class StateMachine
{
/// <summary>
/// 状态缓存
/// </summary>
public Dictionary<int, StateBase> m_StateCache;
/// <summary>
/// 当前状态
/// </summary>
public StateBase m_CurrentState;
/// <summary>
/// 当前状态的前一个状态
/// </summary>
public StateBase m_PreviousState;
#region StateMachine Constructor
/// <summary>
/// Constructor
/// </summary>
/// <param name="beginState">开始状态</param>
public StateMachine(StateBase beginState)
{
m_PreviousState = null;
m_CurrentState = beginState;
m_StateCache = new Dictionary<int, StateBase>();
//注册状态
RegisterState(beginState);
m_CurrentState.OnEnter();
}
#endregion
#region FSMUpdate 状态机监测状态
/// <summary>
/// 状态机监测状态变化
/// </summary>
public void FSMUpdate()
{
if (m_CurrentState != null)
{
m_CurrentState.OnStay();
m_CurrentState.OnCheck();
}
}
#endregion
#region TranslateToState 状态切换
/// <summary>
/// 状态切换
/// </summary>
/// <param name="id">目标状态的id号</param>
/// <param name="args">可变参数</param>
public void TranslateToState(int id, params object[] args)
{
int key_id = id;
if (!m_StateCache.ContainsKey(key_id))
{
Debug.LogError("The key is not Exist");
return;
}
//当前状态退出
m_CurrentState.OnExit();
//保存当前状态为下一个新状态的前一个状态
m_PreviousState = m_CurrentState;
//当前状态更新到下一个新状态
m_CurrentState = m_StateCache[key_id];
//新的状态开始进入
m_CurrentState.OnEnter(args);
}
#endregion
#region RegisterState 注册一个新的状态到缓存中
/// <summary>
/// 注册一个新的状态到缓存中
/// </summary>
/// <param name="aState">新状态</param>
public void RegisterState(StateBase aState)
{
int id = aState.ID;
//状态是否缓存了
if (m_StateCache.ContainsKey(id))
{
Debug.LogError("The State has been added the Cache");
return;
}
//缓存aState状态
m_StateCache.Add(id, aState);
//设置aState状态的状态机对象
aState.machine = this;
}
#endregion
}
2.3 每种状态设计
AttackState代码如下:
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public class AttackState : StateTemplate<PlayerCtrl>
{
public AttackState(int id, PlayerCtrl owner) : base(id, owner) {
}
public override void OnEnter(params object[] args) {
base.OnEnter(args);
Debug.Log("AttackState Enter");
m_owner.GetComponent<MeshRenderer>().material.color = Color.red;
}
public override void OnStay(params object[] args) {
base.OnExit(args);
machine.TranslateToState(3);
}
public override void OnExit(params object[] args) {
base.OnExit(args);
Debug.Log("Attack Exit");
}
public override void OnCheck(params object[] args)
{
base.OnCheck(args);
}
}
IdleState状态代码如下:
using UnityEngine;
using System.Collections;
public class IdleState : StateTemplate<PlayerCtrl>
{
public IdleState(int id, PlayerCtrl owner) : base(id, owner)
{ }
public override void OnEnter(params object[] args)
{
base.OnEnter(args);
Debug.Log("IdleState Enter");
m_owner.GetComponent<MeshRenderer>().material.color = Color.blue;
}
public override void OnStay(params object[] args)
{
base.OnStay(args);
}
public override void OnExit(params object[] args)
{
base.OnExit(args);
Debug.Log("IdleState OnExit");
}
public override void OnCheck(params object[] args)
{
}
}
RunState代码如下:
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public class RunState : StateTemplate<PlayerCtrl>
{
public RunState(int id, PlayerCtrl owner) : base(id, owner)
{ }
public override void OnEnter(params object[] args)
{
base.OnEnter(args);
Debug.Log("RunState Enter");
m_owner.GetComponent<MeshRenderer>().material.color = Color.green;
}
public override void OnStay(params object[] args)
{
base.OnStay(args);
}
public override void OnExit(params object[] args)
{
base.OnExit(args);
Debug.Log("RunState OnExit");
}
public override void OnCheck(params object[] args)
{
}
}
2.4 玩家控制类实现
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public class PlayerCtrl : MonoBehaviour
{
StateMachine m_stateMachine;
// Start is called before the first frame update
void Start()
{
//初始化状态机
m_stateMachine = new StateMachine(new IdleState(1,this));
//注册人物的所有状态
InitState();
}
void InitState() {
m_stateMachine.RegisterState(new RunState(2,this));
m_stateMachine.RegisterState(new AttackState(3, this));
}
void LateUpdate()
{
m_stateMachine.FSMUpdate();
}
// Update is called once per frame
void Update()
{
if (Input.GetKeyDown(KeyCode.A))
{
m_stateMachine.TranslateToState(2);
}
if (Input.GetKeyDown(KeyCode.B))
{
m_stateMachine.TranslateToState(3);
}
if (Input.GetKeyDown(KeyCode.C))
{
m_stateMachine.TranslateToState(1);
}
}
}
将该脚本挂载在立方体身上进行测试,按键盘上的ABC可以实现红绿蓝三种状态之间切换。
2.5 总结变化
(1)当把颜色变化,切换为Animator变化。其实本质上是一样的,在进入的时候播放对应的动画即可。在OnCheck的时候检查动画是否播放完毕,进行对应的状态切换。记住:在状态内部也是可以调用TranslateToState的,因为本身每个状态里面包含了对应的状态机
(2)如果是满足每个状态2s时间,进行切换怎么办?其实也是类似的,在OnCheck函数内部不停检测时间即可,根据自己具体的逻辑实现即可。
(3)不管状态机写法怎么变化,本质思想是一样的。
(4)将状态的ID从int类型修改为枚举更容易表达对应的含义。