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類型修改為枚舉更容易表達對應的含義。