Take the 2-minute tour ×
Code Review Stack Exchange is a question and answer site for peer programmer code reviews. It's 100% free, no registration required.

I have just recently completed an ECS framework that I've been working for some time. It's supposed to provide easy use of ECS architecture concepts with as little syntax as possible.

The framework uses static functions and static storage for storing systems and components. It also allows efficient mapping of types to integers and easy/fast management via the use of templates. Is this kind of static usage tolerated or does it hint to bad design? I found the alternative of passing objects to constructors too bloated for my taste and it leaked implementation details to the interface.

For instance here is the entity class which is a simple wrapper that signals different internal functions:

#ifndef VERITAS_ENTITY_H
#define VERITAS_ENTITY_H

#include <vector>
#include <cstddef>
#include <utility>
#include <veritas/component_manager.hpp>
#include <veritas/system_manager.hpp>

namespace veritas
{
    struct entity 
    {
        template <typename Component, typename... Args>
        void add_component(Args&&... args)
        {
            component_manager::add_component<Component>(id, std::forward<Args>(args)...);
            system_manager::emit_component_added(id);
        }

        template <typename Component>
        Component& get_component()
        {
            return component_manager::get_component<Component>(id);
        }

        template <typename Component>
        void remove_component()
        {
            component_manager::remove_component<Component>(id);
            system_manager::emit_component_removed(id);
        }

        std::size_t id;
    };
}

#endif

Here is the internal component_manager class:

#ifndef VERITAS_COMPONENT_MANAGER_H
#define VERITAS_COMPONENT_MANAGER_H

#include <vector>
#include <memory>
#include <utility>
#include <veritas/component.hpp>

namespace veritas
{
    struct component_manager
    {   
        struct component_data_untyped
        {
            virtual ~component_data_untyped() {}
            std::vector<std::size_t> component_positions;
            std::vector<std::size_t> holes;
        };

        template <typename Component>
        struct component_data_typed : public component_data_untyped
        {
            std::vector<Component> components;
        };

        template <typename... Components>
        struct has_components_helper;

        template <typename Component, typename... Args>
        static void add_component(std::size_t entity_id, Args&&... args)
        {
            const std::size_t type_id = component<Component>::get_id();

            if (type_id >= component_data_table.size()) 
                component_data_table.emplace_back(new component_data_typed<Component>());

            auto& components = static_cast<component_data_typed<Component>&>(*component_data_table[type_id]).components;
            auto& component_positions = component_data_table[type_id]->component_positions;
            auto& holes = component_data_table[type_id]->holes;

            if (entity_id >= component_positions.size())
                component_positions.resize(entity_id + 1, -1);

            if (holes.size())
            {
                component_positions[entity_id] = holes.back();
                holes.pop_back();

                components[component_positions[entity_id]].~Component();
                new (components.data() + component_positions[entity_id]) Component(std::forward<Args>(args)...);
                return;
            }
            else
            {
                component_positions[entity_id] = components.size();
                components.emplace_back(std::forward<Args>(args)...);
            }
        }

        template <typename Component>
        static Component& get_component(std::size_t entity_id)
        {
            const std::size_t type_id = component<Component>::get_id();

            auto& components = static_cast<component_data_typed<Component>&>(*component_data_table[type_id]).components;
            auto& component_positions = component_data_table[type_id]->component_positions;

            return components[component_positions[entity_id]];
        }

        static void remove_component(std::size_t entity_id, std::size_t type_id)
        {
            auto& component_positions = component_data_table[type_id]->component_positions;
            auto& holes = component_data_table[type_id]->holes;

            if (entity_id < component_positions.size())
            {
                    holes.push_back(component_positions[entity_id]);
                    component_positions[entity_id] = -1;
            }
        }

        template <typename Component>
        static void remove_component(std::size_t entity_id)
        {
            const std::size_t type_id = component<Component>::get_id();

            remove_component(entity_id, type_id);
        }

        static void remove_all_components(std::size_t entity_id)
        {
            for (std::size_t type_id = 0; type_id < component_data_table.size(); ++type_id)
            {
                remove_component(entity_id, type_id);
            }
        }

        template <typename... Components>
        static bool has_components(std::size_t entity_id)
        {
            return has_components_helper<Components...>::has_components(entity_id);
        }

