State pattern 是我使用次數最多的設計模式,在 Unity 遊戲的開發中被我用於程式流程的控制、規則的彈性編輯、簡易 AI 的撰寫、腳色控制器的設計等,是應用相當廣泛的模式。
因為次數太多了,索性事先設計好基本的類別,當需要的時候便可以隨時隨地使用 State pattern,本文便是藉機介紹了 State pattern 及我的通用作法。
State pattern 簡介
聽到 State pattern 大家應該會聯想到有限狀態機 (FSM,finite-state machine),但兩者嚴格來說是不完全相同的,FSM 本身是個設計概念,被應用在諸多領域,實作方式百百種;而 State pattern 只是 FSM 在物件導向中的一個實現方案。
用一句話來形容 State pattern 就是「透過不同狀態的切換,讓類別展現不同的行為」,這句話之中可以看出 State pattern 的兩個面向,分離不同的行為模式以及整合(切換)不同的行為狀態。
使用 State pattern (或狀態機) 的好處是:
- 方便拆分程式碼,讓單一份程式只需專注在他當下的工作。
- 容易改動運作流程 – 只要適當使用,狀態之間的轉移次序是相當彈性的。
- 管理資源容易 – 每個狀態所使用的資源,可以在狀態結束前自行釋放。
而缺點是:
- 不易從程式方面限制使用方式,需要開發者主動遵守狀態機的規格,錯誤使用的狀態機可能會失去上述所有優點。特別在合作開發時更要注意不同人對於狀態機的使用是否達成共識。
- 每多一個狀態都要增加一個類別,可能會有類別數量狂增的情況。
State pattern 實踐方向
要實踐一個 State pattern 的通用作法,就相當於要設計一個簡易的 FSM。
上圖取自 Wiki – Finite-state_machine 條目
在物件導向程式設計中,FSM 沒有一個公認的實作方式,有些比較嚴謹的實作方式會定義轉移路徑,也有些實作方式會暫存所有狀態,而我希望的是一個最簡化的設計,只為了隨時可以使用 State pattern,所以只準備了以下三個設計目標:
- 有一個控制器 (Controller) 負責管理與切換狀態。
- 每個狀態 (State) 都有三個階段 – 進入、運作、離開。
- 控制器 (Controller) 必須被注入狀態 (State) 之中,避免當狀態決定執行切換時無從呼叫控制器。
首先是控制器 StateController
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77
| using UnityEngine; using System.Collections; namespace DouduckGame { public sealed class StateController { private IState m_oCurrentState = null; public IState CurrentState { get { return m_oCurrentState; } } private bool m_bStarted = false; private bool m_bTerminated = false; public StateController() {} public StateController(IState oStartState) : this() { Start(oStartState); } public void Start(IState oState) { if (m_bTerminated) { Debug.LogError("[StateController] has been terminated"); return; } if (m_bStarted) { Debug.LogError("[StateController] has been started"); return; } Debug.Log("[StateController] Start: " + oState.ToString()); m_bStarted = true; m_oCurrentState = oState; m_oCurrentState.SetProperty(this); } public void Terminate() { if (m_oCurrentState != null) { m_oCurrentState.StateEnd(); m_oCurrentState = null; } m_bTerminated = true; } public void TransTo(IState oState) { if (m_bTerminated) { Debug.LogError("[StateController] has been terminated"); return; } if (!m_bStarted) { Debug.LogError("[StateController] need to be started first"); return; } Debug.Log("[StateController] TransTo: " + oState.ToString()); if (m_oCurrentState != null) { m_oCurrentState.StateEnd(); } m_oCurrentState = oState; m_oCurrentState.SetProperty(this); } public void StateUpdate() { if (m_bTerminated || !m_bStarted) { return; } if (m_oCurrentState != null) { if (m_oCurrentState.AtStateBegin) { m_oCurrentState.TouchStateBegin(); m_oCurrentState.StateBegin(); if (m_oCurrentState == null) { return; } } m_oCurrentState.StateUpdate(); } } } }
|
StateController 總共設計了四個 public 方法:
void Start(IState)
– 傳入一個狀態,作為最初的狀態開始狀態機的運作。必須呼叫此方法後,才能使用其他方法。
void Terminate()
– 終止當前狀態,結束狀態機的運作,將不再可以使用其他三個方法。
void TransTo(IState)
– 傳入一個狀態,則切換到該狀態,同時會執行上個狀態的 StateEnd()。
void StateUpdate()
– 更新與執行狀態機的運作,會視需要呼叫當前狀態的 StateBegin(),並固定呼叫當前狀態的 StateUpdate()。
通常會搭配一個 Monobehaviour 腳本封裝 StateController ,然後再由 Monobehaviour.Update() 呼叫 StateUpdate() 來持續更新狀態機。
接著是狀態的介面(abstract class) IState
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39
| using UnityEngine; using System.Collections; namespace DouduckGame { public abstract class IState { private StateController m_StateController; protected StateController Controller { get { return m_StateController; } } private bool m_bAtStateBegin = true; public bool AtStateBegin { get { return m_bAtStateBegin; } } public void SetProperty(StateController oController) { m_StateController = oController; } public void TouchStateBegin() { m_bAtStateBegin = false; } protected void TransTo(IState oState) { m_StateController.TransTo(oState); } public virtual void StateBegin() {} public virtual void StateUpdate() {} public virtual void StateEnd() {} public override string ToString () { return string.Format ("<IState>" + this.GetType().Name); } } }
|
作為狀態的主要功能,共有三個 public virtual 方法:
void StateBegin()
– 狀態的預備動作,會在第一次執行 StateUpdate() 之前執行一次。
void StateUpdate()
– 狀態運作的核心,由 StateController.StateUpdate() 負責呼叫,使用邏輯與 Unity Monobehaviour 的 Update() 相似。
void StateEnd()
– 當狀態即將被取代或結束時會呼叫,通常用於狀態的收尾工作、退訂 (反註冊) 事件系統、釋放相關資源等。
其實狀態的三個 virtual 方法使用邏輯便是對應到 Monobehaviour 的 Start()、Update() 及 OnDestory() 之間的關係,但由於透過 State pattern 進行了拆分,可以讓一個類別或者 Monobehaviour 依照情況扮演不同的腳色與功能。
除了三個主要功能,IState 也被**相依注入(Dependency Injection)**了負責控制的 StateController 本體,所以可以在狀態之中直接控制狀態的切換,不須使用 Singleton 或其他手段由外部取得控制。為了方便也實作了IState.TransTo(IState)
可以直接呼叫。
剩餘未介紹的類別成員與方法,則是為了實作 StateController 而設計的 Flag 以及注入方法,有興趣的人可以再自行閱讀程式碼了解。
階層式的狀態機 Hierarchical Finite State Machine
上圖取自 Wiki – Finite-state_machine 條目
原本為了實現上方巢狀設計的最簡單方法,就是另外放一個 StateController 在 IState 之中,實現父子關係般的狀態機結構。
不過經過測試與修改,我決定不直接採用巢狀的資料結構,而是自行實作 Stack 結構來儲存所有的狀態 (IHierarchicalState),為此而外設計了 HierarchicalStateController,總共兩個全新的類別。
使用 Stack 儲存狀態,在執行時與巢狀結構並無差異,皆是有著 LIFO 的特性,這點可以將狀態機的 UML 圖表重繪成 Tree graph 來輕易看出。而在有相同執行結果的情況下,Stack 儲存可以避開多餘的方法呼叫,避免在每一次 StateUpdate() 時產生遞迴一般的呼叫次序,降低執行上的負擔。
下方是兩個新類別的程式碼,其中在 HierarchicalStateController.TransTo() 中我使用 Level 參數來決定是否深入建立子狀態機,由 0 作為第一層的狀態,每建構一層子狀態機 Level 則加一。剩餘部分則與普通狀態機大同小異。
HierarchicalStateController
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88
| using UnityEngine; using System.Collections; using System.Collections.Generic; namespace DouduckGame { public sealed class HierarchicalStateController { private List<IHierarchicalState> m_oCurrentState = null; private bool m_bAtStateBegin = false; private bool m_bStarted = false; private bool m_bTerminated = false; public HierarchicalStateController() { m_oCurrentState = new List<IHierarchicalState> (); } public HierarchicalStateController(IHierarchicalState oStartState) : this() { Start(oStartState); } public void Start(IHierarchicalState oState) { if (m_bTerminated) { Debug.LogError("[HStateController] has been terminated"); return; } if (m_bStarted) { Debug.LogError("[HStateController] has been started"); return; } Debug.Log("[HStateController] Start: " + oState.ToString()); m_bStarted = true; m_oCurrentState.Add(oState); m_oCurrentState[0].SetProperty(this, 0); } public void Terminate() { for (int i = m_oCurrentState.Count - 1; i >= 0; i--) { m_oCurrentState[i].StateEnd(); } m_oCurrentState.Clear(); m_bTerminated = true; } public void TransTo(int iLevel, IHierarchicalState oState) { if (m_bTerminated) { Debug.LogError("[StateController] has been terminated"); return; } if (!m_bStarted) { Debug.LogError("[StateController] need to be started first"); return; } if (iLevel > m_oCurrentState.Count) { Debug.LogError("[StateController] Level is too big"); return; } Debug.Log(string.Format("[StateController] Level {0:} transTo: {1:}", iLevel, oState.ToString())); if (iLevel == m_oCurrentState.Count) { m_oCurrentState.Add(oState); m_oCurrentState [iLevel].SetProperty(this, iLevel); } else { for (int i = m_oCurrentState.Count - 1; i >= iLevel; i--) { m_oCurrentState [i].StateEnd (); m_oCurrentState.RemoveAt (i); } m_oCurrentState.Add(oState); m_oCurrentState [iLevel].SetProperty(this, iLevel); } } public void StateUpdate() { if (m_bTerminated || !m_bStarted) { return; } for (int i = 0; i < m_oCurrentState.Count; i++) { if (m_oCurrentState[i].AtStateBegin) { m_oCurrentState[i].TouchStateBegin(); m_oCurrentState[i].StateBegin(); if (m_oCurrentState == null) { return; } } m_oCurrentState[i].StateUpdate(); } } } }
|
IHierarchicalState
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54
| using UnityEngine; using System.Collections; namespace DouduckGame { public abstract class IHierarchicalState { private HierarchicalStateController m_StateController; protected HierarchicalStateController Controller { get { return m_StateController; } } private int m_iLevel = 0; protected int StateLevel { get { return m_iLevel; } } private bool m_bAtStateBegin = true; public bool AtStateBegin { get { return m_bAtStateBegin; } } public void SetProperty(HierarchicalStateController oController, int iLevel) { m_StateController = oController; m_iLevel = iLevel; } public void TouchStateBegin() { m_bAtStateBegin = false; } protected void TransTo(IHierarchicalState oState) { m_StateController.TransTo(m_iLevel, oState); } protected void TransTo(int iLevel, IHierarchicalState oState) { m_StateController.TransTo(iLevel, oState); } protected void AddSubState(IHierarchicalState oState) { m_StateController.TransTo(m_iLevel + 1, oState); } public virtual void StateBegin() {} public virtual void StateUpdate() {} public virtual void StateEnd() {} public override string ToString () { return string.Format ("<IHState>" + this.GetType().Name); } } }
|