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.   
  5.     Displacement displacement[ENTITY_COUNT];  
  6.     Velocity velocity[ENTITY_COUNT];  
  7.     Appearance appearance[ENTITY_COUNT];  
  8. } 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.   
  12.     printf("Error!  No more entities left!\n");  
  13.     return(ENTITY_COUNT);  
  14. }  
  15.   
  16. void destroyEntity(World *world, unsigned int entity)  
  17. {  
  18.     world->mask[entity] = COMPONENT_NONE;  
  19. }  
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.   
  5.     world->mask[entity] = COMPONENT_DISPLACEMENT | COMPONENT_APPEARANCE;  
  6.   
  7.     world->displacement[entity].x = x;  
  8.     world->displacement[entity].y = y;  
  9.   
  10.     world->appearance[entity].name = "Tree";  
  11.   
  12.     return(entity);  
  13. }  
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.   
  3. void movementFunction(World *world)  
  4. {  
  5.     unsigned int entity;  
  6.     Displacement *d;  
  7.     Velocity *v;  
  8.   
  9.     for(entity = 0; entity < ENTITY_COUNT; ++entity)  
  10.     {  
  11.         if((world->mask[entity] & MOVEMENT_MASK) == MOVEMENT_MASK)  
  12.         {  
  13.             d = &(world->displacement[entity]);  
  14.             v = &(world->velocity[entity]);  
  15.   
  16.             v->y -= 0.98f;  
  17.   
  18.             d->x += v->x;  
  19.             d->y += v->y;  
  20.         }  
  21.     }  
  22. }  
#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系統的主要原則,並且能夠獨立的使用你自己的熟練的語言來實現它。

 


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM