# Unity進階開發-FSM有限狀態機 前言 我們在進行開發時,到了一定程度上,會遇到數十種狀態,繼續使用Unity的Animator控制器會出現大量的bool,float類型的變數,而這些錯綜複雜的變數與Animatator控制器如同迷宮版連線相結合會變得極其的複雜且無法良好維護擴展,出現一個B ...
# Unity進階開發-FSM有限狀態機
前言
我們在進行開發時,到了一定程度上,會遇到數十種狀態,繼續使用Unity的Animator控制器會出現大量的bool,float類型的變數,而這些錯綜複雜的變數與Animatator控制器如同迷宮版連線相結合會變得極其的複雜且無法良好維護擴展,出現一個BUG會導致開發過程中開發者承受極大的精神力,而這時候,使用有限狀態機或者AI行為樹便成為了一個極佳的選擇,本文只記錄了有限狀態機的開發
使用有限狀態機進行狀態管理與切換可以大幅度的減少開發時候的難度,在開發過程中只需要關註各個狀態間的切換即可
圖示FSM工作過程:
我們可以看到,FSM的腳本一共分為兩大塊兒,一塊兒繼承自IState,這裡面保存著狀態內執行的行為,例如進入狀態,離開狀態,邏輯切換(用於判斷是否切換到其他狀態),PlayerState實現這個介面,並繼承ScriptableObject類,看到ScriptableObject我們就知道了PlayerState的作用了,ScriptableObject類的對象不依賴場景中的對象可,是獨立存在的數據文件。而在這裡,PlayState保存的是狀態的行為邏輯函數,這些行為邏輯函數的本質作用就是滿足條件就去執行相關行為。當然,在FSM中用一個例子來說明,當玩家處於跑步狀態時,PlayState文件發現滿足執行跑步行為的邏輯,就需要去播放跑步動畫。我們知道,ScriptableObject的數據是不會自己變化的,那麼所有判斷邏輯只能使用一遍,我們該如何解決這個問題,這就需要FSM第二的板塊兒的類來實現了
我們不難發現,StateMachine是繼承MonoBehaviour的,這就需要掛載到場景中了,並且也可以使用Update函數來時刻改變檢測到的信息,那麼,上文已經說了,PlayerState的數據不能主動變化,而StateMachine的數據可以進行變化,所以我們就可以通過一個函數來講StateMachine中檢測到數據來發給PlayerState,以此來實現PlayerState創建的數據文件可以連續進行邏輯判斷
好了,現在開始上代碼,用代碼繼續說
No.1 IState
public interface IState
{
//進入狀態
void Enter() { }
//退出狀態
void Exit() { }
//狀態邏輯更新
void LogicUpdate() { }
}
在這裡就是狀態內部需要執行的行為函數
No.2 StateMachine
// <summary>
/// 持有所有狀態類,並對他們進行管理和切換
/// 負責當前狀態的更新
/// </summary>
public class StateMachine : MonoBehaviour
{
IState currentState; //當前狀態
public Dictionary<System.Type, IState> stateTable; //字典,用於保存狀態以及查詢狀態,方便狀態切換時 //進行查找
public void Update()
{
currentState.LogicUpdate(); //執行每個狀態的邏輯切換函數,可以使狀態實現檢測 //變換,類似於Update內的函數實時接收信息
}
//切換狀態
protected void SwitchOn(IState newState)
{
//當前狀態變為新狀態
currentState = newState;
//進入新狀態
currentState.Enter();
}
//
public void SwitchState(IState newState)
{
//退出狀態
currentState.Exit();
//進入新狀態
SwitchOn(newState);
}
//切換狀態函數重載
public void SwitchState(System.Type newStateType)
{
SwitchState(stateTable[newStateType]); //將字典內的狀態傳入
}
}
No.3 PlayetState
public class PlayerState : ScriptableObject, IState
{
/*************物理檢測*************/
protected bool isGround; //是否在地面
/*************基礎信息*************/
protected bool isRun; //是否跑步
protected bool isJump; //是否跳躍
protected bool isIdle; //是否靜止
/*************相關組件*************/
protected Rigidbody2D my_Body2D; //剛體組件,用於獲取物體剛體屬性
protected Animator animator; //動畫組件,用來播放動畫
protected PlayerStateMachine stateMachine; //PlayerStateMachine,玩家狀態機類,執行狀態間的切換
public void Initiatize(Animator animator, Rigidbody2D my_Body2D, PlayerStateMachine stateMachine)
{//獲取PlayerStateMachine傳遞進來的 動畫,剛體,狀態機類
this.animator = animator;
this.my_Body2D = my_Body2D;
this.stateMachine = stateMachine;
}
public void PhysicalDetection(bool isGround)
{
this.isGround = isGround;
}
/// <summary>
/// 狀態信息傳遞
/// </summary>
public void BasicInformation(bool isRun,bool isJump,bool isIdle)
{
this.isRun = isRun;
this.isJump = isJump;
this.isIdle = isIdle;
}
//進入狀態
public virtual void Enter() { }
//離開狀態
public virtual void Exit() { }
//邏輯切換
public virtual void LogicUpdate() { }
}
No.4 PlayerStateMachine
/// <summary>
/// 玩家狀態機類
/// </summary>
public class PlayerStateMachine : StateMachine
{
/*************************檢測信息***************************/
PlayerPhysicalDetection playerPhysicalDetection; //物理檢測組件,這個是繼承 //MonoBehaviour,掛載到玩家身上來檢測 //玩家的物理信息,例如是否位於地面
/*************************狀態信息***************************/
//PlayerState資源文件
[SerializeField] PlayerState[] states;
Animator animator; //獲取動畫組件
Rigidbody2D my_Body2D; //獲取剛體組件
void Awake()
{
/*************************物理檢測信息***************************/
playerPhysicalDetection=GetComponent<PlayerPhysicalDetection>(); //獲取物理檢測組件
/*************************狀態信息組件***************************/
stateTable = new Dictionary<System.Type, IState>(states.Length); //初始化字典
animator = GetComponent<Animator>(); //獲取動畫組件
my_Body2D = GetComponent<Rigidbody2D>(); //獲取剛體組件
//迭代器迴圈獲取狀態
foreach (PlayerState state in states)
{
state.Initiatize(animator, my_Body2D, this);//將動畫組件,剛體組件以及PlayerStateMachine傳入進去
//狀態存入字典
stateTable.Add(state.GetType(), state);
}
}
private void Start()
{//在開始時執行Idle,進入Idle狀態
SwitchOn(stateTable[typeof(PlayerState_Idle)]);
}
private new void Update()
{
base.Update();//執行父類StateMachine的Update函數
foreach (PlayerState state in states)
{//將檢測信息傳入進去
state.PhysicalDetection(playerPhysicalDetection.isGround);
state.BasicInformation( isRun, isJump, isIdle);
}
}
}
No.5 PlayerState_Idle
[CreateAssetMenu(menuName = "StateMachine/PlayerState/Idle", fileName = "PlayerState_Idle")]//創建文件
public class PlayerState_Idle : PlayerState
{
/*****************物理檢測*******************/
public override void Enter()
{
//執行該狀態數據文件,首先執行進入狀態函數,在進入狀態函數執行相關的行為
//進入狀態,預設播放Idle動畫
animator.Play("PlayerIdle");
}
//邏輯切換函數,當檢測到處於某種狀態,立刻執行該狀態的數據文件
public override void LogicUpdate()
{
if(isRun)
{
stateMachine.SwitchState(stateMachine.stateTable[typeof(PlayerState_Run)]);
}
if(isJump)
{
stateMachine.SwitchState(stateMachine.stateTable[typeof(PlayerState_Jump)]);
}
if(my_Body2D.velocity.y<0&&!isGround)
{
stateMachine.SwitchState(stateMachine.stateTable[typeof(PlayerState_Fall)]);
}
}
}
No.6 PlayerState_Jump
[CreateAssetMenu(menuName = "StateMachine/PlayerState/Jump", fileName = "PlayerState_Jump")]//創建文件
public class PlayerState_Jump : PlayerState
{
/*****************物理檢測*******************/
public override void Enter()
{
//執行該狀態數據文件,首先執行進入狀態函數,在進入狀態函數執行相關的行為
//進入狀態,預設播放Idle動畫
animator.Play("PlayerJump");
}
//邏輯切換函數,當檢測到處於某種狀態,立刻執行該狀態的數據文件
//該腳本中,跳躍後無法執行其它行為,若有需要可以添加判斷
public override void LogicUpdate()
{
if(my_Body2D.velocity.y<0&&!isGround)
{
stateMachine.SwitchState(stateMachine.stateTable[typeof(PlayerState_Fall)]);
}
}
}
No.7 PlayerState_Fall
[CreateAssetMenu(menuName = "StateMachine/PlayerState/Fall", fileName = "PlayerState_Fall")]//創建文件
public class PlayerState_Jump : PlayerState
{
/*****************物理檢測*******************/
public override void Enter()
{
//執行該狀態數據文件,首先執行進入狀態函數,在進入狀態函數執行相關的行為
//進入狀態,預設播放Fall動畫
animator.Play("PlayerFall");
}
//邏輯切換函數,當檢測到處於某種狀態,立刻執行該狀態的數據文件
//該腳本中,掉落時無法執行其它行為,若有需要可以添加判斷
public override void LogicUpdate()
{
if(isGround)
{//落地進入Idle狀態
stateMachine.SwitchState(stateMachine.stateTable[typeof(PlayerState_Idle)]);
}
}
}
No.7 PlayerState_Run
[CreateAssetMenu(menuName = "StateMachine/PlayerState/Run", fileName = "PlayerState_Run")]//創建文件
public class PlayerState_Idle : PlayerState
{
/*****************物理檢測*******************/
public override void Enter()
{
//執行該狀態數據文件,首先執行進入狀態函數,在進入狀態函數執行相關的行為
//進入狀態,預設播放Run動畫
animator.Play("PlayerRun");
}
//邏輯切換函數,當檢測到處於某種狀態,立刻執行該狀態的數據文件
public override void LogicUpdate()
{
if(isIdle)
{
stateMachine.SwitchState(stateMachine.stateTable[typeof(PlayerState_Idle)]);
}
if(isJump)
{
stateMachine.SwitchState(stateMachine.stateTable[typeof(PlayerState_Jump)]);
}
if(my_Body2D.velocity.y<0&&!isGround)
{
stateMachine.SwitchState(stateMachine.stateTable[typeof(PlayerState_Fall)]);
}
}
}
以上便是一個簡單的狀態機
總結與拓展延伸
FSM可以大幅減少玩家動畫間的判斷和切換,只需要關註一個狀態到另一個狀態的切換條件,滿足條件就切換,不滿足就繼續執行
FSM在玩家上的使用並不明顯,因為在這上面,進入某種狀態後,我們只播放了動畫,並沒有執行其他行為。當我們用在怪物AI中會更加的方便,例如怪物處於巡邏狀態,播放巡邏動畫,執行巡邏的腳本文件(建立一個類,專門用來存怪物AI個狀態行為函數,通過傳遞的方式將該類對象傳遞給EnemyState,由EnemyState進行操作),怪物處於追擊玩家狀態,播放追擊動畫,並執行追擊玩家的行為函數,在怪物AI中,FSM的應用會更加輕鬆的管理怪物,怪物可以放技能了,就進入技能狀態,發現玩家就進入追擊玩家,一個條件一個行為,更加有利於FSM進行管理