This is a simple implementation of an Entity Component System manager. Components are stored in arrays ofuint8_ts, 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);}- \$\begingroup\$Use
charrather thanuint8_t. There are special case rules for char that allow for more flexable usage of its storage.\$\endgroup\$Loki Astari– Loki Astari2022-03-29 05:50:04 +00:00CommentedMar 29, 2022 at 5:50 - \$\begingroup\$Do you have a source for this? I'd like to read more.\$\endgroup\$fortytoo– fortytoo2022-03-29 06:05:27 +00:00CommentedMar 29, 2022 at 6:05
- \$\begingroup\$@MartinYork, I think doing
static_assertshould be enough to guarantee safety. If OP wants byte like functionality, then it is better to usestd::byte.\$\endgroup\$Incomputable– Incomputable2022-03-29 18:08:20 +00:00CommentedMar 29, 2022 at 18:08
1 Answer1
Here are some things that may help you improve your code.
Use appropriate#includes
Since this is C++, you should use the standardstd::uint8_t and#include <cstdint>.
Be careful with signed vs. unsigned
The loop counteroffset withinPool::AddComponent() is declared asint but it's compared withm_componentSize which is of typestd::size_t which is unsigned. I'd suggest either changing this tounsigned or better, see the last suggestion.
Provide complete code to reviewers
This is not so much a change to the code as a change in how you present it to other people. Without the full context of the code and an example of how to use it, it takes more effort for other people to understand your code. This affects not only code reviews, but also maintenance of the code in the future, by you or by others. One good way to address that is by the use of comments. Another good technique is to include test code showing how your code is intended to be used.
Rethink the interface
Lacking an example of how the code is intended to be used, here's what tried:
#include "ecs.hpp"#include <iostream>int main() { ecs::Registry reg; auto entity1 = reg.CreateEntity({1, 2, 3}); std::cout << "entity1 = " << entity1 << '\n';}When I run this simple program, it segfaults and crashes because of an error inCreateEntity. (Hint: where ism_commponentPools being initialized?)
Consider writing a custom allocator
The managing of memory by using using a pool is exactly the sort of thing that acustom allocator could do. Seestd::allocator for inspiration.
You mustlog in to answer this question.
Explore related questions
See similar questions with these tags.


