ECS 遊戲架構 實現
轉載自:http://blog.csdn.net/i_dovelemon/article/details/27230719
實現 元件-實體-系統 - 部落格頻道
這篇文章是在我前面文章,理解元件-實體-系統,的基礎上進行的。如果你還沒有閱讀過這篇文章,建議你去看看,這樣你就會對這裡要實現的內容不會那麼的陌生。
先來總結下,上篇文章講些什麼內容:
- 元件表示一個遊戲物件可以擁有的資料部分
- 實體用來代表一個遊戲物件,它是多個元件的聚合
- 系統提供了在這些元件上進行的操作
這篇文章將會講述如何實現一個ECS系統,並且會解決一些存在的問題。所有的例項程式碼,我都是使用C語言來編寫。
元件
在上篇文章中,我曾說過,元件實際上就是一個C結構體,只擁有簡單普通的資料而已,所以我也就會使用結構體來實現元件。下面的元件,從名字上看就能夠很好的明白它的作用到底是什麼。在下面我會實現三種元件:
- Displacement(x,y)
- Velocity(x,y)
- Appearance(name)
下面的程式碼,演示瞭如何定義Displacement元件。它只是擁有兩個分量的簡單結構體而已:
- typedef struct
- {
- float x ;
- float y ;
- } Displacement ;
typedef struct { float x ; float y ; } Displacement ;
Velocity元件也是同樣的進行定義了,顯示中只是含有一個string成員而已。
除了上面定義的具體的元件型別,我們還需要一個元件標示符,用來對元件進行標示。每一個元件和系統都會擁有一個元件標示符,如何使用將會在下面詳細的解釋。
- typedef enum
- {
- COMPONENT_NONE = 0 ,
- COMPONENT_DISPLACEMENT = 1 << 0 ,
- COMPONENT_VELOCITY = 1 << 1 ,
- COMPONENT_APPEARANCE = 1 << 2
- } Component ;
typedef enum { COMPONENT_NONE = 0 , COMPONENT_DISPLACEMENT = 1 << 0 , COMPONENT_VELOCITY = 1 << 1 , COMPONENT_APPEARANCE = 1 << 2 } Component ;
定義元件標示符是很簡單的事情。在實體的上下文中,我們使用元件標示符來表示這個實體擁有哪些元件。如果這個實體,擁有Displacement和 Appearance元件,那麼這個實體的元件標示符將會是 COMPONENT_DISPLACEMENT | COMPONENT_APPEARANCE 。
實體
實體本身不會被明確的定義為一個具體的資料型別。我們並不會使用面向物件的方法來對實體進行一個類的定義,然後讓它擁有一系列的成員屬性。因此,我們將會 將元件加入到記憶體中去,建立一個結構陣列。這樣會提高緩衝效率,並且會有助於迭代。所以,為了實現這個,我們使用這些結構陣列的下標來表示實體。這個下 標,就表示是實體的一個元件。
我稱這個結構陣列為World。這個結構,不僅僅保留了所有的元件,而且還儲存了每一個實體的元件標示符。
- typedef struct
- {
- int mask[ENTITY_COUNT];
- Displacement displacement[ENTITY_COUNT];
- Velocity velocity[ENTITY_COUNT];
- Appearance appearance[ENTITY_COUNT];
- } World;
typedef struct { int mask[ENTITY_COUNT]; Displacement displacement[ENTITY_COUNT]; Velocity velocity[ENTITY_COUNT]; Appearance appearance[ENTITY_COUNT]; } World;
ENTITY_COUNT在我的測試程式中,被定義為100,但是在一個真實的遊戲中,這個值應該更加的大。在這個實現中,最大數值就被限制在100.實 際上,我更加喜歡在棧中實現這個結構陣列,而不是在堆中實現,但是考慮到讀者可能會使用C++來實現這個World,它也是可以使用vector來儲存 的。
除了上面的結構體之外,我還定義了一些函式,來對這些實體進行建立和銷燬。
- unsigned int createEntity(World *world)
- {
- unsigned int entity;
- for(entity = 0; entity < ENTITY_COUNT; ++entity)
- {
- if(world->mask[entity] == COMPONENT_NONE)
- {
- return(entity);
- }
- }
- printf("Error! No more entities left!\n");
- return(ENTITY_COUNT);
- }
- void destroyEntity(World *world, unsigned int entity)
- {
- world->mask[entity] = COMPONENT_NONE;
- }
unsigned int createEntity(World *world) { unsigned int entity; for(entity = 0; entity < ENTITY_COUNT; ++entity) { if(world->mask[entity] == COMPONENT_NONE) { return(entity); } } printf("Error! No more entities left!\n"); return(ENTITY_COUNT); } void destroyEntity(World *world, unsigned int entity) { world->mask[entity] = COMPONENT_NONE; }
實際上,create方法並不是建立一個實體,而是返回World中第一個為空的實體下標。第二個方法,只是簡單的將實體的元件表示符設定為 COMPONENT_NONE而已。把一個實體設定為空的元件是很直觀的表示方法,因為它為空的話,就表示沒有任何的系統將會在這個實體上進行操作了。
我還編寫了一些用來建立完整實體的程式碼,比如下面的程式碼將會建立一個Tree,這個Tree只擁有Displacement和Appearance。
- unsigned int createTree(World *world, float x, float y)
- {
- unsigned int entity = createEntity(world);
- world->mask[entity] = COMPONENT_DISPLACEMENT | COMPONENT_APPEARANCE;
- world->displacement[entity].x = x;
- world->displacement[entity].y = y;
- world->appearance[entity].name = "Tree";
- return(entity);
- }
unsigned int createTree(World *world, float x, float y) { unsigned int entity = createEntity(world); world->mask[entity] = COMPONENT_DISPLACEMENT | COMPONENT_APPEARANCE; world->displacement[entity].x = x; world->displacement[entity].y = y; world->appearance[entity].name = "Tree"; return(entity); }
在一個真實的遊戲引擎中,你的實體可能需要額外的資料來進行建立,但是這個已經不再我介紹的範圍內了。儘管如此,讀者還是可以看見,這樣的系統將會具有多麼高的靈活性。
系統
在這個實現中,系統是最複雜的部分了。每一個系統,都是對某一個元件進行操作的函式方法。這是第二次使用元件標示符了,通過元件標示符,我們來定義系統將會對什麼元件進行操作。
- #define MOVEMENT_MASK (COMPONENT_DISPLACEMENT | COMPONENT_VELOCITY)
- void movementFunction(World *world)
- {
- unsigned int entity;
- Displacement *d;
- Velocity *v;
- for(entity = 0; entity < ENTITY_COUNT; ++entity)
- {
- if((world->mask[entity] & MOVEMENT_MASK) == MOVEMENT_MASK)
- {
- d = &(world->displacement[entity]);
- v = &(world->velocity[entity]);
- v->y -= 0.98f;
- d->x += v->x;
- d->y += v->y;
- }
- }
- }
#define MOVEMENT_MASK (COMPONENT_DISPLACEMENT | COMPONENT_VELOCITY) void movementFunction(World *world) { unsigned int entity; Displacement *d; Velocity *v; for(entity = 0; entity < ENTITY_COUNT; ++entity) { if((world->mask[entity] & MOVEMENT_MASK) == MOVEMENT_MASK) { d = &(world->displacement[entity]); v = &(world->velocity[entity]); v->y -= 0.98f; d->x += v->x; d->y += v->y; } } }
這裡就顯示出了元件標示符的威力了。通過元件標示符,我們能夠在函式中確定這個實體是否具有這樣的屬性,並且速度很快。如果將每一個實體定義為一個結構體的話,那麼確定它是否有這些元件,這樣的操作將會非常耗時。
這個系統,會自動的新增重力,然後對Displacement和Velocity進行操作。如果所有的實體都是正確的進行了初始化,那麼每一個進行這樣操作的實體,都會有一個有效的Displacement和Velocity元件。
對於這個元件標示符的一個缺點就是,這樣的組合是有限的。在我們這裡的實現中,它最多隻能是32位的,也就是說最多隻能夠擁有32個元件型別。C++提供 了一個名為std::bitset<n>的類,這個類可以擁有N位的型別,而且我確信,如果你使用的是其他的程式語言的話,也會有這樣的型別 提供。在C中,可以使用一個數組來進行擴充套件,像下面這樣:
- (EntityMask[0] & SystemMask[0]) == SystemMask[0] && (EntityMask[1] & SystemMask[1]) == SystemMask[1]
(EntityMask[0] & SystemMask[0]) == SystemMask[0] && (EntityMask[1] & SystemMask[1]) == SystemMask[1] // && ...
這樣的系統在我的一些程式中能夠很好的進行工作,並且這樣的系統能夠很容易的進行擴充套件。它也能夠很容易的在一個主迴圈中進行工作,並且只要新增少量的程式碼就能夠從外部讀取檔案來建立實體物件。
這一小節,將會講述在遊戲機制中可能出現的一些問題,還會講述一些這個系統所具有的高階特性。
升級和碰撞過濾
這個問題是在上篇文章中,網友Krohm提出來得。他想知道,在這樣的系統中,如何實現遊戲特殊行為了。他提出,如果在升級的時候,想要避免和某種型別的物體進行碰撞,該如何進行。
解決這樣的問題,我們使用一個叫做動態元件的東西。我們來建立一個元件,叫做GhostBehavior,這個元件擁有一個限定符列表,我們通過這個列表 來判斷,哪些實體可以讓物體穿越過去。比如說,一個元件標示符的列表,或者是材質下標的列表。任何的元件,都可以在任何時候,任何地方被移除出去。當玩 家,拾取到了一個升級包,GhostBehavior元件將會增加到玩家實體的列表中去。我們還可以為這個元件建立一個內建的定時器,一旦時間到了,就自 動的將自己從列表中移除出去。
為了不進行某些碰撞,我們可以使用物理引擎中的一個經典的碰撞迴應。在大部分的物理引擎中,第一步都是先進行碰撞檢測,然後產生接觸,在然後,為某一個物 體新增一個接觸力。我們假設,這些工作都是在一個系統中實現的,但是有一個元件能夠對每一個實體的碰撞接觸進行跟蹤記錄,這個元件叫做 Collidable。
我們建立一個新的系統,同時對GhostBehavior和Collidable進行處理。在上面介紹的兩個步驟之間,我們將實體之間的接觸刪除掉,這樣 他們就不會產生力,也就不會產生碰撞,讓物體穿越過去了。這樣的效果,就會產生一個無效的碰撞。同樣的系統也能夠用來將GhostBehavior進行移 除。
同樣的策略,也能夠用來處理,當發生了碰撞時,我們希望進行某種特定的操作的情況。對於每一個特定的行為,我們都可以建立一個系統,或者同一個系統可以同 時處理多個特定的動作。不管怎麼樣,系統都要先判斷兩個物體是否發生了碰撞,然後才能夠進行特定的行為。
消滅所有怪物
另外一個問題,就是如何通過一個指令,來秒殺所有的怪物。
解決這個問題的關鍵地方是實現一個系統,這個系統將會在主迴圈的外面進行。任何一個實體,如果它是怪物的話,那麼它就應該有一個同樣的元件標示符。比如說,同時擁有AI和血量的實體,就是怪物,這樣的判斷可以簡單的使用元件標示符來進行判斷。
還記得我們在上面說過的,每一個系統實際上就是對某個元件標示符進行操作的函式。我們將秒殺技能定義為一個系統。這個系統將會用一個函式來實現。在這個函 數中,,最核心的操作就是呼叫destroyMonster函數了,但是同時可能也會建立一個粒子特效,或者播放一段音樂等。這個系統的元件標示符可能是 這樣的COMPONENT_HEALTH COMPONENT_AI。
在前面一篇文章中,我講述過了每一個實體都能夠擁有一個或者多個輸入元件,這些輸入元件將會包括一個boolean值,或者真實的值,用來表示不同的輸 入。我們建立一個MagicInputComponet元件,這個元件只有一個bool值,一旦將這個元件加入到實體中去,每一個實體都會對這個元件進行 處理,從而消滅所有的怪物。
每一個秒殺技能都有一個獨特的ID,這個ID將會用來對查詢表進行查詢。一旦在查詢表中,找到了這個ID,那麼就呼叫這個ID對應的函式,讓這個函式,來執行這個系統消滅所有的怪物。
記住,這裡的實現只是一個非常簡單的方法。它僅僅對我們這裡的測試程式有效而已,對於一個完整的遊戲來說,它並沒有那個能力來驅動它。然後,我希望,通過這個例子,你已經明白了設計ECS系統的主要原則,並且能夠獨立的使用你自己的熟練的語言來實現它。