Skip to main content
9 of 14
added 275 characters in body
user avatar
user avatar

My Way

The way I do it, though this is not for the faint of heart as it involves variable-length structs, reinterpret casts, placement new and manual dtor invocations, and requires some knowledge of proper memory alignment if it is to be compacted as much as possible, is like this:

enter image description here

Each component type is stored in its own contiguous memory block as a VLS. There are no heap/store allocations/deallocations on a per-component instance level (they are stored contiguously in an array that grows, similar to std::vector, and you can use that here, though it needs to behave like a free list with O(1) removals from the middle without invalidating any other indices).

And a pair of indices (type + index into the container for the type) adding up to 48-bits basically links the entities and components associated to that entity together, with -1 indicating the null terminator.

And yes, there is casting going on (in my case a reinterpret cast which is even more ugly than a dynamic cast) to retrieve a component of a particular type from an entity when we write like:

MotionComponent* motion = entity.get<MotionComponent>();

But that is only one get method in the codebase that requires such a cast, and there's runtime checking going on to make sure the cast is legal to mitigate a lot of the usual problems associated with casting pointers.

In my use cases it's not uncommon for various systems to process half a million components per frame, so it's rather tuned for a specific use case. It might be a bit overkill for your needs but it's something that has served me for a long time now if you want to roll up your sleeves and get down and dirty with the bits and bytes.

Your Way

The way you have it now is, if you'll forgive me, about the most inefficient way I can imagine to do it for every kind of use case I can imagine. The problem there is that first you're dynamically-allocating every single component instance. That's often going to result in a loss of spatial locality between components. And then storing a separate container per entity which actually has its own size and capacity and pointer and dynamic allocations is also going to be quite a memory and performance overhead if you have a large, large number of entities. Lastly if you aren't storing components of a particular type contiguously in their own container, then that means when your system wants to fetch a list of components of a particular type (probably the most common query to an ECS), the ECS is going to have to iterate through every single entity in the entire scene that exists checking to see if it has that component.

If that's okay though, then one dynamic_cast in a central place is very understandable to implement ECS. Any time you want a syntax like this:

MotionComponent* motion = entity.get<MotionComponent>();

... that pretty much implies that there's some cast going on somewhere under the hood unless all that information is available statically at compile-time (in my case at least that's not feasible since plugin developers can introduce brand new component types into the software at runtime). The dynamic_cast is at least the safest kind of pointer cast there is and, if you centralize it to one get method as part of the entity interface, then it's not that big of a deal in practice.

As for your particular implementation, if you go with it, I'd seek to add something like this:

class Entity
{
public:
    template <class T>
    T* get() const
    {
        for (size_t j=0; j < components.size(); ++j)
        {
            T* ptr = dynamic_cast<T*>(components[j]);
            if (ptr)
                return ptr;
        }
        return nullptr;
    }

private:
    vector<BaseComponent*> components;
};

That centralizes the dynamic_cast to that one get<T> method in the codebase as opposed to having to litter casts and type checks all over your systems*.

Another Way

Another way I've seen people do, though it could easily be explosive in memory if implemented densely, is like this:

enter image description here

Basically you just think of it like an NxM matrix, where M is the number of entities, and N is the number of component types. You might store some marker or just a null pointer in the grid/matrix cells where no component of a particular type is attached to an entity. That's very straightforward but gets explosive in memory as you can probably quickly see.

A way to optimize the memory use then is to turn this into a sparse representation. For example, you might use a hash map for each column above (each hash table stores instances of a particular component type) which is associated to the row (entity) index as key, for example.

If you use this kind of rep and want this kind of syntax:

MotionComponent* motion = entity.get<MotionComponent>();

Then one straightforward way is to do something like this:

struct MotionComponent
{
    // Indicates which column (hash table, e.g.) to use in the 
    // above table where the motion components are stored.
    enum {type_index = ...};
    ...
};

... in which case you'd do something like:

template <class T>
T* Entity::get() const
{
    enum {idx = T::type_index};
    auto it = hash_maps[idx].find(this->entity_index);
    return (it != hash_maps[idx].end()) ? dynamic_cast<T*>(*it): nullptr;
}
user321630