2
\$\begingroup\$

I'm developing a 2D roguelike with an Entity Component System (ECS) and I've been struggling with this question a lot the past week. For example, let's say you have various effects that occur when an entity spawns, dies, collides with another entity, etc. Where should these effects be coded? I see two main possibilities:

  1. In the entity itself. Example pseudocode:

    let effectsComponent = new EffectsComponent();
    
    effectsComponent.onHit = [ damage(10), deleteSelf() ];
    effectsComponent.onDeath = [ playSound("player-death.mp3") ];
    
    // Effects system
    for (let effect of ...
    
  2. In a system. Example pseudocode:

    // Damage system
    for (let event of scene.events.query<CollisionEvent>()) {
      if (event.entity1 is bullet and event.entity2 is player) { 
        event.entity2.damage(10);
        event.entity1.delete();
      }
    }
    
    // Sound system
    for (let event of scene.events.query<DeathEvent>()) {
      switch (event.entity.type) {
        case ET.Player: playSound("player-death.mp3"); break;
      }
    }
    

I can think of numerous pros and cons for each approach. The first approach can likely cut down on some conditionals because since we are defining it on the entity itself, we already know its "type" already, so the only thing we would have to worry about is the type of the receiving entity. As such, it's likely more terse to code in the first manner.

However, the second approach might be more "proper" within the context of an ECS. That is, you could argue that it is not the responsibility of an entity to describe its behavior. Components should only hold state, and not behavior, and it's a short road to spaghetti when you begin mixing in behavior and state within an entity definition.

In addition, many events may not even have a receiver or target entity, so regardless in some situations we'll have encode it into the system's functionality regardless. And if that is the case, then why not just be consistent and always make systems handle this sort of thing?

Lastly, philosophically, it kind of feels like systems should be responsible for handling the interaction response of two entities. As an example, with the first approach, should the player entity or the bullet entity encode the behavior response for when they collide with each other? It's unclear. Then again, it also feels like entities should describe their data, and things like sound effects should likely be encoded within an entity component instead of being delineated within a system.

In other words, I'm very unsure of how to handle this. What is considered better design here? Should components be responsible for holding this interaction / event data, or should systems?

\$\endgroup\$
4
  • \$\begingroup\$ Not a direct answer to your question, but I think you are missing some of the concepts of ECS. This answer here is what helped me learn them, so it may help you as well. gamedev.stackexchange.com/a/31491/41345 \$\endgroup\$
    – Evorlor
    Jun 17 at 14:21
  • 1
    \$\begingroup\$ Option 2 looks like it could grow into an absolute mess of if checks for various conditions or sprawling switch statements as your game evolves. Is there any way you could direct your events into separate queues for each handler, so that the handler can process just the relevant events consecutively, without a lot of case selection in the middle of that loop? \$\endgroup\$
    – DMGregory
    Jun 17 at 14:29
  • \$\begingroup\$ @DMGregory Ahh, now I'm really unsure, as Philip's answer basically says "go with #2" and your comment is saying #2 could be a mess. How would I direct my events? For example, my CollisionSystem simply detects whether or not there is a collision between two entities. Are you saying it should also determine what sorts of entities they are and then create events other than just CollisionEvents? \$\endgroup\$ Jun 17 at 14:58
  • 1
    \$\begingroup\$ I read Philipp's answer as saying "keep executable code out of the component". So the component wouldn't directly contain a lambda or function pointer for example. But that doesn't preclude the component from containing data that informs how to handle its events. Say you have a "Collision Tag" that you can apply to tag a Collider component. The collision system can then place the collision event into a distinct queue depending on the tag(s) on the colliders involved. Then your DamageCollisionSystem needs only process the collisions in the DamageCollision tag queue, without all the branching. \$\endgroup\$
    – DMGregory
    Jun 17 at 15:23
2
\$\begingroup\$

All ECS architectures run into the problem of event processing eventually. Event processing does not fit neatly into the ECS philosophy, but few moderately complex games can really do without it. So any solution will necessarily feel not entirely ECS-kosher at first.

Approaches I have seen are:

  • Attach events as transient components to the entities they belong to, and remove said components after the event was processed. The feasibility of this solution greatly depends on how costly it is in your architecture to add and remove components. If your architecture does not allow duplicate components on entities, then there is also the drawback that each event can only happen once per update to each entity.
  • Add the possibility for systems to have event queues. When an event happens, you put it into the event queue of the system which handles the event. The system then processes its event queue in its update.
  • If you are using event queues anyway, you can also do them properly. With one global EventManager system which receives all events. Systems can subscribe to events they are interested in and then the EventManager will put those events into their event queues.

But in any case, you should still follow the ECS philosophy that data belongs into components and code into systems. So what actually happens as part of an event should be handled by a system. Components should not contain variables which map to functions or lambda expressions.

\$\endgroup\$
8
  • \$\begingroup\$ Right, I'm using an archetype-based ECS (so iteration is cheap but adding/removing components is expensive), so I have already created an event queue for my project. I think I may have miscommunicated, as I'm not really asking about whether to use an event queue vs components as events, but rather whether to store event handlers within components or within systems. \$\endgroup\$ Jun 17 at 14:13
  • \$\begingroup\$ @RyanPeschel Ah, I see. Answer updated. ECS should not have any executable code in components, so the code which gets executed on an event should be part of a system. Yes, I know that JavaScript makes it really easy to attach small snippets of code to objects in form of lambda expressions. But that's not how ECS is supposed to work. \$\endgroup\$
    – Philipp
    Jun 17 at 14:15
  • \$\begingroup\$ So are you saying I should go with option 2 in my original post, or something slightly different? \$\endgroup\$ Jun 17 at 14:17
  • \$\begingroup\$ @RyanPeschel Yes, I would go with the second one. Although I would not use entity types in the events. I would not decide what to do based on event.entity1 is bullet and event.entity2 is player. I would decide based on the components those entities have. \$\endgroup\$
    – Philipp
    Jun 17 at 14:19
  • \$\begingroup\$ if( (HasComponent(event.entity1, HurtBox) && HasComponent(event.entity2, HitBox) ) { /* apply damage */ } \$\endgroup\$
    – Philipp
    Jun 17 at 14:26

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.