有限狀态機用法教程
本文提供全圖文流程,中文翻譯。
Chinar 堅持将簡單的生活方式,帶給世人!
(擁有更好的閱讀體驗 —— 高分辨率使用者請根據需求調整網頁縮放比例)
Chinar —— 心分享、心創新!
助力快速了解 FSM 有限狀态機,完成遊戲狀态的切換
給新手節省寶貴的時間,避免采坑!
Chinar 教程效果:

全文高清圖檔,點選即可放大觀看 (很多人竟然不知道)
1
Finite-state machine —— 有限狀态機
有限狀态機簡稱: FSM —— 簡稱狀态機
是表示有限個狀态以及在這些狀态之間的轉移和動作等行為的數學模型
其他的不多說,于我們開發者來說,狀态機能通過全局來管理我們的遊戲狀态/人物狀态
使我們的工程邏輯清晰,将遊戲/項目各個狀态的轉換,交由狀态機統一管理
極大的避免了當狀态過多 / 轉換狀态過多時,每次都需要調用相應函數來完成轉換的麻煩
對于初學者來講,套用狀态機來對狀态進行管理,可能認為過于麻煩
其實不用怕,那隻是因為不熟悉用法和邏輯流程導緻的
熟練的運用狀态機來管理我們的項目狀态,是很有必要的
那會使後期,我們的工程非常便于維護
2
Foreword —— 前言()
網上的大神們為了更全面的闡述狀态機的具體工作方式,他們有些說的極為詳細
但對于初學者來講,直接看這樣的圖解,教程,多數都是一臉懵逼
例如:(圖檔引用自網絡大神部落格)
衆所周知 Chinar 講的這些大神不同
Chinar 會通過一些簡單的例子,來帶領初學者了解并學會如何使用狀态機來管理我們的工程.
師傅領進門,修行靠個人 ,一切都需要先入門後,自己再慢慢擴充,不然一切都是扯淡
3
Example —— 示例
腳本引用自 Wiki.unity3d ——
源碼連結這裡 Chinar 用一個簡單的遊戲狀态切換邏輯來說明狀态機用法
MVC 設計模式
FSM 一共2個類,不需要挂載到遊戲對象上
FSMState 狀态父類,所有子類狀态都繼承與這個類
例如以下工程:我們要需要2個狀态: 菜單狀态 與 遊戲狀态
那麼這兩個類MenuState 和 GameState都需要繼承自 FSMState
狀态機腳本
using UnityEngine;
using System.Collections.Generic;
using UnityEngine.Experimental.PlayerLoop;
/// <summary>
/// Place the labels for the Transitions in this enum. —— 在此枚舉中放置轉換的标簽。
/// Don't change the first label, NullTransition as FSMSystem class uses it. —— 不要改變第一個标簽:NullTransition,因為FSMSystem類使用它。
/// </summary>
public enum Transition
{
NullTransition = 0, // Use this transition to represent a non-existing transition in your system —— 使用此轉換表示系統中不存在的轉換
Game, //轉到遊戲
Menu //轉到菜單
}
/// <summary>
/// Place the labels for the States in this enum. —— 在此枚舉中放置狀态的标簽。
/// Don't change the first label, NullStateID as FSMSystem class uses it.不要改變第一個标簽:NullStateID,因為FSMSystem類使用它。
/// </summary>
public enum StateID
{
NullStateId = 0, // Use this ID to represent a non-existing State in your system —— 使用此ID表示系統中不存在的狀态
Menu, //菜單
Game //遊戲
}
/// <summary>
/// This class represents the States in the Finite State System.該類表示有限狀态系統中的狀态。
/// Each state has a Dictionary with pairs (transition-state) showing 每個狀态都有一個對顯示(轉換狀态)的字典
/// which state the FSM should be if a transition is fired while this state is the current state.如果在此狀态為目前狀态時觸發轉換,則FSM應處于那種狀态。
/// Method Reason is used to determine which transition should be fired .方法原因用于确定應觸發哪個轉換。
/// Method Act has the code to perform the actions the NPC is supposed do if it's on this state.方法具有執行NPC動作的代碼應該在這種狀态下執行。
/// </summary>
public abstract class FSMState : MonoBehaviour
{
public Dictionary<Transition, StateID> map = new Dictionary<Transition, StateID>(); //字典 《轉換,狀态ID》
protected StateID stateID; //私有ID
public StateID ID //狀态ID
{
get { return stateID; }
}
protected GameManager manager; //保證子類狀态可以通路到總控 GameManager
public GameManager Manager
{
set { manager = value; }
}
/// <summary>
/// 添加轉換
/// </summary>
/// <param name="trans">轉換狀态</param>
/// <param name="id">轉換ID</param>
public void AddTransition(Transition trans, StateID id)
{
if (trans == Transition.NullTransition) // Check if anyone of the args is invalid —— //檢查是否有參數無效
{
Debug.LogError("FSMState ERROR: NullTransition is not allowed for a real transition");
return;
}
if (id == StateID.NullStateId)
{
Debug.LogError("FSMState ERROR: NullStateID is not allowed for a real ID");
return;
}
if (map.ContainsKey(trans)) // Since this is a Deterministic FSM,check if the current transition was already inside the map —— 因為這是一個确定性FSM,檢查目前的轉換是否已經在字典中
{
Debug.LogError("FSMState ERROR: State " + stateID.ToString() + " already has transition " + trans.ToString() +
"Impossible to assign to another state");
return;
}
map.Add(trans, id);
}
/// <summary>
/// This method deletes a pair transition-state from this state's map. —— 該方法從狀态映射中删除一對轉換狀态。
/// If the transition was not inside the state's map, an ERROR message is printed. —— 如果轉換不在狀态映射内,則會列印一條錯誤消息。
/// </summary>
public void DeleteTransition(Transition trans)
{
if (trans == Transition.NullTransition) // Check for NullTransition —— 檢查狀态是否為空
{
Debug.LogError("FSMState ERROR: NullTransition is not allowed");
return;
}
if (map.ContainsKey(trans)) // Check if the pair is inside the map before deleting —— 在删除之前,檢查這一對是否在字典中
{
map.Remove(trans);
return;
}
Debug.LogError("FSMState ERROR: Transition " + trans.ToString() + " passed to " + stateID.ToString() +
" was not on the state's transition list");
}
/// <summary>
/// This method returns the new state the FSM should be if this state receives a transition and—— 如果該狀态接收到轉換,該方法傳回FSM應該為新狀态
/// 得到輸出狀态
/// </summary>
public StateID GetOutputState(Transition trans)
{
if (map.ContainsKey(trans)) // Check if the map has this transition —— 檢查字典中是否有這個狀态
{
return map[trans];
}
return StateID.NullStateId;
}
/// <summary>
/// This method is used to set up the State condition before entering it. —— 該方法用于在進入狀态條件之前設定狀态條件。
/// It is called automatically by the FSMSystem class before assigning it to the current state.—— 在配置設定它之前,FSMSystem類會自動調用它到目前狀态
/// </summary>
public virtual void DoBeforeEntering()
{
}
/// <summary>
/// 此方法用于在FSMSystem更改為另一個變量之前進行任何必要的修改。在切換到新狀态之前,FSMSystem會自動調用它。
/// This method is used to make anything necessary, as reseting variables
/// before the FSMSystem changes to another one. It is called automatically
/// by the FSMSystem before changing to a new state.
/// </summary>
public virtual void DoBeforeLeaving()
{
}
/// <summary>
/// 這個方法決定狀态是否應該轉換到它清單上的另一個NPC是對這個類控制的對象的引用
/// This method decides if the state should transition to another on its list
/// NPC is a reference to the object that is controlled by this class
/// </summary>
public virtual void Reason()
{
}
/// <summary>
/// 這種方法控制了NPC在遊戲世界中的行為。
/// NPC做的每一個動作、動作或交流都應該放在這裡
/// NPC是這個類控制的對象的引用
/// This method controls the behavior of the NPC in the game World.
/// Every action, movement or communication the NPC does should be placed here
/// NPC is a reference to the object that is controlled by this class
/// </summary>
public virtual void Act()
{
}
}
/// <summary>
/// FSMSystem class represents the Finite State Machine class.FSMSystem類表示有限狀态機類。
/// It has a List with the States the NPC has and methods to add, 它句有一個狀态清單,NPC有添加、删除狀态和更改機器目前狀态的方法。
/// delete a state, and to change the current state the Machine is on.
/// </summary>
public class FSMSystem
{
private List<FSMState> states; //狀态集
// The only way one can change the state of the FSM is by performing a transition 改變FSM狀态的唯一方法是進行轉換
// Don't change the CurrentState directly 不要直接改變目前狀态
private StateID currentStateID;
public StateID CurrentStateID
{
get { return currentStateID; }
}
private FSMState currentState;
public FSMState CurrentState
{
get { return currentState; }
}
/// <summary>
/// 預設構造函數
/// </summary>
public FSMSystem()
{
states = new List<FSMState>();
}
/// <summary>
/// 設定目前狀态
/// </summary>
/// <param name="state">初始狀态</param>
public void SetCurrentState(FSMState state)
{
currentState = state;
currentStateID = state.ID;
state.DoBeforeEntering(); //開始前狀态切換
}
/// <summary>
/// This method places new states inside the FSM, —— 這個方法在FSM内部放置一個放置一個新狀态
/// or prints an ERROR message if the state was already inside the List. —— 或者,如果狀态已經在清單中,則列印錯誤消息。
/// First state added is also the initial state. 第一個添加的狀态也是初始狀态。
/// </summary>
public void AddState(FSMState fsmState, GameManager manager)
{
// Check for Null reference before deleting 删除前判空
if (fsmState == null)
{
Debug.LogError("FSM ERROR: Null reference is not allowed");
}
else // First State inserted is also the Initial state, —— 插入的第一個狀态也是初始狀态,// the state the machine is in when the simulation begins —— 狀态機是在模拟開始時
{
fsmState.Manager = manager; //給每個狀态添加總控 GameManager
if (states.Count == 0)
{
states.Add(fsmState);
return;
}
foreach (FSMState state in states) // Add the state to the List if it's not inside it 如果狀态不在清單中,則将其添加到清單中 (添加狀态ID)
{
if (state.ID == fsmState.ID)
{
Debug.LogError("FSM ERROR: Impossible to add state " + fsmState.ID.ToString() +
" because state has already been added");
return;
}
}
states.Add(fsmState);
}
}
/// <summary>
/// This method delete a state from the FSM List if it exists, —— 這個方法從FSM清單中删除一個存在的狀态,
/// or prints an ERROR message if the state was not on the List. —— 或者,如果狀态不存在,則列印錯誤資訊
/// </summary>
public void DeleteState(StateID id)
{
if (id == StateID.NullStateId) // Check for NullState before deleting —— 判空
{
Debug.LogError("FSM ERROR: NullStateID is not allowed for a real state");
return;
}
foreach (FSMState state in states) // Search the List and delete the state if it's inside it 搜尋清單并删除其中的狀态
{
if (state.ID == id)
{
states.Remove(state);
return;
}
}
Debug.LogError("FSM ERROR: Impossible to delete state " + id.ToString() +
". It was not on the list of states");
}
/// <summary>
/// This method tries to change the state the FSM is in based on
/// the current state and the transition passed. If current state
/// doesn't have a target state for the transition passed,
/// an ERROR message is printed.
/// 該方法嘗試根據目前狀态和已認證的轉換改變FSM所處的狀态。如果目前狀态沒有傳遞的轉換的目标狀态,則輸出錯誤消息。
/// </summary>
public void PerformTransition(Transition trans)
{
if (trans == Transition.NullTransition) // Check for NullTransition before changing the current state 在更改目前狀态之前檢查是否有NullTransition
{
Debug.LogError("FSM ERROR: NullTransition is not allowed for a real transition");
return;
}
StateID id = currentState.GetOutputState(trans); // Check if the currentState has the transition passed as argument 檢查currentState是否将轉換作為參數傳遞
if (id == StateID.NullStateId)
{
Debug.LogError("FSM ERROR: State " + currentStateID.ToString() + " does not have a target state " +
" for transition " + trans.ToString());
return;
}
currentStateID = id; // Update the currentStateID and currentState 更新目前狀态和ID
foreach (FSMState state in states)
{
if (state.ID == currentStateID)
{
currentState.DoBeforeLeaving(); // Do the post processing of the state before setting the new one 在設定新狀态之前是否對狀态進行後處理
currentState = state;
currentState.DoBeforeEntering(); // Reset the state to its desired condition before it can reason or act 在它推動和動作之前,重置狀态到它所需的條件
break;
}
}
}
}
4
Moltimode —— 多狀态
菜單狀态腳本:MenuState
遊戲狀态腳本:GameState
我們來控制這兩個狀态,交由狀态機進行切換
菜單腳本
/// <summary>
/// 菜單狀态
/// </summary>
public class MenuState : FSMState
{
void Awake()
{
stateID = StateID.Menu;
AddTransition(Transition.Game, StateID.Game); //(菜單狀态下:需要轉遊戲)→→添加轉換,轉換遊戲 —— 對應遊戲狀态
//map.Add(Transition.Game, StateID.Game);//上邊也可這麼寫
}
void Start()
{
manager.View.StartButton.onClick.AddListener(OnStarGameClick);
}
/// <summary>
/// 開始遊戲
/// </summary>
public void OnStarGameClick()
{
manager.Fsm.PerformTransition(Transition.Game);
}
/// <summary>
/// 進入該狀态時
/// </summary>
public override void DoBeforeEntering()
{
manager.View.ShowMenuUi();
}
/// <summary>
/// 離開該狀态時
/// </summary>
public override void DoBeforeLeaving()
{
manager.View.HideMenuUi();
}
}
遊戲腳本
/// <summary>
/// 遊戲狀态
/// </summary>
public class GameState : FSMState
{
void Awake()
{
stateID = StateID.Game;
AddTransition(Transition.Menu, StateID.Menu); //(遊戲狀态下:點選暫停需要轉菜單)→→添加轉換,轉換菜單—— 對應菜單狀态
//map.Add(Transition.Menu, StateID.Menu);//上邊也可這麼寫
}
void Start()
{
manager.View.PauseButton.onClick.AddListener(OnPauseButton);
}
/// <summary>
/// 暫停
/// </summary>
public void OnPauseButton()
{
manager.Fsm.PerformTransition(Transition.Menu);
}
/// <summary>
/// 進入該狀态時
/// </summary>
public override void DoBeforeEntering()
{
manager.View.ShowGameUi();
}
/// <summary>
/// 離開該狀态時
/// </summary>
public override void DoBeforeLeaving()
{
manager.View.HideGameUi();
}
}
5
GameManager ——遊戲總控腳本
遊戲總控腳本:GameManager —— 用來控制全局遊戲邏輯 (C)
我們在這個腳本中,将所有狀态批量添加到狀态機中
這裡我通過修改,傳入了 GameManager 到所有狀态中
這樣我們後期可以在各個狀态中完成對 GameManager中函數的調用,同時節省了代碼,邏輯也非常清晰
遊戲總控腳本
using UnityEngine;
/// <summary>
/// 遊戲總控腳本
/// </summary>
public class GameManager : MonoBehaviour
{
public FSMSystem Fsm; //有限狀态機系統對象
public View View; // 顯示層
private void Awake()
{
View = GameObject.FindGameObjectWithTag("View").GetComponent<View>(); //這裡要給 View 遊戲對象設定标簽 "View"
//添加所有狀态到狀态集(這裡,我也通過修改,将 GameManager傳到所有狀态中,簡化代碼,便于調用)
Fsm = new FSMSystem(); //調用構造函數,内部會自動初始化 狀态集
FSMState[] states = GetComponentsInChildren<FSMState>(); //找到所有 狀态
foreach (FSMState state in states)
{
Fsm.AddState(state, this); //将狀态,逐個添加到 狀态機中
}
MenuState menuState = GetComponentInChildren<MenuState>();
Fsm.SetCurrentState(menuState); //預設狀态是 菜單狀态
}
}
6
View —— 視圖腳本
用 View 腳本來對我們所有 UI 元素進行指派與管理
項目中引用了 DoTween 插件,來完成對UI簡單動畫的控制
視圖腳本
using DG.Tweening;
using UnityEngine;
using UnityEngine.UI;
/// <summary>
/// 視圖腳本 —— 管理UI元素
/// </summary>
public class View : MonoBehaviour
{
private RectTransform menuUi; //菜單頁
private RectTransform gameUi; //遊戲頁
public Button StartButton; //開始按鈕
public Button PauseButton; //暫停按鈕
public Ease PubEase;
/// <summary>
/// 初始化函數
/// </summary>
void Awake()
{
menuUi = (RectTransform) Find("Menu Ui");
gameUi = (RectTransform) Find("Game Ui");
StartButton = Find("Menu Ui/Menu Button").GetComponent<Button>();
PauseButton = Find("Game Ui/Pause Button").GetComponent<Button>();
}
/// <summary>
/// 顯示菜單頁
/// </summary>
public void ShowMenuUi()
{
menuUi.DOScale(new Vector3(0.3f, 0.3f, 0.3f), 0.1f).OnComplete(() =>
{
menuUi.DOScale(Vector3.one, 0.3f);
StartButton.enabled = true;
}).SetEase(PubEase);
menuUi.DOAnchorPos(Vector2.zero, 0.3f).SetEase(PubEase);
}
/// <summary>
/// 隐藏菜單頁
/// </summary>
public void HideMenuUi()
{
menuUi.DOScale(new Vector3(0.3f, 0.3f, 0.3f), 0.1f).OnComplete(() =>
{
menuUi.DOAnchorPos(new Vector2(-600, -450), 0.3f);
menuUi.DOScale(Vector3.zero, 0.3f).OnComplete(() => { StartButton.enabled = false; }).SetEase(PubEase);
}).SetEase(PubEase);
}
/// <summary>
/// 顯示遊戲頁
/// </summary>
public void ShowGameUi()
{
gameUi.DOScale(new Vector3(0.3f, 0.3f, 0.3f), 0.1f).OnComplete(() =>
{
gameUi.DOScale(Vector3.one, 0.3f);
PauseButton.enabled = true;
}).SetEase(PubEase);
gameUi.DOAnchorPos(Vector2.zero, 0.3f).SetEase(PubEase);
}
/// <summary>
/// 隐藏遊戲頁
/// </summary>
public void HideGameUi()
{
gameUi.DOScale(new Vector3(0.3f, 0.3f, 0.3f), 0.1f).OnComplete(() =>
{
gameUi.DOAnchorPos(new Vector2(-600, -450), 0.3f);
gameUi.DOScale(Vector3.zero, 0.3f).OnComplete(() => { PauseButton.enabled = false; }).SetEase(PubEase);
}).SetEase(PubEase);
}
/// <summary>
/// 查找對Ui元素完成指派
/// </summary>
/// <param name="uiElement">Ui名查找路徑</param>
Transform Find(string uiElement)
{
return transform.Find("Canvas/" + uiElement);
}
}
7
Final effect —— 最終效果
我們通過狀态機簡單的完成了 開始遊戲 和暫停的狀态切換
代碼中注釋寫的非常詳細了,請初學者認真看下
具體流程就是:
1. GameManager 完成将所有子類狀态添加到狀态集中
2. View 擷取到我們所需要的所有 UI 元素對象,并提供公有方法可供各個狀态通路
3. 做好各個狀态的進入 與離開時機發生時,該執行的事件,交由狀态機去管理!
狀态進入:
DoBeforeEntering()
狀态離開:
DoBeforeLeaving()
4. 例子較為簡單,為了友善初學者了解學習隻寫了2個狀态
根據需求,大家可以舉一反三,多謝幾個狀态練習一下,其實流程很簡單!
8
Project —— 項目檔案
項目檔案為 unitypackage 檔案包:
下載下傳導入 Unity 即可使用
最終效果: (由于GIF錄制 60幀數的限制,是以我點選太快了,看着有些卡似得)

至此:狀态機教程結束
其他教程
May Be —— 搞開發,總有一天要做的事!
擁有自己的伺服器,無需再找攻略! Chinar 提供一站式教程,閉眼式建立! 為新手節省寶貴時間,避免采坑! |
1 ——
雲伺服器包年包月 - 超全教程 (新手必備!)2 ——
阿裡ECS雲伺服器自定義配置 - 購買教程(新手必備!)3——
Windows 伺服器配置、運作、建站一條龍 !4 ——
Linux 伺服器配置、運作、建站一條龍 !$ $
技術交流群:806091680 ! Chinar 歡迎你的加入
END
本部落格為非營利性個人原創,除部分有明确署名的作品外,所刊登的所有作品的著作權均為本人所擁有,本人保留所有法定權利。違者必究
對于需要複制、轉載、連結和傳播部落格文章或内容的,請及時和本部落客進行聯系,留言,Email: [email protected]
對于經本部落客明确授權和許可使用文章及内容的,使用時請注明文章或内容出處并注明網址