Skip to main content
13 of 14
added 849 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/free 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.

The real version actually stores the 48-bit link indices in parallel instead of directly into the same container as the component instances as a form of hot/cold field splitting (the links aren't traversed that often so much as traversing components of a particular type where it helps to reduce the stride), but I was too lazy to draw that into the diagram and that's a micro-optimization you can apply later.

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 (auto comp: components)
        {
            auto ptr = dynamic_cast<T*>(comp);
            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 = ecs.comp_maps[idx].find(this->entity_index);
    return (it != ecs.comp_maps[idx].end()) ? dynamic_cast<T*>(*it): nullptr;
}

To avoid the dynamic allocation per component, you can abstract at the hash map level like this as a very crude example:

class ComponentMap
{
public:
    virtual ~ComponentMap() {}
    virtual BaseComponent* find(int n) = 0;
};

template <class ComponentType>
struct ComponentMapT: public ComponentMap
{
    virtual BaseComponent* find(int n) override
    {
         auto it = comps.find(n);
         return (it != comps.end()) ? &*it: nullptr;
    }
    std::hash_map<ComponentType> comps;
};

Then you can store a polymorphic list of ComponentMap which actually stores each component subtype instance by value instead of storing BaseComponent* with dynamic allocations per component instance.

I suspect this solution can be quite performant if you do it this way. Typically your most critical execution paths in an ECS involve plowing through all available components in a scene of a particular type, like your physics system might want to plow through all the motion components in the system to transform, or your rendering system might want to plow through all the sprites (in 2D) or meshes/model (in 3D) in the system to render (or if a spatial index is involved, some system might want to plow through those components to update the spatial index only for your rendering system to then use the spatial index to render the appropriate components in the screen/frustum). This solution allows that to be done by just sequentially iterating through the hash map (which tends to store elements contiguously) storing all components of a given type without any kind of traversal from entity to an associated component.

user321630