We start from the basic systems-components-entities approach.
Let's create assemblages (term derived from this article) merely out of information about types of components. It is done dynamically at runtime, just like we would add/remove components to an entity one by one, but let's just name it more precisely since it is only about type information.
Then we construct entities specifying assemblage for every of them. Once we create the entity, its assemblage is immutable which means we cannot directly modify it in place, but still we can obtain existing entity's signature to a local copy (along with contents), make proper changes to it, and create a new entity out of it.
Now for the key concept: whenever an entity is created, it is assigned to an object called assemblage bucket, which means that all entities of the same signature will be in the same container (e.g. in std::vector).
Now the systems just iterate through every bucket of their interest and do their job.
This approach has some advantages:
- components are stored in a few (precisely: number of buckets) contiguous memory chunks - this improves memory friendliness and it is easier to dump whole game state
- systems process components in a linear manner, which means improved cache coherency - bye bye dictionaries and random memory jumps
- creating a new entity is as easy as mapping an assemblage to bucket and pushing back needed components to its vector
- deleting an entity is as easy as one call to std::move to swap the last element with the deleted one, because order doesn't matter at this moment
If we have a lot of entities with completely different signatures, benefits of cache coherency kind of diminish, but I don't think it would happen in most applications.
There is also a problem with pointer invalidation once vectors are reallocated - this could be solved by introducing a structure like:
struct assemblage_bucket {
struct entity_watcher {
assemblage_bucket* owner;
entity_id real_index_in_vector;
};
std::unordered_map<entity_id, std::vector<entity_watcher*>> subscribers;
//...
};
So whenever for some reason in our game logic we want to keep track of a newly created entity, inside the bucket we register an entity_watcher, and once the entity has to be std::move'd during removal, we lookup its watchers and update their real_index_in_vector
to new values. Most of the time this imposes just a single dictionary lookup for every entity deletion.
Are there any more disadvantages to this approach?
Why is the solution nowhere mentioned, despite being pretty obvious?
EDIT: I'm editing the question to "answer the answers", since comments are insufficient.
you lose the dynamic nature of pluggable components, which was created specifically to get away from static class construction.
I don't. Maybe I did not explain it clearly enough:
auto signature = world.get_signature(entity_id); // this would just return entity_id.bucket_owner->bucket_signature or so
signature.add(foo_component);
signature.remove(bar_component);
world.delete_entity(entity_id); // entity_id would hold information about its bucket owner
world.create_entity(signature); // automatically assigns new entity to an existing or a new bucket
It's as simple as just taking existing entity's signature, modifying it and uploading again as a new entity. Pluggable, dynamic nature? Of course. Here I'd like to emphasize that there is only one "assemblage" and one "bucket" class. Buckets are data-driven and created at runtime in an optimal quantity.
you'd need to go through all buckets that might contain a valid target. Without an external data structure, collision detection could be equally as difficult.
Well, this is why we have the aforementioned external data structures. The workaround is as simple as introducing an iterator in System class that detects when to jump to next bucket. The jumping would be purely transparent to the logic.