I tried to write a simple entity component system for my game engine. Each scene will have an EntityManager. It will create entities and add or remove components from them. An entity will be destroyed if its isAlive
flag is set to false.
Example usage
class Position : public Component
{
Position(float x, float y, float z)
: x(x)
, y(y)
, z(z)
{}
float x, y, z;
}
EntityManager manager;
Entity& entity = manager.addEntity();
manager.entityAddComponent<Position>(entity, 1.0f, 2.0f, 3.0f);
auto& positions = manager.getComponentPool<Position>();
for (Position& pos : positions)
{
// update
}
manager.removeDeadEntities();
Here is the code.
component id generator
static size_t registeredComponentCount = 0;
template <typename T>
size_t getComponentId()
{
static const size_t id = registeredComponentCount++;
return id;
}
Entity
class Entity
{
friend EntityManager;
template <typename T>
friend class ComponentPool;
public:
static constexpr size_t MAX_COMPONENTS = 32;
private:
Entity(size_t id)
: m_components({ nullptr })
, m_id(id)
, isAlive(true)
{}
Entity(const Entity&) = delete;
Entity& operator= (const Entity&) = delete;
public:
bool isAlive;
private:
std::array<Component*, MAX_COMPONENTS> m_components;
size_t m_id;
};
Component
struct Component
{
friend EntityManager;
template <typename T>
friend class ComponentPool;
public:
template<typename T>
T& getComponent()
{
return reinterpret_cast<T>(m_entity->m_components[getComponentId<T>]);
}
Entity& getEntity()
{
return *m_entity;
}
Entity& getEntity();
private:
Entity* m_entity;
};
EntityManager
class
class EntityManager
{
public:
static constexpr size_t MAX_ENTITES = 1024;
public:
EntityManager();
~EntityManager();
EntityManager(const EntityManager&) = delete;
EntityManager& operator= (const EntityManager&) = delete;
Entity& addEntity();
void removeDeadEntities();
template<typename T, typename ...Args>
void entityAddComponent(Entity& entity, Args&&... args)
{
T* component = getComponentPool<T>().addComponent(std::forward<Args>(args)...);
component->m_entity = &entity;
entity.m_components[getComponentId<T>()] = reinterpret_cast<Component*>(component);
}
template<typename T>
void entityRemoveComponent(Entity& entity)
{
getComponentPool<T>().removeComponent(entity.m_components[getComponentId<T>()]);
}
template<typename T>
ComponentPool<T>& getComponentPool()
{
size_t id = getComponentId<T>();
if (m_initializedComponentPools.test(id) == false)
{
m_componentPools[id] = reinterpret_cast<BaseComponentPool*>(new ComponentPool<T>());
m_initializedComponentPools.set(id, true);
}
return *reinterpret_cast<ComponentPool<T>*>(m_componentPools[id]);
}
private:
UninitializedArray<Entity, MAX_ENTITES> m_entites;
size_t m_entityCount;
size_t m_createdEntityCount;
BaseComponentPool* m_componentPools[Entity::MAX_COMPONENTS];
std::bitset<Entity::MAX_COMPONENTS> m_initializedComponentPools;
};
ComponentPool
class
class BaseComponentPool
{
public:
virtual ~BaseComponentPool() {};
virtual void removeComponent(Component* component) = 0;
};
template<typename T>
class ComponentPool : BaseComponentPool
{
public:
ComponentPool()
: m_size(0)
{}
~ComponentPool() override
{
for (size_t i = 0; i < m_size; i++)
{
m_data[i].~T();
}
}
ComponentPool(const ComponentPool&) = delete;
ComponentPool& operator= (const ComponentPool&) = delete;
template<typename ...Args>
T* addComponent(Args && ...args)
{
T* component = &m_data[m_size];
new (&m_data[m_size]) T(std::forward<Args>(args)...);
m_size++;
return component;
}
void removeComponent(T* component)
{
component->~T();
if (component != &m_data[m_size - 1])
{
new (component) T(std::move(m_data[m_size - 1]));
component->m_entity->m_components[getComponentId<T>()] = component;
}
m_size--;
}
void removeComponent(Component* component) override
{
removeComponent(reinterpret_cast<T*>(component));
}
T* begin()
{
return m_data.data();
}
T* end()
{
return m_data.data() + m_size;
}
const T* cbegin() const
{
return m_data();
}
const T* cend() const
{
return m_data() + m_size;
}
private:
UninitializedArray<T, EntityManager::MAX_ENTITES> m_data;
size_t m_size;
};
System
class
class System
{
public:
virtual ~System() = 0;
virtual void update(Scene& scene) = 0;
};
Helper class UninitializedArray
template <typename T, size_t size>
class UninitializedArray
{
public:
T& operator[](size_t index)
{
return reinterpret_cast<T*>(m_data)[index];
}
T* data()
{
return reinterpret_cast<T*>(m_data);
}
private:
alignas(alignof(T)) unsigned char m_data[sizeof(T) * size];
};
Is there a better way to implement removing components without using virtual function calls?
Does the m_initializedComponentPools
bitset in EntityManager
make sense or would it be better to just set the uninitialized pools to nullptr
?
Would it be better to register what types of components are going to be used so the component pools are initialized before the game loop starts?
Any tips on how to improve the code would be appreciated.
1 Answer 1
Answers to your questions
Is there a better way to implement removing components without using virtual function calls?
You have to have some way to call the right ComponentPool<T>::removeComponent()
corresponding to the component type. With virtual functions, this is handled for you by the language. If you want to do it without a virtual function call, then you'd need some way to store the component type in a BaseComponentPool
and then dispatch the right version of removeComponent()
based on it. It's going to be more work for little to no gain.
Also, there is actually no virtual function call being done in your code: you already explicitly pass the desired type to EntityManager::entityRemoveComponent()
, and then it will call getComponentPool<T>
, which will return a ComponentPool<T> &
, not a BaseComponentPool &
, so it can directly call the right removeComponent()
. You can remove removeComponent()
from BaseComponentPool
and your code should still compile and run fine. You should keep the virtual destructor though.
Does the
m_initializedComponentPools
bitset inEntityManager
make sense or would it be better to just set the uninitialized pools tonullptr
?
I would just set them to nullptr
, or better yet make it an array of std::unique_ptr
s:
std::array<std::unique_ptr<BaseComponentPool>, Entity::MAX_COMPONENTS> m_componentPools;
Would it be better to register what types of components are going to be used so the component pools are initialized before the game loop starts?
If you can do that, yes. Not only allows it to have the pools be initialized beforehand, it also ensures you know exactly the right size of EntityManager::m_componentPools
. You could even consider making m_componentPools
a std::tuple
of component pool types then, so you don't need a virtual base class anymore:
template<typename... Ts>
class EntityManager
{
...
template<typename T>
ComponentPool<T>& getcomponentPool()
{
return std::get<ComponentPool<T>>(m_componentPools);
}
...
std::tuple<ComponentPool<Ts>...> m_componentPools;
}
Simplify entityAddComponent()
Instead of making entityAddComponent()
be a variadic template that takes all the arguments necessary to construct a new component, consider having it take a single parameter for the component, like so:
template<typename T>
void entityAddComponent(Entity& entity, T&& component)
{
T* component = getComponentPool<T>().addComponent(std::forward<T>(component));
...
}
This way, addComponent
can copy or move-construct a new component. The main benefit it that this now allows the compiler to deduce T
automatically, so you can write:
manager.entityAddComponent(entity, Position{1.0f, 2.0f, 3.0f});
This becomes especially helpful if you already have the position in a variable, in which case you can do:
Position position{1.0f, 2.0f, 3.0f};
manager.entityAddComponent(entity, position);
Instead of:
manager.entityAddComponent<Position>(entity, position.x, position.y, position.z);
Of course, this won't work if the component type doesn't have copy or move constructors.
Make IDs const
The ID of an entity is set a construction time and should never change afterwards, so make it const
.
Consider storing entities and components in std::deque
s
The UninitializedArray
is indeed a good way to avoid default-constructing a large number of components, but you still need to specify the maximum number of elements you want it to hold up-front. There is a standard container that you can use instead: std::deque
. It provides fast index-based access, pointers to elements are stable, and it grows as required.
Explore related questions
See similar questions with these tags.