patterncppMinor
ComponentManager - Entity-Component-System architecture in C++
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
In short, here is how the
```
#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
{
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:
with this:
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
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:
And implement the other methods by calling
I'm also not sure if your
Should be:
I would also rename
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
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.