ECS is excellent at decoupling: you split your game logic into multiple pieces, and every system is responsible for dealing with a specific piece. However, I don't know if each system should be aware of the rules of my whole game or if it should only care about its own little domain and be as unaware as possible of the general game rules.
For example, let's say I start building a game where initially entities have position and can move. So I create a Position
component and whenever I want an entity to move, I assign a Movement
component to it and let a MovementSystem
do the job. The movement system will then iterate through all entities with position and movement, do some checking e.g. if movement is inside the entity map's bounds, and calculate the path for the destination. If everything is fine, it'll advance an entity's position a little bit given a delta time. And this process repeats each frame.
Later at some point, I decide to add a StatusEffects
component which holds the multiple status effects an entity can have such as Freeze
. Naturally, since an entity is not supposed to be frozen forever, I also add a StatusEffectSystem
that'll be in charge of updating and removing expired status effects. Conceptually speaking, the movement system should know nothing about an entity status effects since all it cares about is movement. But how do I stop an entity from moving when it's frozen?
The naive answer would be to simply remove the Movement
component whenever a Freeze
status effect is assigned to an entity. However, that would only stop the ongoing movement, and as long as the player pressed a mouse button to move the entity again, another Movement
component would be created by the input system thus disregarding the entity's status effects. Of course, we could have an intermediary system for removing Movement
components of entities with a given status effect so that if any other system created a movement, an status effect remover system would take care of that, and the core update logic would be similar to:
void update(dt) {
InputSystem.update(dt);
StatusEffectRemoverSystem.update(dt); // removes Movements if needed
MovementSystem.update(dt);
StatusEffectSystem.update(dt);
}
However, I think this is bad because if the number of systems is large - and it tends to be -, then it can quickly become unmanageable and bugs can be difficult to spot.
Another approach would be to add a Movable
component and assign it to entities capable of movement, and whenever we wanted to remove the movability of an entity, we'd remove such a component. The status effect system would then remove the movable component of a frozen entity, and when the frozen status expired, it would add it back. The movement system would also be changed so that it now iterates through entities with position, movement, and movable. Everything is fine, right? Not quite.
What would happen if another part of my game logic, maybe a new system, also wanted to add and remove the movability of an entity? Say for example I add an inventory system that allows the player to store their items, but they shouldn't be able to move when they're managing their inventory. Well, I could remove the Movable
component from the player's entity when they open their inventory, and add it back when they close it. And here's where things get messy. Because if the player happen to have a frozen status (maybe they're opening their inventory to use a potion?), then closing their inventory is all it takes to make them movable again and dismiss their status. Or conversely, if their frozen status ends before they close their inventory, they'll be able to move with their inventory open, which is incorrect behaviour.
Given such considerations, it seems to me that the most straightforward approach for such an issue is to detect if the player has a Freeze
status in the movement system, in which case the movement would be ignored. Something like:
void MovementSystem.update(dt) {
foreach (entity with Position and Movement) // forget about "Movable"
{
if (entity has StatusEffects and has Freeze)
continue; // or maybe remove Movement
...
}
}
I like it for two reasons. First, it clearly shows the precondition for every movement; no need for such status effect remover system or the like. Secondly, since everything movement-related would be in one place, it's easier to find if something is not behaving as expected. However, it makes the movement system dependant on any changes or feature/component addition I make on my game, which would make each system aware of my whole game rules, which boils down to the question: is this aligned with the ECS pattern? Because I can see how this could be a problem for e.g. a game engine aiming to be extensible.