This is a simple implementation of an Entity Component System manager. Components are stored in arrays of uint8_t
s, because I couldn't figure out how to just have arrays of the raw components.
// ecs.hpp
#include <unordered_map>
#include <vector>
namespace ecs {
class Pool;
class Registry;
using ComponentID = std::size_t;
using ComponentList = std::vector<ComponentID>;
using Entity = std::size_t;
// Provides interface for accessing a byte array representing a number of component instances
class Pool {
friend class Registry;
private:
std::size_t m_componentSize; // Size of each instance of component
std::vector<uint8_t> m_data; // Contiguous array of uint8_t which make up the individual bytes of all the components in the pool
std::vector<std::size_t> m_deleted; // list of indices into m_data where (m_deleted[n] -> m_deleted[n] + componentSize) is freed component memory
public:
// Create a pool to handle components of a certain componentSize
// Reserves (componentSize * count) number of bytes for internal buffer
Pool(std::size_t componentSize, std::size_t count);
// Add component to pool
// Will reuse deleted components
std::size_t AddComponent();
// Free component from pool
// Component slot will be able to be reused
void DeleteComponent(std::size_t index);
};
// Provides interface for managing components and entities
class Registry {
private:
std::size_t m_incrementingEntityID = 0; // Incrementer used to set Entity IDs
std::vector<Pool> m_componentPools; // Vector of component pools
std::vector<std::size_t> m_deletedComponents; // list of indices into componentPools where a component is marked for reuse
std::unordered_map<Entity, std::unordered_map<ComponentID, std::size_t>> m_entityComponentList; // Provides interface for seeing what components an entity has, and where those components are in memory
public:
// Create an entity and add components to the necesarry pools
Entity CreateEntity(ComponentList components) {
Entity entity = m_incrementingEntityID++;
// Add new entity to the entityComponentList
m_entityComponentList.insert({ entity, {} });
// Generate new components for this entity and add them to the entity's component list (both the ComponentID and the index into the component pool where that entity's instance of the comopnent is)
for (ComponentID componentID : components) {
m_entityComponentList[entity].insert({ componentID, m_componentPools[componentID].AddComponent() });
}
return entity;
}
// Deletes an entity and frees all the components it is associated with
void DeleteEntity(Entity entity) {
for (auto temp : m_entityComponentList[entity]) {
m_componentPools[temp.first].DeleteComponent(temp.second);
}
m_entityComponentList.erase(entity);
}
// Inserts new component pool into the component pool vector
// Will reuse the ComponentID of deleted components
template <typename Component>
ComponentID CreateComponent(std::size_t count = 0) {
std::size_t index;
if (m_deletedComponents.size() > 0) { // Construct new pool in place of previously deleted pool
index = m_deletedComponents.back();
m_deletedComponents.pop_back();
m_componentPools[index] = Pool(sizeof(Component), count);
}
else { // Construct a new pool and add it to the component pool vector
m_componentPools.emplace_back(sizeof(Component), count);
index = m_componentPools.size() - 1;
}
return index;
}
// Mark a component for reuse
void DeleteComponent(ComponentID componentID) {
m_deletedComponents.push_back(componentID);
}
// Return a list of all instances of the given component
template <typename Component>
Component* GetComponents(ComponentID componentID) {
return reinterpret_cast<Component*>(m_componentPools[componentID].m_data.data());
}
// Return a component for a given entity
template <typename Component>
Component& GetEntityComponent(Entity entity, ComponentID componentID) {
return *reinterpret_cast<Component*>(m_componentPools[componentID].m_data.data() + m_entityComponentList[entity][componentID]);
}
};
}
// ecs.cpp
#include "ecs.hpp"
ecs::Pool::Pool(std::size_t componentSize, std::size_t count) :
m_componentSize{ componentSize }
{
m_data.reserve(componentSize * count);
};
std::size_t ecs::Pool::AddComponent() {
std::size_t index;
if (m_deleted.size() > 0) { // Reuse an old component instance's memory to create new component instance
index = m_deleted.back();
m_deleted.pop_back();
// Intialize the component's byte values to zero
for (int offset = 0; offset < m_componentSize; offset++)
m_data[index + offset] = 0;
return index;
}
else { // Create new component instance
index = m_data.size();
// Append an array of zero bytes to the component pool, the array being equal in size to a single component
std::vector<uint8_t> temp(m_componentSize, 0);
m_data.insert(m_data.begin() + index, temp.begin(), temp.end());
return index;
}
}
void ecs::Pool::DeleteComponent(std::size_t index) {
m_deleted.push_back(index);
}
char
rather thanuint8_t
. There are special case rules for char that allow for more flexable usage of its storage. \$\endgroup\$static_assert
should be enough to guarantee safety. If OP wants byte like functionality, then it is better to usestd::byte
. \$\endgroup\$