一、前言
這篇文章將會講述設計模式中的狀態模式,其資料源自於書籍、網路、個人理解,從簡介→架構→撰寫→測試的整個流程進行介紹與分享。
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)中的建構式,我一直沒有看懂這個函式是幹嘛的,那個時候我還沒有注意到它跟類別名稱一樣,直到我清楚建構式的概念後,我才算是真正學會了狀態模式。
閱讀這本書籍以後,我才理解所謂的單元測試是在幹嘛,測試程式中的所有階段,以及可能遇到的狀態,而我以前寫的所有程式,根本都不到需要使用單元測試的階段,當然不需要。