3
\$\begingroup\$

This is a simple implementation of an Entity Component System manager. Components are stored in arrays of uint8_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);
}
\$\endgroup\$
3
  • \$\begingroup\$ Use char rather than uint8_t. There are special case rules for char that allow for more flexable usage of its storage. \$\endgroup\$ Mar 29 at 5:50
  • \$\begingroup\$ Do you have a source for this? I'd like to read more. \$\endgroup\$ Mar 29 at 6:05
  • \$\begingroup\$ @MartinYork, I think doing static_assert should be enough to guarantee safety. If OP wants byte like functionality, then it is better to use std::byte. \$\endgroup\$ Mar 29 at 18:08

1 Answer 1

2
\$\begingroup\$

Here are some things that may help you improve your code.

Use appropriate #includes

Since this is C++, you should use the standard std::uint8_t and #include <cstdint>.

Be careful with signed vs. unsigned

The loop counter offset within Pool::AddComponent() is declared as int but it's compared with m_componentSize which is of type std::size_t which is unsigned. I'd suggest either changing this to unsigned 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 in CreateEntity. (Hint: where is m_commponentPools being initialized?)

Consider writing a custom allocator

The managing of memory by using using a pool is exactly the sort of thing that a custom allocator could do. See std::allocator for inspiration.

\$\endgroup\$

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.