I wonder how to implement the fastest version of an entity component system (ECS from now on) in C++.
First off, regarding terminology:
- a Scene is a container for Entities (and Systems in some implementations)
- a Component is a simple data storage (like position, collision box, image to render etc)
- a System performs logic on Components fitting the System's requirements (this could be physics, player input, simple rendering etc.)
- an Entity contains several Components to make up the final behavior
I listed all the designs we came up with below.
1. The "naive" way
The Scene contains all Entities unordered.
As the Systems update, every System has to loop through all Entities and check whether each Entity contains all required Components and then perform the update upon those Entities.
Obviously, this way is not too performant when having a lot of Systems and/or a lot of Entities.
2. Using "bitmask enums" and mapping
Each Component contains a type identifier in the form of a bitmask (e.g. 1u << 5
/ binary [0...]100000
).
Each Entity can then compose all Component's type identifiers (assuming all typeIDs are unique inside the Entity), so it looks something like
1u << 5 | 1u << 3 | 1u << 1
binary [0...]101010
The Scene contains some kind of map where Systems can easily look up fitting Entities:
MovementSystem::update() {
for (auto& kv : parent_scene_) { // kv is pair<TypeID_t, vector<Entity *>>
if (kv.first & (POSITION | VELOCITY))
update_entities(kv.second); // update the whole set of fitting entities
}
}
Pros:
- Faster than the naive way
Cons:
- Systems have to look up appropriate Entities every single time they are updated.
- A bitmask (enum) is limited to a number of bits (32 for
uint32_t
, at least 64 forunsigned long long
) and in some cases you might need more Components than the bitmask allows.
3. Using no Systems
This method is described by Danvil in an answer below.
Pros:
- Gets rid of the bitmask thing completely.
- Likely to be faster than design #2.
Cons:
- Relies on
dynamic_cast
for looking up a component whereas design #2 can directly look up a component and then safelystatic_cast
it.
4. Using spare sets
This method has been described in by skypjack in an answer below. He explained his approach in great detail, so I'd suggest you read his answer.