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.
Immovable
component instead of aMovable
component for the sake of clarity (i.e. it makes no sense aMovable
entity to have a reason why they can't move). Then theMovementSystem
would skipImmovable
entities. But I think this approach is so counter-intuitive that I didn't even bother mention it. That is, a programmer could wonder wtf is thisreason
field in theImmovable
component, and why such a component exist to begin with. Also, iterating through entities which are not supposed to move seems like a design flaw to me. – Giovanni L Nov 09 '19 at 18:37enum
s or strings? If the former, that means each system I add that wants to use theImmovable
component would have to add itself to theenum
; if the latter, then other systems should make sure their string value doesn't conflict with other systems'. – Giovanni L Nov 09 '19 at 18:47Immovable
. About the strings, prefix with the name of the system, unless you have two systems with the same name, that solves that. And, you are right about iterating over them... I'll edit my answer. – Theraot Nov 09 '19 at 19:03MovementSystem
iterate over all immovable entities and check the reasons is not ok, having theMovementSystem
check the conditions directly is not particulary better. With that said, requirements triumph design principles. If a design results in an unsable system (e.g. too slow), throw the design. By the way, I would look if you can use code generation for those checks, so you do not forget to update them when you change other systems (in fact, why not pass predicates). – Theraot Nov 09 '19 at 20:05MovementSystem
as you add new features to your game (as proposed in the end of the original post) is the ECS way to do things, yes? Or, at least, it is not your particular way of working with ECS? Even though we're doing DOD not OOD here, I think the open-closed principle is still relevant and I can see how going down this path would break that. – Giovanni L Nov 09 '19 at 20:07reason
s and cache missing every time you access the field, or using a fixed-size set which may not be what you want. This is a common problem when using containers in a component. Are you okay with that?Immovable
entities, so if you have e.g. 100,000 entities and you command an admin command to stop them all (idk like "/charlesxavier" lmao) and your application runs at 60 Hz, then theMovementSystem
will be iterating through 60 * 100,000 = 6,000,000 entities per second unnecessarily. – Giovanni L Nov 09 '19 at 20:07if (entity has StatusEffects and has Freeze)
you are still iterating over all of the immovable entities. If your ECS supports running a system when a component changes, you can have a proxy system run once when theImmovableRasons
component change, setting the entitiesimmovable
, and then they can be avoided in theMovementSystem
query. That isMovementSystem
would query objects that do not haveimmovable
. So, in your "charlesxavier" example, the proxy system runs once for all those entities, and that's it. – Theraot Nov 09 '19 at 20:14Immovable
is even more performant than using the if condition you mentioned because the if condition is checking for several things. I'm afraid my ECS only support positive queries (so only entities with a component, and not entities without), but I think your idea of proxy systems may be doable. I still have mixed feelings about this approach though. Right now, we only have two systems and it's already complicated. What will happen if my game scales to maybe hundreds of systems? Will I have to add anxReason
for every conditionx
a system may have? – Giovanni L Nov 09 '19 at 21:51Movable
andImmovableReasons
. Upon entity construction, you assign both. The latter holds an initially-empty set and is never removed. When a system wants to restrict movement, it adds a reason to the set and removesMovable
from an entity if it has it. When a system wants to make an entity movable again, it removes a reason from the set and checks if the set is empty, in which case the system adds aMovable
component... – Giovanni L Nov 10 '19 at 14:21MovementSystem
then iterates through entities withPosition
,Movable
, andMovement
, which makes it as performant as expected. Also, since adding/removing reasons only happen in certain frames, checking forMovable
won't be a performance hit. My only concern is that having such checks feels like each system is cleaning the mess of another system. Systems are still decoupled in the sense that any system only needs to know that another system may want to restrict movement, not which system. So I suppose this is aligned with ECS, yes? – Giovanni L Nov 10 '19 at 14:21StatusEffectSystem
can be the messier of all systems. Think about status such as "slow" and "haste" (buff and debuff of attributes used by other systems), or stuff like "silence" ("can't cast spells", probably similar to "can't move"), and of course everyones favorite "poison" (damage over time). "sleep" will probably be a problem. Some status effects affects some systems in a way and some other status effects affects another system in another way. – Theraot Nov 10 '19 at 16:23StatusEffectSystem
. – Theraot Nov 10 '19 at 16:24StatusEffectSystem
puts lots of conditions in many parts of the logic and that doesn't work well with pipelines. In this sense, anStatusEffectRemoverSystem
would be ideal as it "cut offs" what shouldn't be in the next step of the pipeline, similar to how the GPU works. So if you have e.g. aSilence
status, you'd remove everySpellCast
component. But unlike the GPU, game logic tends to be so complex that using such many cut-off/remover systems will drive the game designer crazy. – Giovanni L Nov 10 '19 at 17:29