        static std::vector<std::unique_ptr<component_data_untyped>> component_data_table;
    };

    template <>
    struct component_manager::has_components_helper<>
    {
        static bool has_components(std::size_t entity_id) 
        {
            return true;
        }
    };

    template <typename Component, typename... Components>
    struct component_manager::has_components_helper<Component, Components...>
    {
        static bool has_components(std::size_t entity_id)
        {
            const std::size_t type_id = component<Component>::get_id();

            return type_id < component_data_table.size() && 
                   entity_id < component_data_table[type_id]->component_positions.size() &&
                   component_data_table[type_id]->component_positions[entity_id] != -1 &&
                   has_components_helper<Components...>::has_components(entity_id);
        }
    };
}

#endif

The component template is a way to map components using zero-based integers. This has the benefit of being able to remove all the components without being aware of the component types:

#ifndef VERITAS_COMPONENT_H
#define VERITAS_COMPONENT_H

namespace veritas
{
    struct component_base
    {
        static std::size_t next_id() 
        {
            static std::size_t id = 0;
            return id++;
        }
    };

    template <typename Component>
    struct component : public component_base
    {
        static std::size_t get_id()
        {
            static std::size_t id = component_base::next_id();
            return id;
        }
    };
}

#endif

Below is the system_manager which takes cares of notifying systems on component creation/deletion and calling the system update methods:

#ifndef VERITAS_SYSTEM_MANAGER_H
#define VERITAS_SYSTEM_MANAGER_H

#include <veritas/system_base.hpp>
#include <memory>
#include <vector>

namespace veritas
{
    class system_manager
    {
    public:
        template <typename System, typename... Args>
        static void add_system(Args&&... args)
        {
            systems.emplace_back(new System(std::forward<Args>(args)...));
        }

        static void emit_component_added(std::size_t entity_id)
        {
            for (auto& system : systems)
            {
                system->handle_component_added(entity_id);
            }
        }

        static void emit_component_removed(std::size_t entity_id)
        {
            for (auto& system : systems)
            {
                system->handle_component_removed(entity_id);
            }
        }

        static void emit_entity_destroyed(std::size_t entity_id)
        {
            for (auto& system : systems)
            {
                system->handle_entity_destroyed(entity_id);
            }
        }

        static void update(float dt)
        {
            cleanup();
            initialization();
            for (auto& system : systems)
            {
                system->update(dt);
            }
        }

    private:    
        static void cleanup()
        {
            for (auto& system : systems)
            {
                system->cleanup();
            }
        }

        static void initialization()
        {
            for (auto& system : systems)
            {
                system->initialization();
            }
        }

        static std::vector<std::unique_ptr<system_base>> systems;
    };
}

#endif

The system_base abstract class:

#ifndef VERITAS_SYSTEM_BASE_H
#define VERITAS_SYSTEM_BASE_H

#include <cstddef>

namespace veritas
{
    struct system_base
    {
        virtual ~system_base() {}
        virtual void handle_component_added(std::size_t) = 0;
        virtual void handle_component_removed(std::size_t) = 0;
        virtual void handle_entity_destroyed(std::size_t) = 0;
        virtual void update(float) = 0;
        virtual void initialization() = 0;
        virtual void cleanup() = 0;
    };
}

#endif

And the template derived class that the users use:

#ifndef VERITAS_SYSTEM_H
#define VERITAS_SYSTEM_H

#include <veritas/system_base.hpp>
#include <veritas/entity.hpp>
#include <veritas/component_manager.hpp>

namespace veritas
{   

    template <typename... Components>
    class system : public system_base
    {
    public:
        virtual ~system {}

        void handle_component_added(std::size_t entity_id) final
        {
            if (entity_id >= entity_positions.size())
                entity_positions.resize(entity_id + 1, -1);

            if (entity_positions[entity_id] == -1 && component_manager::has_components<Components...>(entity_id))
            {
                mark_entity_registration(entity_id);
            }

        }

        void handle_component_removed(std::size_t entity_id) final
        {
            if (entity_id < entity_positions.size() && entity_positions[entity_id] != -1 && 
                !component_manager::has_components<Components...>(entity_id))   
            {   
                mark_entity_unregistration(entity_id);
            }
        }

