3
\$\begingroup\$

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);}
Ben A's user avatar
Ben A
10.8k5 gold badges40 silver badges103 bronze badges
askedMar 29, 2022 at 0:41
fortytoo's user avatar
\$\endgroup\$
3
  • \$\begingroup\$Usechar rather thanuint8_t. There are special case rules for char that allow for more flexable usage of its storage.\$\endgroup\$CommentedMar 29, 2022 at 5:50
  • \$\begingroup\$Do you have a source for this? I'd like to read more.\$\endgroup\$CommentedMar 29, 2022 at 6:05
  • \$\begingroup\$@MartinYork, I think doingstatic_assert should be enough to guarantee safety. If OP wants byte like functionality, then it is better to usestd::byte.\$\endgroup\$CommentedMar 29, 2022 at 18:08

1 Answer1

2
\$\begingroup\$

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.

answeredMar 29, 2022 at 13:34
Edward's user avatar
\$\endgroup\$

You mustlog in to answer this question.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.