HiveBrain v1.2.0
Get Started
← Back to all entries
patterncppMinor

ComponentManager - Entity-Component-System architecture in C++

Submitted by: @import:stackexchange-codereview··
0
Viewed 0 times
architecturecomponentmanagersystemcomponententity

Problem

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
#include
#include

typedef std::size_t Entity;

namespace vf
{

namespace details
{
template
struct TypeIndex;

template
struct TypeIndex
{
static std::size_t constexpr value = 0;
};

template
struct TypeIndex
{
static std::size_t constexpr value = TypeIndex::value + 1;
};
}

template
class ComponentManager
{
public:

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

template
Component& getComponent(Entity entity)
{
return *(std::get::value>(m_components[entity]));
}

template
const Component& getComponent(Entity entity) const
{
return *(std::get::value>(m_components[entity]));
}

template
bool hasComponent(Entity entity) const
{

Solution

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

I would replace this:

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


with this:

template 
Component& emplaceComponent(Entity entity, Args... args)
{
    std::get::value>(m_components[entity]) = std::make_shared(std::forward(args)...);
    return getComponent(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 
const std::shared_ptr& getComponentPtr(Entity entity) const
{
    return std::get::value>(m_components[entity]);
}

template 
std::shared_ptr& getComponentPtr(Entity entity)
{
    return std::get::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::value + 1;


Should be:

static std::size_t constexpr value = TypeIndex::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 with std::map. 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.

Code Snippets

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, 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);
}
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]);
}
static std::size_t constexpr value = TypeIndex<Type, Types...>::value + 1;
static std::size_t constexpr value = TypeIndex<AnotherType, Types...>::value + 1;

Context

StackExchange Code Review Q#63599, answer score: 2

Revisions (0)

No revisions yet.