之前稍微提了一下一般遊戲物件及行為的架構,繼承的部分我就不寫了,各位有興趣可以自己用滿滿的繼承寫一個遊戲,這裡我們要演示的是Component Pattern
。
我Component Pattern
的相關知識是從這本書(Game Programming Pattern)學來的,基本上這本書裡講了很多在遊戲設計上,可以很好解決問題的方式,有些我覺得甚至不會限於遊戲設計上,在其他領域也可以用。
有點離題了,我打算用Component Pattern
寫一個小小的demo,大概就是一堆物件由上往下掉這麼簡單,至於這個物件呢,由於我很喜歡沙奈朵,就決定放成千上萬個沙奈朵在螢幕上了!
這裡的render
部份我不會用到openGL
,因為我主要想講的是架構的部分,若想要用前幾天寫好的Renderer2D
也是可以的喔。
我還只是個菜鳥,經驗還不夠,一定有更好的寫法。
我們最終目標是成千上萬個沙奈朵往下掉,那我們就先想辦法讓一隻沙奈朵往下掉。
我在Info提過,我們會有個Entity
,裡面存著Component
所以呢,我們就先寫一個空殼Entity
吧
class Entity {
public:
Entity();
void update(float dt);
void render(sf::RenderTarget& target);
};
身為一個遊戲物件,有一個update
是很稀鬆平常的事,至於這個render
嗎,事後想想把它寫成一個component
好像比較好,不過既然我知道我整個螢幕的每個物件都會需要render
,就讓我偷懶一下吧XD。
接著我們來想想我們會需要那些component
想好之後就可以開始寫我們的component
囉
在寫之前,因為知道我會需要一些data
,所以把他寫進Entity
class Entity {
public:
float x_, y_;
float v_;
Entity();
void update(float dt);
void render(sf::RenderTarget& target);
};
如果讀了GPP那本書,就會知道我這樣寫不好,更好的方式是將這些
data
寫進component
裡
接著Component
class Entity;
class Components {
public:
virtual ~Components() {}
virtual void update(Entity &entity, float dt) {}
virtual void render(sf::RenderTarget &target) {}\
};
class Transform: public Components {
public:
Transform();
virtual void update(Entity &entity, float dt) override;
};
class RigidBody: public Components {
public:
virtual void update(Entity &entity, float dt) override;
};
class Graphic: public Components {
public:
Graphic(std::string filePath);
virtual void update(Entity &entity, float dt) override;
virtual void render(sf::RenderTarget &target) override;
private:
sf::Texture texture_;
sf::Sprite sprite_;
};
其實可以不用一層抽象層,不過寫習慣了就不小心也寫進去了XD
接著把他時做就可以了
實作完之後呢,我們回到我們的Entity
class Transform;
class RigidBody;
class Graphic;
class Entity {
public:
float x, y;
float v;
Entity();
Entity(Transform *transform, RigidBody *rigidBody, Graphic *graphic);
void update(float dt);
void render(sf::RenderTarget &target);
private:
Transform *transform_;
RigidBody *rigidBody_;
Graphic *graphic_;
};
有那層抽象層我們可以利用外部代碼決定我們需要哪些不需要哪些Component,不過這裏我們明確知道我們需要哪些Component。
我們在Entity
分別存著3個Component
的pointer,而我們的update
也很簡單
void Entity::update(float dt) {
transform_->update(*this, dt);
rigidBody_->update(*this, dt);
graphic_->update(*this, dt);
}
這樣就完事了? 差不多, 還差一點呢
我們要來產生物件
MAX_ENTITES = 10000;
std::vector<Entity*> v;
for(int i = 0 ; i < MAX_ENTITES ; i++) {
Entity *tmp = new Entity(new Transform(), new RigidBody(), new Graphic("沙奈朵.png"));
v.push_back(tmp);
}
這裡出現了問題,我將圖片路徑傳進Graphic
,讓他載入圖片,但我這樣寫他會需要載入一萬次圖片,你能接受遊戲開始前要等5分鐘或更久嗎。
答案是不能,絕對不可能。
所以我打算利用這個模式來處理
SFML載入圖片是存在sf::Texture
,而能繪製在螢幕上的是sf::Sprite
他們之間的關係就是這樣
簡單來說Texture
就是一張圖片,而把這張圖片貼在矩形物件上就成為Sprite
,就可以繪製了
既然我要產生一萬個同樣的沙奈朵物件,我要做的不應該是載入一萬張同樣沙奈朵圖片,而是載入一張沙奈朵圖片,然後每個Sprite
都利用那張沙那朵去設定。
所以我寫了個沙奈朵的Model
class Gardevoir {
public:
Gardevoir(std::string path);
sf::Texture texture;
};
然後將我的Graphic
改成這樣
class Graphic: public Components {
public:
Graphic(Gardevoir *gardevoir);
virtual void update(Entity &entity, float dt) override;
virtual void render(sf::RenderTarget &target) override;
private:
Gardevoir *gardevoir_;
sf::Sprite sprite_;
};
我在這裡存了沙那朵的pointer,等於我一萬個物件都會指向同一個沙奈朵
我產生物件的方式就會變這樣
std::vector<Entity*> v;
Gardevoir *gardevoir = new Gardevoir("沙奈朵.png");
for(int i = 0 ; i < MAX_ENTITES ; i++) {
Entity *tmp = new Entity(new Transform(), new RigidBody(), new Graphic(gardevoir));
v.push_back(tmp);
}
這樣就只需要載入一次沙奈朵圖片,太棒惹。
我們的Gamp loop
怎麼進行呢
float dt = 0.0;
while (running) {
auto startTime = std::chrono::high_resolution_clock::now();
sf::Event event;
while (window.pollEvent(event))
{
if (event.type == sf::Event::Closed)
running = false;
}
window.clear(bgColor);
for(int i = 0 ; i < v.size() ; i++)
v[i]->update(dt);
for(int i = 0 ; i < v.size() ; i++)
v[i]->render(window);
window.display();
auto stopTime = std::chrono::high_resolution_clock::now();
dt = std::chrono::duration<float, std::chrono::seconds::period>(stopTime - startTime).count();
std::cout << dt << "\n";
}
這樣就完成了,雖然我偷懶了很多地方,但就算組件多起來,寫起來還是挺清晰的,我自己本身是很喜歡這個方法的,因為真的很好寫。
接下來要講的ECS是真的非常難,而且那也不算我自己寫出來的,是看著國外教學文寫的XDDDD,寫完真的覺得想出這個架構的人真的是鬼吧。
接下來給大家看看demo,擺在最後是因為要請有密集恐懼症的人趕緊逃離阿