1. 程式人生 > >ECS 遊戲架構 實現

ECS 遊戲架構 實現

轉載自:http://blog.csdn.net/i_dovelemon/article/details/27230719

實現 元件-實體-系統 - 部落格頻道

       這篇文章是在我前面文章,理解元件-實體-系統,的基礎上進行的。如果你還沒有閱讀過這篇文章,建議你去看看,這樣你就會對這裡要實現的內容不會那麼的陌生。

       先來總結下,上篇文章講些什麼內容:

  •        元件表示一個遊戲物件可以擁有的資料部分
  •        實體用來代表一個遊戲物件,它是多個元件的聚合
  •       系統提供了在這些元件上進行的操作

        這篇文章將會講述如何實現一個ECS系統,並且會解決一些存在的問題。所有的例項程式碼,我都是使用C語言來編寫。

元件

           在上篇文章中,我曾說過,元件實際上就是一個C結構體,只擁有簡單普通的資料而已,所以我也就會使用結構體來實現元件。下面的元件,從名字上看就能夠很好的明白它的作用到底是什麼。在下面我會實現三種元件:

  1.            Displacement(x,y)
  2.            Velocity(x,y)
  3.           Appearance(name)

          下面的程式碼,演示瞭如何定義Displacement元件。它只是擁有兩個分量的簡單結構體而已:

  1. typedef struct  
  2. {  
  3.         float x ;  
  4.         float y ;  
  5. } Displacement ;  
typedef struct
{
        float x ;
        float y ;
} Displacement ;

         Velocity元件也是同樣的進行定義了,顯示中只是含有一個string成員而已。

         除了上面定義的具體的元件型別,我們還需要一個元件標示符,用來對元件進行標示。每一個元件和系統都會擁有一個元件標示符,如何使用將會在下面詳細的解釋。

  1. typedef enum  
  2. {  
  3.       COMPONENT_NONE = 0 ,  
  4.       COMPONENT_DISPLACEMENT = 1 << 0 ,  
  5.       COMPONENT_VELOCITY = 1 << 1 ,  
  6.       COMPONENT_APPEARANCE = 1 << 2  
  7. } 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。這個結構,不僅僅保留了所有的元件,而且還儲存了每一個實體的元件標示符。

  1. typedef struct  
  2. {  
  3.     int mask[ENTITY_COUNT];  
  4.     Displacement displacement[ENTITY_COUNT];  
  5.     Velocity velocity[ENTITY_COUNT];  
  6.     Appearance appearance[ENTITY_COUNT];  
  7. } 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來儲存 的。

      除了上面的結構體之外,我還定義了一些函式,來對這些實體進行建立和銷燬。

  1. unsigned int createEntity(World *world)  
  2. {  
  3.     unsigned int entity;  
  4.     for(entity = 0; entity < ENTITY_COUNT; ++entity)  
  5.     {  
  6.         if(world->mask[entity] == COMPONENT_NONE)  
  7.         {  
  8.             return(entity);  
  9.         }  
  10.     }  
  11.     printf("Error!  No more entities left!\n");  
  12.     return(ENTITY_COUNT);  
  13. }  
  14. void destroyEntity(World *world, unsigned int entity)  
  15. {  
  16.     world->mask[entity] = COMPONENT_NONE;  
  17. }  
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。

  1. unsigned int createTree(World *world, float x, float y)  
  2. {  
  3.     unsigned int entity = createEntity(world);  
  4.     world->mask[entity] = COMPONENT_DISPLACEMENT | COMPONENT_APPEARANCE;  
  5.     world->displacement[entity].x = x;  
  6.     world->displacement[entity].y = y;  
  7.     world->appearance[entity].name = "Tree";  
  8.     return(entity);  
  9. }  
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);
}

      在一個真實的遊戲引擎中,你的實體可能需要額外的資料來進行建立,但是這個已經不再我介紹的範圍內了。儘管如此,讀者還是可以看見,這樣的系統將會具有多麼高的靈活性。

系統

      在這個實現中,系統是最複雜的部分了。每一個系統,都是對某一個元件進行操作的函式方法。這是第二次使用元件標示符了,通過元件標示符,我們來定義系統將會對什麼元件進行操作。

  1. #define MOVEMENT_MASK (COMPONENT_DISPLACEMENT | COMPONENT_VELOCITY)  
  2. void movementFunction(World *world)  
  3. {  
  4.     unsigned int entity;  
  5.     Displacement *d;  
  6.     Velocity *v;  
  7.     for(entity = 0; entity < ENTITY_COUNT; ++entity)  
  8.     {  
  9.         if((world->mask[entity] & MOVEMENT_MASK) == MOVEMENT_MASK)  
  10.         {  
  11.             d = &(world->displacement[entity]);  
  12.             v = &(world->velocity[entity]);  
  13.             v->y -= 0.98f;  
  14.             d->x += v->x;  
  15.             d->y += v->y;  
  16.         }  
  17.     }  
  18. }  
#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中,可以使用一個數組來進行擴充套件,像下面這樣:

  1. (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系統的主要原則,並且能夠獨立的使用你自己的熟練的語言來實現它。