3

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.

Giovanni L
  • 31
  • 6

2 Answers2

3

Make a Immovable component that contains a set※1. The idea is that when this set is empty the entity can move.

We can do this by having The MovementSystem check, however that would imply iterating over entities that are set to not move. Ideally the MovementSystem would not have to do that※2.


So, for example, your Immovable component has a set of strings, and when the character is frozen, then StatusEffectSystem can add the "frozen" string to that set. When the MovementSystem checks, it sees it is not empty, and thus it does not move. If you need to debug why... well, you look at the set, and it says "frozen". Eventually the status goes away, and that item can be removed from the set by the StatusEffectSystem.

Similarly, the inventory system or whatever other system you need to restrict movement can add and remove items from the set in the Immovable component. As long as they only remove items they added, there should be no problem.

This way, you know that a system will not accidentally remove a restriction to movement imposed by another system.


※1: A set of what? If you plan on allowing third parties to add system to your game, the enum is not an option. Otherwise, an enum would work. It is not that system has to add themselves to the enum, it is that you add them. If enums are not an option, you can use strings... if conflict is really a problem (you are developing the game, you should know what strings you are using), you can always prefix the strings with the name of the system, assuming you do not have two systems with the same name (which I consider a reasonable assumption) that would solve conflicts. Consider also start the development using strings and switching to enums when the set of systems of your game has been fully decided.

※2: Right, so, we can have Immovable and ImmovableReason. Where Immovable is empty and ImmovableReason has the set. Then an intermediary system that runs on changes of ImmovableReason and adds or removes Immovable. Then MovementSystem can query on the absence of Immovable. This is, of course, contingent on whatever or not your ECS supports notifications on change of components.

Theraot
  • 26,532
  • 4
  • 50
  • 78
  • I considered that option. However, I'd put the set into an Immovable component instead of a Movable component for the sake of clarity (i.e. it makes no sense a Movable entity to have a reason why they can't move). Then the MovementSystem would skip Immovable 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 this reason field in the Immovable 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:37
  • Moreover, even if I decided to do so, I'd have the problem of how to identify the values in the set. Would I use a set of enums or strings? If the former, that means each system I add that wants to use the Immovable component would have to add itself to the enum; 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:47
  • 1
    @SepiaColor sure, call it Immovable. 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:03
  • 1
    @SepiaColor Ideally a system is not aware of the others, so they can change idependently. On that note, if having the MovementSystem iterate over all immovable entities and check the reasons is not ok, having the MovementSystem 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:05
  • Alright. So I have two questions: 1) You don't think adding code in the MovementSystem 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:07
  • One of the benefits of ECS is it reduces cache misses significantly. But using a set would either force you to dynamically allocate memory for storing the reasons 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?
  • – Giovanni L Nov 09 '19 at 20:07
  • Another thing is you're still iterating through 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 the MovementSystem will be iterating through 60 * 100,000 = 6,000,000 entities per second unnecessarily. – Giovanni L Nov 09 '19 at 20:07
  • 1
    @SepiaColor on the set, a reasonable initial capacity size should do. You may decide on something like the next power of two after the number of systems. You may log if it needs to grow during tests, and then based on that decide to increse the initial capacity. – Theraot Nov 09 '19 at 20:12
  • 1
    @SepiaColor On the code on your question where you say if (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 the ImmovableRasons component change, setting the entities immovable, and then they can be avoided in the MovementSystem query. That is MovementSystem would query objects that do not have immovable. So, in your "charlesxavier" example, the proxy system runs once for all those entities, and that's it. – Theraot Nov 09 '19 at 20:14
  • Yeah. Actually, using Immovable 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 an xReason for every condition x a system may have? – Giovanni L Nov 09 '19 at 21:51
  • So, after giving some thought to this, I found the following solution. So you'd have two components, Movable and ImmovableReasons. 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 removes Movable 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 a Movable component... – Giovanni L Nov 10 '19 at 14:21
  • ...MovementSystem then iterates through entities with Position, Movable, and Movement, which makes it as performant as expected. Also, since adding/removing reasons only happen in certain frames, checking for Movable 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:21
  • 1
    @SepiaColor yes, that is good use of ECS and a valid solution of the problem at hand... The advantage of the proxy system, in the problem at hand, is that you do not have to repeat the logic of checking when the set is empty, That would be in a single place. – Theraot Nov 10 '19 at 16:18
  • 1
    @SepiaColor I have also been thinking about the problem and I think StatusEffectSystem 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:23
  • 1
    @SepiaColor I do not know how to generalize the status effects. The closest thing I can think of is a "chemistry engine", a term coined by the Breath of the Wild team. At least there is confort in that other systems will be easier than StatusEffectSystem. – Theraot Nov 10 '19 at 16:24
  • The reason I looked for an alternative to this proxy system is because it's reactive, and that would cause a lot of cache misses. But proxy system or not, this is a dead end because as soon as you check for the existence of a component you'll jump to another memory block, so you're gonna cache miss either way. Btw, cache friendliness is one of the advantages of ECS which apparently can only be achieved with simple computations. Other, perhaps more important ones are flexibility and potential parallelization so I suppose using a reactive system along with ECS is not intrinsically bad. – Giovanni L Nov 10 '19 at 17:27
  • Yeah. I think the core of the problem here is that ECS works at its best when used as a pipeline, but the StatusEffectSystem puts lots of conditions in many parts of the logic and that doesn't work well with pipelines. In this sense, an StatusEffectRemoverSystem 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. a Silence status, you'd remove every SpellCast 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