1
\$\begingroup\$

I'm attempting to create an entity-component system in C++.

However, I've run into an annoying issue where a component system function inherently involves a lot of ceremony and boilerplate. This is because each function needs to perform a lot of validation before the actual meaningful code can be executed.

Example:

void cmpnt_enemy_damage(cmpntEnemy* cmpnt_enemy, int damage_amount, vec2* force)
{

    // Validation starts here

    cmpntPhys* cmpnt_phys = component_get<cmpntPhys>(cmpnt_enemy.ent);
    if (IS_NULL(cmpnt_phys))
        return;

    cmpntCombat* cmpnt_combat = component_get<cmpntCombat>(cmpnt_enemy.ent);
    if (IS_NULL(cmpnt_combat))
        return;

    cmpntAnimator* cmpnt_animator = component_get<cmpntAnimator>(cmpnt_enemy.ent);
    if (IS_NULL(cmpnt_animator))
        return;


    // Actual function starts here

    cmpnt_animator_set_animation(cmpnt_animator, cmpnt_enemy.hurt_anim);
    cmpnt_phys_impulse(cmpnt_phys, force);
    cmpnt_combat_damage(cmpnt_combat, damage_amount);

}

That's a lot of necessary validation for just 3 lines of code.

I was wondering if this is a common problem and if there are any well known solutions. Or is this just a necessary evil when using an entity-component system?

Thanks.

\$\endgroup\$
1
  • 1
    \$\begingroup\$ Why is this function even invoked on entities that might lack the necessary components? Have you looked into archetype-based solutions, so that sets of entities with compatible components stay tightly-packed for iterating consecutively, rather than interleaved randomly with other incompatible entities? \$\endgroup\$
    – DMGregory
    Aug 13, 2023 at 14:38

2 Answers 2

1
\$\begingroup\$

Expanding on my comment above:

One way to reduce these checks for necessary components is to arrange your data so that the function is only ever invoked on entities that definitely have those components.

This is usually done not so much for simplifying the code, but for eliminating dynamic branches, for better prediction and throughput when processing large sets of similar entities. But it does come with engineering cost of its own.

The way this is commonly implemented is using the concept of archetypes. An archetype is a particular combination of components that an entity can have. So a background image with a Transform and Sprite component would belong to one archetype, while a projectile with a Transform and Sprite and Collider and PhysicsBody component would belong to a different archetype, even though they overlap on two components.

Each archetype is realized as a data structure holding arrays of components for entities that belong to it. A system can then iterate over all entities that have a particular set of components by iterating over the archetypes that have those components, then iterating over all corresponding components for entities belonging to those archetypes. Because you have a densely-packed collection of entities that match your whole system query, you nearly eliminate branch misprediction, and you can sometimes even operate on blocks of consecutive components at a time, where it makes sense to do so.

This adds some overhead for finding/storing the set of archetypes to iterate, and when switching from one archetype to another, but if you're going this route, usually your archetypes number in the dozens while your entities number in the hundreds to thousands, so the savings per-entity outweigh the archetype cost. (If this isn't the case for your game, then this might not be the architecture for you)

I sketch an approach to implementing archetypes in C# in this answer, and some notes on minimizing branches when using archetypes here. Although a C/C++ solution wouldn't be identical, these other answers help make more concrete some of the details I've hand-waved above.

\$\endgroup\$
1
\$\begingroup\$

As DMGregory wrote, the key is to move the responsibility for checking whether or not an entity is relevant for a system out of the system's update function.

Archetypes are one approach to this, but I would like to present a way that can do this without this additional abstraction layer.

  • Each system maintains an own array of component-tuples (structures of pointers to a set of components that belong to the same entity) that are relevant to it.
  • When an entity is created, or when an entity's composition changes, you pass it to a method of each system that checks if that entity has the required components for this system, and then adds/removes the corresponding component tuple in that array.
  • When a system is updated, it iterates over that array of component tuples.

Changes to entities will probably be a lot less common than executing the functions that update them. So by pre-processing the eligibility for various systems on composition changes, you avoid checking them again in every single update even though the composition didn't change. That makes your update methods faster and shorter. You also avoid the overhead of calling the component_get function again and again in the update (which might or might not be expensive, depending on your overall architecture) and replace it with a simple pointer lookup.

\$\endgroup\$
1
  • \$\begingroup\$ This does mean every time we iterate to the next entity, we're chasing pointers and potentially eating a cache miss, right? But if the game doesn't need to iterate thousands of entities, the code simplification of not having archetypes may be worth that cost. \$\endgroup\$
    – DMGregory
    Aug 18, 2023 at 11:42

You must log in to answer this question.

Not the answer you're looking for? Browse other questions tagged .