0
\$\begingroup\$

I'm developing a top-down 2D game in Javascript using an Entity-Component-System architecture and I'm struggling with the question of exactly how to implement temporary buffs / permanent passives.

For example, let's say we want to give some entities Regeneration, such that they regenerate 5 health per second.

One option is to have a BuffsComponent that holds a list of all active buffs. You could create a RegenerationBuff object that inherits from some base Buff class, and then stick that in a list inside the BuffsComponent. Then a BuffsSystem would query all entities with a BuffsComponent every frame, and execute the active effect on each buff. Simple.

However, could you not argue that these effects could just be regular old components? That is, why not create a Regeneration component and then have a system that queries for all entities with a Regeneration component, and then update their health in that system?

Each approach seems to have its own pros and cons, and I'm not sure if I should have a mix of the two, or fully commit to one. I feel like fully committing to one approach would simplify the architecture and design of the program, so I'm leaning towards that, but I'm not sure which approach to pick. Even if I did have a mix, I'm not sure how to decide on what should be a component versus what should be a buff.

The first approach seems more computationally efficient I suppose? Because one potential implementation of such could involve you just iterating through the list of active buffs and calling execute on each one. However, I feel that while it may be more efficient, it may also be less powerful, because with the second solution I can directly query entities with certain buffs, so I could potentially have more complex interactions between entities based on who is holding which buffs. For example, with a Flight buff I would likely desire the ability to directly query entities with such a buff, so the latter solution would be preferable.

However, besides the latter solution being potentially slower, it also seems like it could get a bit out of hand. What if I ended up having over 100 different buffs? Adding and removing components to entities is O(1) with my ECS library, but still, it sounds like it could be a bit ridiculous to have hundreds of components for an entity, most of which are just random buffs / passives.

What should I do?

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

Component-per-buff may cause some serious performance issues. Of course, at this point we can only speculate about the performance, but so far I can only see the downsides — at least in comparison to a more traditional approach.

Some of the the major arguments for using ECS stem from it's ability to exploit cache locality and parallelism for the performance gains. All of that is either hard or impossible to achieve in JavaScipt. To make things worse, each additional system increases performance overhead of ECS just by existing, because each System's dependencies must be considered when an Entity is created or modified.

More specifically, due to the nature of buffs, entities will be modified often (as buffs being added and removed), triggering recalculation of the ECS cache, but Components will be updated rarely, causing Systems to loop a lot without achieving anything of use.

Contrarily, more specialized design enables minimal overhead and allows to extend beyond the capabilities of the ECS.

  • Buffs may be represented by a priority queue of timers, limiting update to the only buffs that must be updated.
  • Buffs may interact with each other, producing new buffs, modifying or cancelling each other.
  • Buff rules will be isolated from the ECS, causing no namespace pollution.

And one last detail, tangential to the question:

For example, with a Flight buff I would likely desire the ability to directly query entities with such a buff, so the latter solution would be preferable.

It should be more efficient to design an unified movement system, that allows flight with a flip of a switch. Besides, how your characters are supposed to move, given both the innate movement component and a temporary flying component at the same time?

\$\endgroup\$
1
  • \$\begingroup\$ Thanks for the reply. I worry that many of these points don't apply though, because I'm not using an archetypal implementation for the ECS architecture. That is, adding and removing components to an entity is O(1) with my implementation. My query operation is slightly less performant as a result, but it's still very fast. \$\endgroup\$ Apr 2 '21 at 19:31

You must log in to answer this question.

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