實踐中,發現面向組件-狀態機-消息驅動,如果整合起來,能夠更加自然和簡單的進行抽象。這些都是以面向對象爲基礎,更進一步的抽象擴展。最初的靈感是在看這本書的時候產生的,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是向訂閱者發佈消息。
總結
組件,狀態機,消息驅動,是常用基礎的設計模式,只不過大部分時候是獨立分散的使用。這裏我把它們放到一起,作爲一個最基礎的結構,強制性使用,可能會浪費一些空間。但換一種視角,現實世界所有的一切,的確無論大到宇宙星球,還是小到微觀粒子,都有狀態,能接受信息,也能釋放信息。這種抽象或許就是應對了現實世界的運作模式。
當然,這個模式可以使用任何語言實現,好不好用,嘗試了才知道。
「組件-狀態-消息」