4
\$\begingroup\$

Lately I've been working on a small C++ Entity-Component-System framework.

Like most other ECS frameworks the internal data is presented as a table where an entity is a simple row index and each Component type maps to a column index.

In contrast to many other C++ ECS frameworks that usually map the Components to the indexes using RTTI, I thought that since Components are known beforehand, it's simply a case of implementing the get/set/has functions at compile time. Much of this functionality is implemented in the std::tuple template.

In short, here is how the ComponentManager class vaguely looks like:

#include <vector>
#include <tuple>
#include <memory>

typedef std::size_t Entity;

namespace vf
{

        namespace details
        {
                template <class... Types>
                struct TypeIndex;

                template <class Type, class... Types>
                struct TypeIndex<Type, Type, Types...>
                {
                        static std::size_t constexpr value = 0;
                };

                template <class Type, class AnotherType, class... Types>
                struct TypeIndex<Type, AnotherType, Types...>
                {
                        static std::size_t constexpr value = TypeIndex<Type, Types...>::value + 1;
                };
        }

        template <class... Components>
        class ComponentManager  
        {
        public:

                template <class Component>
                Component& addComponent(Entity entity, Component* component)
                {
                        std::get<details::TypeIndex<Component, Components...>::value>(m_components[entity]).reset(component);
                        return *component;
                }

                template <class Component>
                Component& getComponent(Entity entity)
                {
                        return *(std::get<details::TypeIndex<Component, Components...>::value>(m_components[entity]));
                }

                template <class Component>
                const Component& getComponent(Entity entity) const
                {
                        return *(std::get<details::TypeIndex<Component, Components...>::value>(m_components[entity]));
                }

                template <class Component>
                bool hasComponent(Entity entity) const
                {
                        return std::get<details::TypeIndex<Component, Components...>::value>(m_components[entity]) != nullptr;
                }

                void addComponentEntry()
                {
                        m_components.push_back(std::tuple<std::unique_ptr<Components>...>());
                }

                void resetComponentEntry(Entity entity)
                {
                        m_components[entity] = std::tuple<std::unique_ptr<Components>...>();
                }

       private:

                std::vector<std::tuple<std::unique_ptr<Components>...>> m_components;
        };


}

The actual framework wiring is implemented in 3 classes (ComponentManager, EntityManager, SystemManager) that are semi-coupled and communicate together. All of the functionality is hidden in a wrapper class. Systems keep a list of Entities according to the components on which they function.

Now one thing that I want to address is caching:

In my current implementation, the table actually keeps pointers to the component objects since:

  1. It makes checking for existence much easier.
  2. It doesn't require that the components are default initialized.

This makes the retrieval of components slower in some cases.

I know that I shouldn't try to optimize before bench-marking but assuming that the results show a noticeable difference (I won't be doing tests any time soon since the framework is by no means ready yet), do you think that it's worth it to introduce a few requirements in the components in order to increase performance for a few admittedly rare situations?

I am also open to any criticism on the ComponentManager template. It doesn't make sense to talk about efficiency if the current implementation needs correction.

\$\endgroup\$
2
\$\begingroup\$

I don't have much time to give right now but I can offer one comment on the API.

I would replace this:

template <class Component>
Component& addComponent(Entity entity, Component* component)
{
    std::get<details::TypeIndex<Component, Components...>::value>(m_components[entity]).reset(component);
    return *component;
}

with this:

template <class Component, typename... Args>
Component& emplaceComponent(Entity entity, Args... args)
{
    std::get<details::TypeIndex<Component, Components...>::value>(m_components[entity]) = std::make_shared<Component>(std::forward<Args>(args)...);
    return getComponent<Component>(entity);
}

because your current api doesn't make it clear about who owns the memory and is susceptible to ownership bugs as you're passing raw pointers. Yet you don't want to expose the implementation detail that is shared_ptr.

Edit: I didn't check that it compiles but shouldn't be too hard to fix if it doesn't. :)

Edit 2

I've had a bit more time to look at this now.

You should factor out the code that retrieves the smart pointer as a private method to reduce code duplication:

private:

template <class Component>
const std::shared_ptr<Component>& getComponentPtr(Entity entity) const
{
    return std::get<details::TypeIndex<Component, Components...>::value>(m_components[entity]);
}

template <class Component>
std::shared_ptr<Component>& getComponentPtr(Entity entity)
{
    return std::get<details::TypeIndex<Component, Components...>::value>(m_components[entity]);
}

And implement the other methods by calling getComponentPtr.

I'm also not sure if your TypeIndex detail is correct. I think (without testing) that:

static std::size_t constexpr value = TypeIndex<Type, Types...>::value + 1;

Should be:

static std::size_t constexpr value = TypeIndex<AnotherType, Types...>::value + 1;

I would also rename resetComponent to clearComponent as reset implies that you will replace the component with another value while clear (or unset) are more clear as to the action taken.

On a more general note your algorithm can never shrink the entity database which might or might not be a problem in your application but it makes your code less generally usable. I would replace the std::vector<tuple> with std::map<Entity, tuple>. The map is implemented as a form of binary tree giving logarithmic lookup and insertion. If this proves too slow you can implement a hash map with amortised constant time insertion and lookup. The integer key means that you don't need a hash function.

\$\endgroup\$
  • \$\begingroup\$ Thanks, this was also something that bothered me. My experience is limited so I wasn't sure which was the /right/ approach. \$\endgroup\$ – Veritas Sep 22 '14 at 21:40
  • \$\begingroup\$ @Veritas updated answer. \$\endgroup\$ – Emily L. Sep 23 '14 at 11:39
  • 1
    \$\begingroup\$ TypeIndex is correct. I use a vector because any removed entities are recycled so I just need efficient random lookup. \$\endgroup\$ – Veritas Sep 23 '14 at 11:53

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.