В настоящее время я изучаю архитектуру Entity-Component-System. Проектирование на основе данных как способ противодействия неправильному проектированию ООП и оптимизации извлечения данных как операции, дружественной к процессору.
Я пытаюсь написатьпростая система ECS, которая пытается следовать этим принципам:
- ЦП дружествен к процессам за счет хранения сущностей / компонентов в непрерывном порции памяти и минимизации пропадания кэша
- Не устанавливать верхнюю границу для числа сущностей/ компоненты / системы в системе
Моя базовая реализация на данный момент выглядит следующим образом:
Entity
класс с подсчетом m_ID
члена, увеличиваемого для каждого вновь создаваемого объекта. std::vector<Entity*>
для содержания дочерних объектов. std::vector<bool>
для хранения подписи компонентов, которые использует объект. Я выбрал std::vector<bool>
вместо std::bitset<S>
, поскольку хотел бы иметь возможность добавлять / удалять компоненты во время выполнения и не хочу устанавливать верхнюю границу для размера компонентов, которые есть у сущности, поэтому я не могу знатьво время компиляции, поэтому я решил использовать специализацию std::vector<bool>
, в которой каждый bool
хранится как 1 бит, что экономит место и позволяет расти. std::unordered_map<unsigned int, unsigned int>
, где ключ - это идентификатор компонента, а значение - это индекс в std::vector<unsigned int, std::vector<ComponentBase>>
сопоставлении идентификатора компонента с вектором компонента, находящимся в классе ECSManager
.
Component
класс с уникальным идентификаторомдля каждого типа компонента с использованием шаблонов для генерации уникального Component<T>::ID
для каждого типа компонента.
System
класс, содержащий std::vector<bool>
, отвечающий за хранение подписи тех объектов, над которыми система хочет работать, зависит от того, имеют ли ониподпись достаточного количества компонентов.
Теперь я уверен, что моя реализация не совсем соответствует моим требованиям, например, я не совсем уверен, что она дружественна к ЦП, как должна быть ECS, поскольку я сильно полагаюсь наstd::unordered_map
, который реализован в виде связанного списка. Но, если честно, я совсем не уверен, поэтому я публикую здесь свою реализацию в надежде получить отзыв о том, что хорошо, а что нет, и как я могу улучшить его.
ComponentBase.h
:
#pragma once
#include <atomic>
static std::atomic<unsigned int> s_CurrentId;
class ComponentBase
{
public:
inline static unsigned GetID() { return ++s_CurrentId; }
};
Component.h
:
#pragma once
#include "ComponentBase.h"
template <typename T>
class Component : public ComponentBase
{
public:
static const unsigned int ID = ComponentBase::GetID();
};
Entity.h
:
#pragma once
#include <vector>
#include <unordered_map>
class Entity
{
private:
unsigned int m_Id;
std::vector<Entity*> m_Children;
std::vector<bool> m_Signature;
std::unordered_map<unsigned int, unsigned int> m_ComponentsIndex;
protected:
Entity();
friend class ComponentManager;
friend class Scene;
public:
Entity(const Entity& other) = delete;
virtual ~Entity();
Entity& operator=(const Entity& other) = delete;
inline unsigned int GetID() { return m_Id; }
void AddChild(Entity* entity);
inline const std::vector<Entity*>& GetChildren() const { return m_Children; }
void AddComponent(unsigned int componentID, unsigned int index);
void RemoveComponent(unsigned int componentID);
inline const std::vector<bool>& GetSignature() const { return m_Signature; }
inline const std::unordered_map<unsigned int, unsigned int>& GetComponentsIndex() const { return m_ComponentsIndex; }
};
Entity.cpp
:
#include "Entity.h"
#include <atomic>
static std::atomic<unsigned int> s_CurrentId;
Entity::Entity()
: m_Id(s_CurrentId++)
{
}
Entity::~Entity()
{
for (const auto& child : m_Children)
delete child;
}
void Entity::AddChild(Entity* entity)
{
m_Children.push_back(entity);
}
void Entity::AddComponent(unsigned int componentID, unsigned int index)
{
m_Signature[componentID] = true;
m_ComponentsIndex[componentID] = index;
}
void Entity::RemoveComponent(unsigned int componentID)
{
m_Signature[componentID] = false;
m_ComponentsIndex.erase(componentID);
}
System.h
:
#pragma once
#include <vector>
#include "Entity.h"
class System
{
private:
std::vector<bool> m_ComponentIDs;
protected:
System(std::vector<unsigned int>& componentIDs);
public:
virtual ~System();
virtual void Update() = 0;
};
System.cpp
:
#include "System.h"
System::System(std::vector<unsigned int>& componentIDs)
{
for (const auto& componentID : componentIDs)
m_ComponentIDs[componentID] = true;
}
System::~System()
{
}
ECSManager.h
:
#pragma once
#include <vector>
#include <unordered_map>
#include "../Entity.h"
#include "ComponentBase.h"
class ECSManager
{
private:
std::unordered_map<unsigned int, std::vector<ComponentBase>> m_Components;
std::unordered_map<unsigned int, Entity> m_Entities;
private:
ECSManager();
public:
const Entity& CreateEntity()
{
Entity e;
m_Entities.emplace(e.GetID(), std::move(e));
return m_Entities[e.GetID()];
}
template <typename TComponent, typename... Args>
void AddComponent(unsigned int entityId, Args&&... args)
{
unsigned int componentID = TComponent::ID;
if (m_Components[componentID] == m_Components.end())
m_Components[componentID] = std::vector<TComponent>();
m_Components[componentID].push_back(T(std::forward<Args>(args)...));
m_Entities[entityId].AddComponent(componentID, m_Components[componentID].size() - 1);
if (m_Signatures[m_Entities[entityId].GetSignature()] == m_Signatures.end())
m_Signatures[m_Entities[entityId].GetSignature()] = std::vector<unsigned int>();
m_Signatures[m_Entities[entityId].GetSignature()].push_back(entityId);
}
template <typename TComponent>
TComponent& GetComponent(Entity& entity)
{
return m_Components[TComponent::ID][entity.GetComponentsIndex()[TComponent::ID]];
}
template <typename TComponent>
std::vector<TComponent>& GetComponents()
{
unsigned int componentID = TComponent::ID;
return m_Components[componentID];
}
std::vector<Entity&> GetEntities(std::vector<bool> componentIDs)
{
std::vector<Entity&> entities;
for (auto& entity : m_Entities)
{
const auto& entitySignature = entity.second.GetSignature();
if (componentIDs.size() > entitySignature.size())
continue;
bool equal = true;
for (std::size_t i = 0; i != componentIDs.size(); i++)
{
if (componentIDs[i] & entitySignature[i] == 0)
{
equal = false;
break;
}
}
if (!equal)
continue;
entities.push_back(entity.second);
}
return entities;
}
};