「遊戲引擎Mojoc」(4)面向組件-狀態機-消息驅動3合1編程模型

實踐中,發現面向組件-狀態機-消息驅動,如果整合起來,能夠更加自然和簡單的進行抽象。這些都是以面向對象爲基礎,更進一步的抽象擴展。最初的靈感是在看這本書的時候產生的,Practical UML Statecharts in C/C++, Second Edition。本文會分別簡單介紹一下,面向組件,狀態機,消息驅動的各自特點。然後,結合Mojoc的代碼,看看是如何把三者整合起來的使用的。

面向組件

顯然遊戲引擎的架構是一個很切合的使用場景,通常有以下特點。
* 基類Component提供組件生命週期的綁定,子類負責實現特定的功能。
* Entity對象使用組合模式,負責管理多個Component,包括Component的狀態切換和生命週期。
* 代碼功能以組件爲單位得到複用與自由組合。
* Entity作爲組件的抽象集合,可與其它Entity發生交互和消息交換。

面向組件,其實就是把繼承得到的功能複用模式,拆散到組件裏,然後使用組合模式來綁定組件使用。這樣就解決了面向對象的以下幾個問題。
* 爲了一個功能去繼承,獲得了父類其它無用的,甚至不相關的功能,形成了繼承模板的冗餘。
* 繼承鏈超過3層的時候,對象職能無法保持單一,不便於記憶和使用,增加了心智負擔,讓人心理感覺複雜。
* 繼承鏈中,父類屬性個數增長迅速,不僅有潛在的衝突,還會互相影響形成意外的結果。
* 對於複用功能層面,繼承複用顯得力度過大,也不利於類的抽象設計。

面向組件是抽象力度更小的描述,組合模式相比繼承帶來了隔離性,不會傳遞繼承鏈上的屬性和功能。如果在面向對象的設計中,抽象更多的工具類,然後組合起來使用,並且增加這些工具類的生命週期和狀態管理,這已經是面向組件的設計思想了。

狀態機

  • 任何事物都有狀態,並在某一刻處在某一種狀態下。
  • 狀態機是一種視角,通過變化的切入點來抽象和描述。
  • 如果說組件和對象是一種靜態描述,那麼狀態機就是一種動態描述。
  • 狀態機起到了隔離並捕捉變化的作用,讓狀態描述和抽象更加單一和內聚。

消息驅動

有了組件化的靜態描述,有了狀態機的動態描述,那麼剩下的就是消息傳遞來產生交互了。消息的傳遞,驅動了狀態的變化,驅動依靠的是行爲也就是函數,狀態變化則就是屬性變化的結果。有了消息驅動,就讓靜態描述,表現出了動態變化,產生了交互。消息驅動,一般通過,觀察者模式,消息訂閱,或是消息輪詢來實現。

3合1

如果,我們把以上三者合起來,抽象成一個最基本的結構。可以想象,一個原子化的組件,實現了單一功能,有自己的狀態變化,可以發送消息,也能夠處理消息。這個原子組件,可以自由的組合,形成更大一些組件,然後遞歸的組合,形成更豐富的組件。並且這一切都是動態變化的,無論是更大的組件還是更小的組件,都是由原子組件所構成,有自己狀態和交互性。這將會形成很強的抽象和描述能力。

Mojoc的實現

Mojoc實現了這個模式,Component.h,在編寫遊戲邏輯的時候感覺是清晰而明確的,可以參看,Hero.c 和 Enemy.c。下面介紹一下Mojoc的實現代碼。

組件

這裏並沒有使用Entity去管理Component,而是把Entity的功能嵌入了Component,因爲我覺得這樣也行。Component形成一個遞歸的樹形結構,可以管理子Component。

struct Component
{
    int                            order;
    Component*                     parent;
    ArrayIntMap(order, Component*) childMap[1];
};


struct AComponent
{
    void  (*AddChild)          (Component* parent, Component* child, int order);
    void  (*AppendChild)       (Component* parent, Component* child);
    void  (*RemoveChild)       (Component* parent, Component* child);
    void  (*RemoveAllChildren) (Component* parent);
    void  (*ReorderAllChildren)(Component* parent);
};
  • Component通過childMap去管理子Component。
  • 平行的子Component通過Order排序。

狀態機

struct ComponentState
{
    int  id;
    void (*Update)     (Component* component, float deltaSeconds);
    void (*UpdateAfter)(Component* component, float deltaSeconds);
    bool (*OnMessage)  (Component* component, void* sender, int subject, void* extraData);
};


struct Component
{
    ComponentState*                       curState;
    ComponentState*                       preState;
    ComponentState*                       defaultState;
    ArrayIntMap(stateId, ComponentState*) stateMap[1];
};


struct AComponent
{
    void            (*SetState)(Component* component, int stateId);
    ComponentState* (*AddState)(Component* component, int stateId, ComponentStateOnMessage onMessage, ComponentStateUpdate update);
};
  • 每一個狀態,都可以通過OnMessage去接受和處理消息。
  • 狀態並沒有,OnEnter和OnExit的回調,是因爲被整合進了OnMessage。
  • 包括事件處理,Component之間的消息,都是通過OnMessage來處理。
  • 層次的Components,整體上看,可以組成一個層次狀態機。

消息驅動

struct Component
{
    ArrayIntSet(Component*) observerSet[1];
};


struct AComponent
{
    void (*AddObserver)   (Component* sender,    Component* observer);
    void (*RemoveObserver)(Component* sender,    Component* observer);
    bool (*SendMessage)   (Component* component, void*      sender,  int   subject, void* extraData);
    void (*Notify)        (Component* sender,    int        subject, void* extraData);
};
  • 每一個Component都是可以發佈消息,或訂閱消息的。
  • SendMessage是向Component和子Component發送消息。
  • Notify是向訂閱者發佈消息。

總結

組件,狀態機,消息驅動,是常用基礎的設計模式,只不過大部分時候是獨立分散的使用。這裏我把它們放到一起,作爲一個最基礎的結構,強制性使用,可能會浪費一些空間。但換一種視角,現實世界所有的一切,的確無論大到宇宙星球,還是小到微觀粒子,都有狀態,能接受信息,也能釋放信息。這種抽象或許就是應對了現實世界的運作模式。

當然,這個模式可以使用任何語言實現,好不好用,嘗試了才知道。


「組件-狀態-消息」

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章