0
\$\begingroup\$

Let's say we have three enemies, a bear trap, a fire trap, and a minotaur. When you walk over the bear trap, the game spawns an invisible entity that, upon the player colliding with it, slows the player and deals damage. The fire trap is similar except it applies a damage over time debuff instead. The minotaur creates a fast moving invisible projectile on attack that causes damage on collision.

Initially I had a solution where there were no events, and it was just a pure ECS. That is, an entity would have something like a ThornsComponent and upon a collision of two entities, if one had the ThornsComponent and the other had a HealthComponent, damage would be dealt.

However, this began to get a bit messy for a couple reasons. For one, when I had to introduce entities which apply debuffs on hit, then I suddenly needed a DebufferComponent, such that when an entity collided with another, it would apply a debuff.

The problem with this is that I would need a separate component for every single possible type of interaction / event. In addition, it doesn't allow for easy scripting, playback, debugging, etc.


I read around and some advocate using an event queue instead. Something like this:

events.create(new DamageEvent(entity1, entity2));

for (let event of scene.events.get<DamageEvent>()) {
   // code
}

The issue with this is that we don't know there should be a damage event, or how exactly to handle this one in particular. Within an ECS, there is no "minotaur" - it's just a collection of components.

That is, when two entities collide, we don't know it's a minotaur's melee projectile and a player. So, I feel like I'd have to do something like this (in the collision handling code):

if (e1.hasComponents(CT.Thorns, CT.Team, CT.Physics) && e2.hasComponents(CT.Health, CT.Team)) {
  events.create(new DamageEvent(e1, e2));
} else if (e2.hasComponents(CT.Thorns, CT.Team, CT.Physics) && e1.hasComponents(CT.Health, CT.Team)) {
  events.create(new DamageEvent(e2, e1));
}

But even then, how do I handle entities which should apply a debuff on-hit? Then I would still need to have a Debuffer component regardless! Likewise, we still have a ThornsComponent

This seems just as gross as the original code. Basically, it seems like regardless of what I do, I still need to have a Component for each possible thing that could happen, which would quickly get out of hand.

What I feel would be better is to instead be able to just encode these event handlers into when I create the entity itself, like this:

let eventsComponent = new EventsComponent();
eventsComponent.add(ET.Collision, event => {
  let healthComponent = event.target.get(CT.Health);
  if (healthComponent) { health.damage(10); }

  let buffsComponent = event.target.get(CT.Buffs);
  if (buffsComponent) { buffs.list.add(new DamageOverTime(5)); } 
});

But now, the problem with this is two-fold:

  1. All of this event processing is synchronous and we can no longer, say, run damage-dealing code all in one part of the code-base and debuff-setting code in another part of the code-base. It's just a single-event handler that is ran when the event occurs, which does all the behaviors.

  2. We're encoding behavior into the entity definitions, instead of letting systems describe behaviors and letting components describe state.

That is, while this code seems cleaner at first glance, in reality it seems to go against many tenets of proper ECS design, and may fail down the line and become hard-to-debug as like-behaviors are ran out of order and entities began to describe their own behavior in addition to their state.

\$\endgroup\$
7
  • \$\begingroup\$ Seems like the kind of thing you might want to delegate to your scripting engine. Detect the collision, and pass the two colliding objects (or some data describing them) to the appropriate script (might be global, might be attached to one of the colliders, up to you), and let the script decide. You don't even have to do it right away; you can put the (trigger event + collision data) in a queue — fire and forget — and then process it later in a pass that deals with scripting. So, nothing specific like "damage event", just "trigger tripped" + enough data so that the script can make sense of it. \$\endgroup\$ Commented Jun 16, 2021 at 18:35
  • \$\begingroup\$ @FilipMilovanović I think that sounds right too, but I guess I'm confused how the script will figure out how to handle it. Because the entities it is passed are just component bags. How should it know that this entity is a Fire Trap Attack and it should apply such a debuff? The only thing I can think of is to have a million different marker components, or just include a name in the entity somewhere (which seems to kind of defeat the purpose of an ECS..) Also, the script would cause everything to happen at the same time (damage, debuffs, etc.), as opposed to by the proper system. \$\endgroup\$ Commented Jun 16, 2021 at 19:11
  • \$\begingroup\$ "include a name in the entity somewhere (which seems to kind of defeat the purpose of an ECS..)" - I don't think so; a name is a property of an entity like any other (velocity, transform matrix, ...); it's just that you don't have an explicit type. You can have a "Description" component that includes things like a Name and/or an ID, a Description, a Tag of some sort, maybe something for debugging purposes. Nothing wrong with that. You don't have to pass your internal data structures to the script, you can construct something on the fly (e.g., { trigger: 'FIRE_TRAP_01', modifiers: [...] }). \$\endgroup\$ Commented Jun 16, 2021 at 19:26
  • \$\begingroup\$ Alternatively, you could arrange for your eventsComponent(s) to process the queue in a similar way - not immediately but in a separate "events" pass. \$\endgroup\$ Commented Jun 16, 2021 at 19:29
  • \$\begingroup\$ Is this the actual recommended solution? If so, feel free to elaborate with an answer. I feel like most places I've read online shy away from something like this because it's considered "bad practice" within the context of an ECS to basically specify the type of something in order to properly handle it, and instead they usually recommend either creating additional components to describe the behavior, or encode the behavior itself into the entity via effect callbacks when events occur, or something of the sort.. \$\endgroup\$ Commented Jun 16, 2021 at 20:03

0

You must log in to answer this question.