        void handle_entity_destroyed(std::size_t entity_id) final
        {
            if (entity_id < entity_positions.size() && entity_positions[entity_id] != -1)
            {
                mark_entity_unregistration(entity_id);
            }
        }

        void initialization() final
        {
            for (std::size_t id : to_be_registered)
            {
                register_entity(id);
            }
            to_be_registered.clear();
        }

        void cleanup() final
        {
            for (std::size_t id : to_be_unregistered)
            {
                unregister_entity(id);
            }
            to_be_unregistered.clear();
        }

        virtual void update(float) = 0;

        std::vector<entity> entities;

    private:
        void mark_entity_unregistration(std::size_t entity_id)
        {
            to_be_unregistered.push_back(entity_id);
        }

        void mark_entity_registration(std::size_t entity_id)
        {

            to_be_registered.push_back(entity_id);
        }

        void register_entity(std::size_t entity_id)
        {
            entity_positions[entity_id] = entities.size();
            entities.push_back({entity_id});
        }

        void unregister_entity(std::size_t entity_id)
        {
            entities[entity_positions[entity_id]].id = entities.back().id;
            entity_positions[entities.back().id] = entity_positions[entity_id];
            entity_positions[entity_id] = -1;
            entities.pop_back();
        }

        std::vector<std::size_t> entity_positions;
        std::vector<std::size_t> to_be_registered;
        std::vector<std::size_t> to_be_unregistered;

    };          
}   

#endif

The very simple entity_manager that is responsible for managing the entity ids:

#ifndef VERITAS_ENTITY_MANAGER_H
#define VERITAS_ENTITY_MANAGER_H

#include <vector>
#include <cstddef>
#include <veritas/entity.hpp>
#include <veritas/system_manager.hpp>

namespace veritas
{
    class entity_manager
    {
    public:
        static entity create()
        {
            if (destroyed.size())
            {
                std::size_t id = destroyed.back();
                destroyed.pop_back();
                return {id};
            }

            return {next_id++};
        }

        static void destroy(entity& entity)
        {
            destroyed.push_back(entity.id);
            component_manager::remove_all_components(entity.id);
            system_manager::emit_entity_destroyed(entity.id);
        }

    private:    
        static std::size_t next_id;
        static std::vector<std::size_t> destroyed;
    };
}

#endif

Finally, here is a pseudocode example that should not be reviewed in order to provide some insight on how the framework is used:

#include <veritas/veritas.hpp>

struct collision_component { .. };
struct rendering_component { .. };
struct transform_component { .. };
..

struct collision_system : public veritas::system<collision_component, 
                                                 transform_component>
{
    void update(float dt)
    {
        for (auto& entity : registered_entities)
        {
            auto& collision = entity.get_component<collision_component>();
            auto& transform = entity.get_component<transform_component>();
            ..
        }
    }
};

struct rendering_system : public veritas::system<rendering_component>
{
    void update(float dt)
    {
        for (auto& entity : registered_entities)
        {
            auto& rendering = entity.get_component<registered_entities>();
            ..
        }
    }
};

void create_example_entity1()
{
    veritas::entity entity = veritas::entity_manager::create();
    entity.add_component<transform_component>(..);
    entity.add_component<collision_component>(..);
}

void create_example_entity2()
{
    veritas::entity entity = veritas::entity_manager::create();
    entity.add_component<transform_component>(..);
    entity.add_component<rendering_component>(..);
    entity.add_component<collision_component>(..);
}

void main()
{
    veritas::system_manager::add_system<collision_system>();
    veritas::system_manager::add_system<rendering_system>();
    ..
    create_example_entity1(); //created entity is now registered to the collision_system.
    create_example_entity2(); //created entity is now registered to the collision_system
                                                              //and the rendering_system.
    ..
    ..   veritas::system_manager::update(1/60.f); //inside game loop

    return 0;
}

I know the overall code is not consistent, I don't apply encapsulation and some things would be much better implemented as namespaces and functions instead of classes but these are on my do list, the framework is not ready in this regard but I'm getting there.

Sorry for the code size. Any feedback is much appreciated!

share|improve this question

Your Answer

 
discard

By posting your answer, you agree to the privacy policy and terms of service.

Browse other questions tagged or ask your own question.