2023-06-23|閱讀時間 ‧ 約 12 分鐘

Unity C# | 狀態模式(State Diagram)

一、前言

  這篇文章將會講述設計模式中的狀態模式,其資料源自於書籍、網路、個人理解,從簡介→架構→撰寫→測試的整個流程進行介紹與分享。

1. 書籍《設計模式與遊戲開發的完美結合》
  我從班導師的研究室中找到一本書,書籍的名稱叫做《設計模式與遊戲開發的完美結合(Design Pattrns in Game Development)》,我買來這本書籍有一段時間了,這一個暑假正式開始研究它。

2. 網路資源
  在網路上我找到了很多人的教學,有些人比較直接,拍著書籍畫面就開始介紹這個設計方法,其內容看起來與其說是文章,不如說是心得;也有一些講述很完整,都很值得閱讀與參考。

3. 個人理解
  這篇文章將會結合我的個人理解,包含我是怎麼理解狀態模式,有沒有比較形象且具體的理解方式,我期許這篇文章將會幫助「想要學習狀態模式」或「想要複習狀態模式」的讀者。
  有時候我會回來看看我自己寫的文章,這極大的幫助我複習我所學習的知識與技術,這個暑假如果有時間與精力,我會考慮把文章做一個系統性的整理,發佈在 Medium 或 Blogger 上面。

二、簡介-狀態模式(State Diagram)

  替物件導向構思了設計模式(Disign Pattern)的四位作者,被暱稱為四人幫(Gof),其初衷為:「每一種設計模式都在說明一個一再出現的問題,並描述解決方案的核心,讓設計師能夠據以變化,產生出各種招式來,解決上萬個類似的問題。」

1. 定義
  在狀態模式(State Diagram)中的定義為:「讓一個物件的行為隨著狀態的改變而變化,而該物件也像是換了類別一樣。」
  在這句話中,有一個容易被忽略的重點:這個物件沒有改變,玩家依然看到同樣的物件,具有相同的架構與組成;玩家要控制的函式依然是同一個,只是裡面的執行內容不同了。
  像是魔法師會施放法術,玩家在操控這位魔法師的時候,它只需要按下鍵盤上的「Q鍵」施放「法術」,而不需要知道實際上是「小火球」還是「閃電」的效果,也不用思考有幾種法術要切換。

2. 用途
  狀態模式是常被使用的設計模式,它可以被拿來運用於關卡場景,讓場景切換不那麼死板,甚至附加一些規則;也能拿來被使用於角色狀態,給予角色不同的狀態欄位,只需要添加上程式腳本就可以完成擴充與切換;運用在敵人AI上面,讓敵人具有更多變的動作與維護性。

3. 重要性
  狀態模式的重要性很高,它具有高內聚性、鬆耦合性的特色,這點我在一篇講述耦合內聚的文章有說過,這是一種利於維護、可讀性高,無論擴充還是刪減都很方便的一種優秀設計。
  因此其擴展性與適應性都很不錯,能夠讓遊戲角色、敵人、關卡等等,與玩家的互動更具彈性和多樣性,這讓遊戲開發人員能輕鬆新增更多的狀態和相應的行為,卻不會影響現有的程式碼,這種可擴展性使得遊戲的功能和內容可以逐步擴充和升級。

三、架構-狀態模式(State Diagram)

  在這一次的介紹中,狀態模式總共有五個程式腳本,依據我個人的理解,可以粗淺的分類為管理者與封裝內容,我們可以通過 Request() 函式來跟管理者要求執行狀態,並用 SetState() 來跟管理者要求替換狀態。
  當狀態模式(State Diagram)全部撰寫完成以後,管理者是其他程式腳本唯一可以調用的內容,其中狀態(State)與具體狀態(Concrete State)都是不可以調用的封裝內容。
  如果未來要更新具體狀態,也就是讓玩家多出一個新的狀態、或讓敵人新增一個新的判斷邏輯,程式設計師只需要新建一個程式腳本,就可以多出一個新的具體狀態(ex. ConcreteStateD),而不需要修改任何一個程式腳本。

1. 環境(Context)
  這個英文單字的中文翻譯是上下文或語境,在英漢字典中,我認為有一個更貼切的翻譯是環境圖(Context Diagram),環境中會有單個或複數個狀態,而詳細的狀態則可以多達上萬個。
  環境程式腳本是一個普通的類別,裡面帶有私有的狀態欄位,可以設定新的狀態給它,或要求它執行該狀態的行為。

