以前呢,讀過Game Programming Patterns這本書,讀完真的是受益良多阿~~。
以前學過C++
的繼承之後,在寫大型一點的專案的時候,就會開始無腦的繼承,接著就會遇到循環繼承問題,又或著是繼承鍊長到不行,根本無法維護。
當時就在想,一個大型的遊戲到底是怎麼設計的,一定不可能無腦繼承阿,於是乎讀了這本書之後,才發現原來還有這種方法阿。這幾天就來稍微講講遊戲的實體(Entity)到體是怎麼實現的。
遊戲的本質其實就是大量實體(Entity)的行為以及它們之間的交互(Manager)。
但顯然遊戲裡的物件不會只有少少幾個。隨著物件邏輯的膨脹,我們會將相同邏輯的部分進行拆分,而拆分的方法有很多種。
最直觀的寫法就是繼承,當我們的實體需要哪些邏輯的部分,我們就繼承那個部分。
除了菱形繼承的問題,我們說過,遊戲裡的物件不只有那麼少,當遊戲物件多起來邏輯多起來,如果用繼承來解,祖譜肯定是很可觀的。
比起繼承,組件的靈活度更高。當我們需要那部分邏輯時,我們不再繼承他,而是讓物件擁有他。
這樣不但沒有宏偉的繼承樹,要讓各個實體溝通也就方便許多。
這裡我喜歡稱這些邏輯為
component
。
遊戲一般到這裡就很OK了,當然偶爾也會用到繼承,但這裡的繼承通常只會讓你更方便(多個虛構層)。在利用組件模式設計時,大概只有在實體間互相邏輯會費較多心思。
我相信利用組件模式寫一些課堂上小專案的作業肯定綽綽有餘。不過你可能還是會遇到一些問題。
manager
,接著你會思考,同一段邏輯究竟要放在component
,還是放在manager
Entity
以及Game loop
87%長這樣class Entity {
public:
void update() {
physis->update();
transform->update();
graphic->update();
}
private:
Physis* physis;
Transform* transform;
Graphic* graphic;
};
GameLoop() {
while(running) {
for(int i = 0 ; i < MAX_ENTITES ; i++)
entityList->update();
}
}
這裡要說的問題是,這種方法對緩存不有好,CPU在從記憶體抓取資料時會一次抓取一組資料,稱為cache line
這樣當CPU處裡完你當前要他處理的資料,要去做下一個時,他會從cache line
找,如果找到,就不必浪費時間再去記憶體找。
我們的遊戲存了每個實體的pointer,實體存著每個component的pointer,這樣在遍歷時,會有很多很多的cache miss
,這樣會花費很多額外的時間。一個遊戲注重的,除了遊戲本身好不好玩,另外一個就是效能了。大家都知道遊戲的卡頓才是造成人類暴力的原因。
那剛剛提到的兩個問題有甚麼解決方法呢,就是以下的這個方法囉
Entity Component System,簡稱ECS,這裡是指我們會有3個東西-Entity, Component, System-而不是 "Entity Component" System XD。
先上圖了解一下ECS的架構
你問Component
去哪了呢?其實就是Data
啦
我來說明一下以上的東西
Component
,不一樣的地方在於,這次他不會存在任何邏輯,他存的僅僅只有data
struct Transform {
float posX_;
float posY_;
float scale_;
}
Data
跟Logic
都發出去了那還剩甚麼,Entity還真的甚麼都沒有,他僅僅是一個ID
,想想看,我們一群component
要怎麼知道我們屬於哪個Entity
?ID就好了。所以Entity其實就是Data溝通的橋樑,常常Entity只是一個int
而已using Entity = std::size_t;
在實作的過程,會將資料緊密的包在連續記憶體裡,為了確保不會發生太多的cache miss
接下來我會給大家看看我自己時做的component pattern
、ECS
(參考)以及別人做的ECS系統Entt