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:
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.
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.
Fire Trap Attackand 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\${ trigger: 'FIRE_TRAP_01', modifiers: [...] }). \$\endgroup\$