很多遊戲引擎都使用ECS架構,所以也想寫寫看。但如果只了解理論就要從0開始時做我來真的不會,畢竟我就爛。不過還好有這篇的幫助,才讓我順利的寫(抄?)完ㄌ,之後有時間在寫寫看屬於自己的ECS架構吧XD。
前面說過我們將遊戲物件及其行為拆成以下三個東西
每個東西還有每個東西的Manager,所以寫起來其實是很麻煩的。
Entity僅是一個簡單的ID,他不會真的存著Component或任何其他東西,事實上他只會用來作為Component陣列的索引而已
using Entity = std::uint32_t;
const Entity MAX_ENTITES = 10000;
Component只會存著需要的Data,所以寫起來也很簡單
struct Transform {
float x_;
float y_;
};
struct Graphic {
sf::Texture_;
sf::Sprite_;
}
每種Component也會需要一個ID。
using ComponentType = std::uint8_t;
const ComponentType MAX_COMPONENT = 32;
我們希望夠能追蹤這個Entity擁有哪些Component,同時也必須追蹤這個System會有哪些Component參與其中。
為了方便追蹤,我們會給Entity及System一個Signature,上面登記著它擁有了哪些Component。
std::bitset很適合達到我們的要求,前面我們幫每種Component訂了ID,意思是指要此物件或系統擁有這物件,就只要設定那個ID的bit。
舉個例子,Transform是0,Graphic是1,RigidBody是2,
擁有Transform和RigidBody的物件,它的Signature會被設定成0b101(bit 0, 2)會被設定。
Entity Manager是負責分配及追蹤Entity,記錄著哪些Entity ID已經被使用
用最簡單的std::queue就可以達成,當產生一個Entity,我們就返回一個Entity ID,當Entity被銷毀,我們再將此ID push回去就好了
class EntityManager {
public:
EntityManager();
Entity createEntity();
void destroyEntity(Entity entity);
void setSignature(Entity entity, Signature signature);
Signature getSignature(Entity entity);
private:
// Array of signatures where the index is the Entity
std::array<Signature, MAX_ENTITIES> Signatures_;
// ALL Unused Entities
std::queue<Entity> Entites_;
// Total living Entites
uint32_t EntitiesCounts_ = 0;
};
EntityManager::EntityManager() {
// Unused Entites
for(Entity entity = 0 ; entity < MAX_ENTITIES ; entity++)
Entites_.push(entity);
}
Entity EntityManager::createEntity() {
assert(EntitiesCounts_ < MAX_ENTITIES && "Too Many Entities");
Entity id = Entites_.front();
Entites_.pop();
++EntitiesCounts_;
return id;
}
void EntityManager::destroyEntity(Entity entity) {
assert(entity < MAX_ENTITIES && "Entity Out Of Range");
Signatures_[entity].reset();
Entites_.push(entity);
--EntitiesCounts_;
}
void EntityManager::setSignature(Entity entity, Signature signature) {
assert(entity < MAX_ENTITIES && "Entity Out Of Range");
Signatures_[entity] = signature;
}
Signature EntityManager::getSignature(Entity entity) {
assert(entity < MAX_ENTITIES && "Entity Out Of Range");
return Signatures_[entity];
}
在這裡,我們必須實作一種簡單的陣列,但必須永遠是連續的,代表說它是不會有洞出現的。
因為我們的Entity只是ID,要獲得跟Entity相關的Component是很簡單的,但是當Entity被銷毀時,對於陣列來說此Index(Entity ID)已經失效,我們不希望在遍歷陣列時出現失效的Entity。
但是事實上當Entity被銷毀時,它仍然是存在在陣列裡的。
在這裡,我們維護了兩個map,存著Entity ID與Index之間的關係。當你要使用Entity時,你利用Entity ID去尋找在陣列裡實際的位置。當Entity被移除時,我們將陣列裡最後一個尚未失效的Entity移動到被移除的位置上,接著只要更新map,一切就完成。
在這裡我先掩飾給大家看看。
我們假設MAX_ENTITES5,陣列一開始是空的,map也是空的
| Array | 0: | 1: | 2: | 3: | 4: | 5: |
|---|---|---|---|---|---|---|
| Entity->Index | ||||||
| Index->Entity | ||||||
| Size | 0 |
接著我們加入Entity A, 它的Entity ID為0
在陣列中的位置是0,所以將map的對應關係寫好
| Array | 0: A | 1: | 2: | 3: | 4: |
|---|---|---|---|---|---|
| Entity->Index | 0:0 | ||||
| Index->Entity | 0:0 | ||||
| Size | 1 |
接著加入B
| Array | 0: A | 1: B | 2: | 3: | 4: |
|---|---|---|---|---|---|
| Entity->Index | 0:0 | 1:1 | |||
| Index->Entity | 0:0 | 1:1 | |||
| Size | 2 |
加入C
| Array | 0: A | 1: B | 2: C | 3: | 4: |
|---|---|---|---|---|---|
| Entity->Index | 0:0 | 1:1 | 2:2 | ||
| Index->Entity | 0:0 | 1:1 | 2:2 | ||
| Size | 3 |
加入D
| Array | 0: A | 1: B | 2: C | 3: D | 4: |
|---|---|---|---|---|---|
| Entity->Index | 0:0 | 1:1 | 2:2 | 3:3 | |
| Index->Entity | 0:0 | 1:1 | 2:2 | 3:3 | |
| Size | 4 |
到這裡都很好,陣列保持連續,但接著我們要刪除B,也就是Entity 1,為了保持連續,我們將D覆蓋B的位置然後更新map
刪除B
| Array | 0: A | 1: D | 2: C | 3: | 4: |
|---|---|---|---|---|---|
| Entity->Index | 0:0 | 3:1 | 2:2 | ||
| Index->Entity | 0:0 | 1:3 | 2:2 | ||
| Size | 3 |
接著刪除Entity 3,也就是D
| Array | 0: A | 1: C | 2: | 3: | 4: |
|---|---|---|---|---|---|
| Entity->Index | 0:0 | 2:1 | |||
| Index->Entity | 0:0 | 1:2 | |||
| Size | 2 |
然後我們加入E,也就是Entity 4
| Array | 0: A | 1: C | 2: E | 3: | 4: |
|---|---|---|---|---|---|
| Entity->Index | 0:0 | 2:1 | 4:2 | ||
| Index->Entity | 0:0 | 1:2 | 2:4 | ||
| Size | 3 |
這樣陣列就保持連續了!!!
class IComponentArray {
public:
virtual ~IComponentArray() = default;
virtual void entityDestroyed(Entity entity) = 0;
};
template<typename T>
class ComponentArray: public IComponentArray {
public:
void insertData(Entity entity, T component) {
// assert(EntityToIndex.find(entity) == EntityToIndex.end() && "Components added to the same Entity more than once");
size_t newIndex = size_;
EntityToIndex[entity] = newIndex;
IndexToEntity[newIndex] = entity;
componentArray_[newIndex] = component;
++size_;
}
void removeData(Entity entity) {
// assert(EntityToIndex.find(entity) != EntityToIndex.end() && "Entity does not exist");
size_t remove = EntityToIndex[entity];
size_t lastElement = size_-1;
componentArray_[remove] = componentArray_[lastElement];
Entity lastEntity = IndexToEntity[lastElement];
EntityToIndex[lastEntity] = remove;
IndexToEntity[remove] = lastEntity;
EntityToIndex.erase(entity);
IndexToEntity.erase(lastElement);
--size_;
}
T& getComponent(Entity entity) {
// assert(EntityToIndex.find(entity) != EntityToIndex.end() && "retrieving none exist component");
return componentArray_[EntityToIndex[entity]];
}
void entityDestroyed(Entity entity) override {
if(EntityToIndex.find(entity) != EntityToIndex.end())
removeData(entity);
}
private:
std::array<T, MAX_ENTITIES> componentArray_;
std::unordered_map<Entity, size_t> EntityToIndex;
std::unordered_map<size_t, Entity> IndexToEntity;
size_t size_ = 0;
};
這層抽象層是必要的,之後我們會有很多很多的Component array,並且用一個list存著他們,每當有Entity被銷毀時,我們必須逐一地去通知他們有Entity被銷毀了。能讓我們存各個不同type的Component的辦法就是有一層抽象層了。
當然一定有不用抽象層的方法,但這裡只是簡單實作,而且EntityDestroyed()也不是每個frame都會被呼叫。
Component Manager是負責管理所有的Component Array的,並不是讓他們之間溝通,而是通知他們我註冊了哪些Component,或是該刪除哪些Component
class ComponentManager {
public:
template<typename T>
void registerComponent() {
const char* typeName = typeid(T).name();
assert(componentTypes_.find(typeName) == componentTypes_.end() && "Registering component type more than once");
componentTypes_.insert({typeName, nextComponentType_});
componentArray_.insert({typeName, std::make_shared<ComponentArray<T>>()});
++nextComponentType_;
}
template<typename T>
ComponentType getComponentType() {
const char* typeName = typeid(T).name();
assert(componentTypes_.find(typeName) != componentTypes_.end() && "Component did not register");
return componentTypes_[typeName];
}
template<typename T>
void addComponent(Entity entity, T component) {
getComponentArray<T>()->insertData(entity, component);
}
template<typename T>
void removeComponent(Entity entity) {
getComponentArray<T>()->removeData(entity);
}
template<typename T>
T& getComponent(Entity entity) {
return getComponentArray<T>()->getComponent(entity);
}
void entityDestroyed(Entity entity) {
for(auto const& pair: componentArray_) {
auto const& component = pair.second;
component->entityDestroyed(entity);
}
}
private:
std::unordered_map<const char*, ComponentType> componentTypes_;
std::unordered_map<const char*, std::shared_ptr<IComponentArray>> componentArray_;
ComponentType nextComponentType_ = 0;
template<typename T>
std::shared_ptr<ComponentArray<T>> getComponentArray() {
const char* typeName = typeid(T).name();
assert(componentTypes_.find(typeName) != componentTypes_.end() && "Component did not register");
return std::static_pointer_cast<ComponentArray<T>>(componentArray_[typeName]);
}
};
System是行為存在的地方。
每個System都放著存著Entity的list
class System {
public:
std::set<Entity> Entities_;
};
你說行為呢?
當我們需要甚麼系統就繼承它
class PhysicSystem : public System {
public:
void update(float dt);
};
class GraphicSystem : public System {
public:
void update();
void render(sf::RenderTarget& target);
};
一個System的upadte一般長這樣
void PhysicSystem::update(float dt) {
for(auto const& entity : Entities_) {
auto& transform = ecs.getComponent<Transform>(entity);
auto& rigidBody = ecs.getComponent<RigidBody>(entity);
transform.y_ += rigidBody.v_ * dt;
rigidBody.v_ += 10 * dt;
}
}
System Manager是負責管理所有的System及他們的Signature。
每個System的Signature代表此System會用到的Component,這樣才能將合適的Entity分配給它。
當Entity被銷毀,System的list也得跟著更新。
class SystemManager {
public:
template<typename T>
std::shared_ptr<T> registerSystem() {
const char* typeName = typeid(T).name();
assert(system_.find(typeName) == system_.end() && "Registering system more than once");
auto system = std::make_shared<T>();
system_.insert({typeName, system});
return system;
}
template<typename T>
void setSignature(Signature signature) {
const char* typeName = typeid(T).name();
assert(system_.find(typeName) != system_.end() && "System did not register");
signatures_.insert({typeName, signature});
}
void entityDestroyed(Entity entity) {
for(auto const& pair : system_) {
auto const& system = pair.second;
system->Entities_.erase(entity);
}
}
void entitySignatureChanged(Entity entity, Signature entitySignature) {
for(auto const& pair: system_){
auto const& type = pair.first;
auto const& system = pair.second;
auto const& systemSignature = signatures_[type];
if((entitySignature & systemSignature) == systemSignature) system->Entities_.insert(entity);
else system->Entities_.erase(entity);
}
}
private:
std::unordered_map<const char*, Signature> signatures_;
std::unordered_map<const char*, std::shared_ptr<System>> system_;
};
現在我們大致上的功能都有了,我們有管理Entity的Entity Manager。
管理Component的Component Manager。管理System的System Manager。
這三個Manager必須互相溝通。
有很多種辦法 來處理這個問題,設定成gloabal之類的,但我這裡打算寫個class把它包起來
class ECS {
public:
ECS() = default;
void init();
Entity createEntity();
void destroyEntity(Entity entity);
template<typename T>
void registerComponent() {
componentManager_->registerComponent<T>();
}
template<typename T>
void addComponent(Entity entity, T component) {
componentManager_ ->addComponent<T>(entity, component);
auto signature = entityManager_->getSignature(entity);
signature.set(componentManager_->getComponentType<T>(), true);
entityManager_->setSignature(entity, signature);
systemManager_->entitySignatureChanged(entity, signature);\
}
template<typename T>
void removeComponent(Entity entity){
componentManager_->removeComponent<T>(entity);
auto signature = entityManager_->getSignature(entity);
signature.set(componentManager_->getComponentType<T>(), false);
entityManager_->setSignature(entity, signature);
systemManager_->entitySignatureChanged(entity, signature);
}
template<typename T>
T& getComponent(Entity entity) {
return componentManager_->getComponent<T>(entity);
}
template<typename T>
ComponentType getComponentType() {
return componentManager_->getComponentType<T>();
}
template<typename T>
std::shared_ptr<T> registerSystem() {
return systemManager_->registerSystem<T>();
}
template<typename T>
void setSystemSignature(Signature signature) {
systemManager_->setSignature<T>(signature);
}
private:
std::unique_ptr<EntityManager> entityManager_;
std::unique_ptr<ComponentManager> componentManager_;
std::unique_ptr<SystemManager> systemManager_;
};
void ECS::init() {
entityManager_ = std::make_unique<EntityManager>();
componentManager_ = std::make_unique<ComponentManager>();
systemManager_ = std::make_unique<SystemManager>();
std::cout << "ECS init\n";
}
Entity ECS::createEntity() {
return entityManager_->createEntity();
}
void ECS::destroyEntity(Entity entity) {
entityManager_->destroyEntity(entity);
componentManager_->entityDestroyed(entity);
systemManager_->entityDestroyed(entity);
}
一樣,為了讓成千上萬個沙奈朵掉下來,我們一樣有三個Component
struct Transform{
public:
float x_;
float y_;
};
struct RigidBody{
public:
float v_;
};
struct Graphic{
public:
Gardevoir *gardevoir_;
sf::Texture texture;
sf::Sprite sprite_;
};
Gardevoir的用處在Component Pattern有提到
然後我們要來寫我們的System
我們的Graphic System
class GraphicSystem : public System {
public:
void update();
void setSprite(Entity entity);
void render(sf::RenderTarget& target);
};
void GraphicSystem::update() {
for(auto const& entity: Entities_) {
auto& graphic = ecs.getComponent<Graphic>(entity);
auto& transform = ecs.getComponent<Transform>(entity);
graphic.sprite_.setPosition(transform.x_, transform.y_);
}
}
void GraphicSystem::render(sf::RenderTarget& target) {
for(auto const& entity : Entities_) {
auto& graphic = ecs.getComponent<Graphic>(entity);
target.draw(graphic.sprite_);
}
}
void GraphicSystem::setSprite(Entity entity) {
auto& graphic = ecs.getComponent<Graphic>(entity);
graphic.sprite_.setTexture(graphic.gardevoir_->texture);
graphic.sprite_.setScale(0.1, 0.1);
}
PhysicSystem
class PhysicSystem : public System {
public:
void update(float dt);
};
void PhysicSystem::update(float dt) {
for(auto const& entity : Entities_) {
auto& transform = ecs.getComponent<Transform>(entity);
auto& rigidBody = ecs.getComponent<RigidBody>(entity);
transform.y_ += rigidBody.v_ * dt;
rigidBody.v_ += 10 * dt;
}
}
接著我們的主程式我分開來講
首先我們初始化我們的ECS,並註冊我們的Component
ECS::ECS ecs;
ecs.init();
ecs.registerComponent<ECS::Transform>();
ecs.registerComponent<ECS::RigidBody>();
ecs.registerComponent<ECS::Graphic>();
接著我們註冊我們的System,記得設定Signature
auto graphicSystem = ecs.registerSystem<ECS::GraphicSystem>();
ECS::Signature signature;
signature.set(ecs.getComponentType<ECS::Graphic>());
signature.set(ecs.getComponentType<ECS::Transform>());
ecs.setSystemSignature<ECS::GraphicSystem>(signature);
auto physicSystem = ecs.registerSystem<ECS::PhysicSystem>();
signature.set(ecs.getComponentType<ECS::Transform>());
signature.set(ecs.getComponentType<ECS::RigidBody>());
ecs.setSystemSignature<ECS::PhysicSystem>(signature);
接著就可以開始產生我們的Entity了
std::vector<ECS::Entity> entites(ECS::MAX_ENTITIES);
for(auto& entity: entites) {
entity = ecs.createEntity();
ecs.addComponent(
entity,
ECS::Transform{randPositionX(generator), randPositionY(generator), p}
);
ecs.addComponent(
entity,
ECS::RigidBody{20}
);
ecs.addComponent(
entity,
ECS::Graphic{gardevoir}
);
}
for(auto& entity: graphicSystem->Entities_) {
graphicSystem->setSprite(entity);
}
這裡利用我們的Graphic System幫我們設定圖片
接著就是Game Loop了
while (running) {
auto startTime = std::chrono::high_resolution_clock::now();
sf::Event event;
while (window.pollEvent(event))
{
// Close window: exit
if (event.type == sf::Event::Closed)
running = false;
}
physicSystem->update(dt);
graphicSystem->update();
window.clear(bgColor);
graphicSystem->render(window);
// window.draw(s2);
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";
}
這樣就大功告成啦~~~
出來的效果應該跟昨天的一喔。