Entity Component System Designs

I wonder how to implement in C ++ the fastest version of the system of entity components (ECS from now on).

First of all, regarding terminology:

  • A scene is a container for entities (and systems in some implementations).
  • The component is a simple data warehouse (e.g. position, collision column, image for rendering, etc.).
  • a The system executes logic for components that meet the requirements of the System (this may be physics, player input, simple rendering, etc.).
  • An Entity Contains Several Components to Compose the Final Behavior

I have listed all the projects that we came up with below.


1. The "naive" way

The scene contains all unordered entities.
As a system update, each system must scroll through all the objects and check whether each Entity contains all the necessary components, and then perform an update for these objects.

Obviously, this method is not very effective when there are a large number of systems and / or many objects.


2. Using "bitmask enumerations" and mapping

Each component contains a type identifier in the form of a bitmask (for example, 1u << 5 / binary [0...]100000 ). Each Entity can then compile all component identifiers of the component (provided that all type identifiers are unique within Entity), so it looks something like this:

 1u << 5 | 1u << 3 | 1u << 1 binary [0...]101010 

The scene contains some kind of map where Systems can easily search for suitable objects:

 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

vs

  • Systems must search for relevant objects every time they are updated.
  • The bitmask (enumeration) is limited by the number of bits (32 for uint32_t , at least 64 for unsigned long long ), and in some cases you may need more components than the bitmask allows.

3. Use without systems

This method is described by Danvil in the answer below .

Pros

  • Completely gets rid of bitmasks.
  • Most likely faster than design # 2.

vs

  • It relies on dynamic_cast to search for a component, while design # 2 can directly search for a component, and then static_cast it static_cast .

4. Use of spare kits

This method is described in skypjack in the answer below . He explained his approach in detail, so I suggest you read his answer.

+6
source share
2 answers

I would say that you call the "System" actually a component. An example for rendering: there is a Pose component (for 3D-rotation of a location) and a Mesh component (contains vertex buffers). Now, instead of a function that checks whether it can display this particular entity, add the Renderer component. This component connects to the Pose and Mesh components. Now the rendering of the "System" should only interact with the Renderer component. And each object is either visualized, or now, there is no need to check the components every time, and all the rendering work is collected as a component.


Code example:

 struct Pose : public Component { float x,y; }; struct Mesh : public Component { std::vector<Vertex> vertices; }; struct Renderer : public Component { Entity* entity; void render() { if(!mesh|| entity->componentsChanged) { mesh = entity->getComponent<Mesh>(); if(!mesh) throw error; } if(!entity->pose) throw error; glTranslate(entity->pose->x, entity->pose->y); ... } private: Mesh* mesh; }; struct Entity { std::vector<Component*> components; bool componentsChanged; template<typename C> C* getComponent() const { for(Component* c : components) { C* cc = dynamic_cast<C>(c); if(cc) return cc; } return NULL; } // "fast links" to important components Pose* pose; Renderer* renderer; PhysicsStuff* physics; }; struct Rendering { private: void render(const std::vector<Entity*>& entities) { for(Entity* e : entities) { if(!e->renderer) continue; e->renderer->render(); } } }; 
0
source

Another approach, which I found quite promising and used in my project (see EnTT on GitHub), is based on sparse sets .
I used them in component pools to keep track of which entity the component is associated with and what its slot is.

Main advantages:

  • You get a small array of all entities that have a specific component for free (see here for more information), and this gives you a significant performance boost in terms of performance when iterating over them.

  • It saves at least the number of actually assigned components. In addition, all components are compact in memory.

  • Cache misses are reduced as a minimum, as you only pay for what you use. In other words, you get only those components that are actually assigned to the entity, and they are all next to each other in memory, without holes.

You get it at the price of an additional array with a maximum length equal to the number of entities for each component pool (note that in real applications these arrays are usually smaller).

Tests have shown that performance is much better than that of the well-known component system based on bitmask descriptors (see the link above for more information). I also verified that the memory load is more or less the same, since you are getting rid of the array of bitmask descriptors, but injecting a set of mini-arrays into the component pools.

Iterating over entity sets when searching for multiple components can also be greatly improved with a trick: find the shortest set and iterate over its entities (very fast operation), then check if the nth entity has other components, and ultimately return it .
Tests have shown that this is still faster than the design based on bit masks for dense sets (where each entity has all the components). In case of sets that are not so dense (which is a reasonable assumption for real software), the performance is definitely better than that of solutions based on a bit mask.

Finally, unlike solution No. 4, in this case, dynamic casting is not required.

All this gives you what I would call an entity component registry. Systems can be defined as lambdas that capture the registry or functors into which you can transfer the registry. There is no need to register systems in the registry itself.


Hope you have an idea for this implementation.
If you need more details, feel free to ask.

+2
source

Source: https://habr.com/ru/post/969465/


All Articles