2. 狀態(State)
  這是一個抽象類別,具有一個建構式與一個抽象函式,建構式是一個帶參數的建構式,初始化的內容為指定具體的環境(Context),抽象函式則是規定所有具體狀態,它們都必須持有一個要執行的函式。
  我不完全理解抽象(Abstract)與介面(Interface)的差異,抽象只能被一個程式腳本繼承,並且可以寫一些具體的執行內容;介面可以被多個程式腳本繼承,不過無法寫具體的執行內容。
  使用抽象(Abstract)而非介面(Interface)的原因,我推測是因為單一繼承的特性,或存在非公用(Public)的建構式,所以才使用抽象,否則我猜使用抽象或介面應該都可以完成狀態模式(State Diagram)的撰寫。

3. 具體狀態(Concrete State)
  這是一個繼承自狀態(State)的類別,可以有複數類似的程式腳本,它具有一個建構式、實例化的抽象函式,建構式除了自己本身以外,還會呼叫基底類別的建構函式,初始化的功能是指定一個具體的環境。
  實例化的抽象函式則是實作基底類別的抽象函式,每個類似的程式腳本可以依據自己的需求撰寫不同的功能。

四、撰寫-狀態模式(State Diagram)

  接下來我們談談程式撰寫的具體範例,所有的程式腳本都不需要使用Unity內建的 MonoBehaviour,如果沒有刪除,在測試時使用建構函式的過程會讓系統警告你實例化了 Monobehaviour。
  我們會從環境(Context)開始撰寫,接下來定義抽象類別(State),最後撰寫三個具體狀態(Concrete State)。

1. 環境(Context)
public class Context {   State m_State = null;      public void Request(int Value)   {     m_state.Handle(value);   }      public void SetState(State theState)   {     Debug.Log("Context.SetState:" + theState);     m_State = theState;   } }
  為了方便測試,環境(Context)要求具體狀態執行的內容,我們採用簡單的數值計算,並且在設定一個新的狀態中,添加一個除錯用的說明訊息。

2. 狀態(State)
public abstract class State {   protected Context m_Context = null;   public State (Context theContext)   {     m_Context = theContext;   }   public abstract void Handle (int Value); }
  其中函式 State 沒有 void 卻不用使用回傳值(return),是因為它跟類別名稱相同,因此會被判斷為一個建構式,有興趣了解建構式可以參考下列文章。

3. 具體狀態(Concrete State)
public class ConcreteStateA : State {   public ConcreteStateA(Context theContext) : Base (the Context)   {}      public overrid void Handle (int Value)   {     Debug.Log("ConcreteStateA.Handle");     if ( Value 10)       m_Context.SetState ( new ConcreteStateB(m_Context );   } }
public class ConcreteStateB : State {   public ConcreteStateB(Context theContext) : Base (the Context)   {}      public overrid void Handle (int Value)   {     Debug.Log("ConcreteStateB.Handle");     if ( Value 20)       m_Context.SetState ( new ConcreteStateC(m_Context );   } } public class ConcreteStateC : State {   public ConcreteStateC(Context theContext) : Base (the Context)   {}      public overrid void Handle (int Value)   {     Debug.Log("ConcreteStateC.Handle");     if ( Value 30)       m_Context.SetState ( new ConcreteStateA(m_Context );   } }
  為了方便測試,數值每到一個階段 ,就會切換狀態。

五、測試-狀態模式(State Diagram)

  接下來,我們可以創建一個新的 Unity 程式腳本:
public class UnitTest : MonoBehaviour { private void Start()     UnitTest_implement(); private void UnitTest_implement() { Context context = new Context(); context.SetState(new ConcreteStateA(context)); context.Request(5); context.Request(15); context.Request(25); context.Request(35); } }

1. 建立新的物件 / 物件實例化
  在「Context context = new Context();」的過程中,我們創建了一個新的環境(Context)欄位,並且使用建構式創建一個新的環境給指派進去。

2. 指定具體狀態
  隨後,我們指定了一個新的具體狀態給這個新環境,也用類似的做法創建一個新的具體狀態A,並且把這個新環境給具體狀態A。

3. 測試不同內容
  隨後就是測試整個狀態模式可能會有的所有狀態了,詳細的就不多說,可以直接看結果,核對是否正確。

六、後記

  當初在學習狀態模式的時候,我卡在狀態類別(State)中的建構式,我一直沒有看懂這個函式是幹嘛的,那個時候我還沒有注意到它跟類別名稱一樣,直到我清楚建構式的概念後,我才算是真正學會了狀態模式。
  閱讀這本書籍以後,我才理解所謂的單元測試是在幹嘛,測試程式中的所有階段,以及可能遇到的狀態,而我以前寫的所有程式,根本都不到需要使用單元測試的階段,當然不需要。
分享至
成為作者繼續創作的動力吧!
© 2024 vocus All rights reserved.