ETH官方钱包

前往
大廳
主題

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

瓶裝雪 | 2023-06-23 19:00:08 | 巴幣 116 | 人氣 385

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

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

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

3. 個人理解
  這篇文章將會結合我的個人理解,包含我是怎麼理解狀態模式,有沒有比較形象且具體的理解方式,我期許這篇文章將會幫助「想要學習狀態模式」或「想要複習狀態模式」的讀者。

  有時候我會回來看看我自己寫的文章,這極大的幫助我複習我所學習的知識與技術,這個暑假如果有時間與精力,我會考慮把文章做一個系統性的整理,發佈在 Medium 或 Blogger 上面。

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

1. 定義
  在狀態模式(State Diagram)中的定義為:「讓一個物件的行為隨著狀態的改變而變化,而該物件也像是換了類別一樣?!?/font>

  在這句話中,有一個容易被忽略的重點:這個物件沒有改變,玩家依然看到同樣的物件,具有相同的架構與組成;玩家要控制的函式依然是同一個,只是裡面的執行內容不同了。

  像是魔法師會施放法術,玩家在操控這位魔法師的時候,它只需要按下鍵盤上的「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)中的建構式,我一直沒有看懂這個函式是幹嘛的,那個時候我還沒有注意到它跟類別名稱一樣,直到我清楚建構式的概念後,我才算是真正學會了狀態模式。

  閱讀這本書籍以後,我才理解所謂的單元測試是在幹嘛,測試程式中的所有階段,以及可能遇到的狀態,而我以前寫的所有程式,根本都不到需要使用單元測試的階段,當然不需要。



送禮物贊助創作者 !
0
留言

創作回應

相關創作

更多創作