3
\$\begingroup\$

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.

\$\endgroup\$
3
\$\begingroup\$

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.

\$\endgroup\$
  • 1
    \$\begingroup\$ @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. \$\endgroup\$ – Theraot Nov 9 at 19:03
  • 1
    \$\begingroup\$ @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). \$\endgroup\$ – Theraot Nov 9 at 20:05
  • 1
    \$\begingroup\$ @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. \$\endgroup\$ – Theraot Nov 9 at 20:12
  • 1
    \$\begingroup\$ @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. \$\endgroup\$ – Theraot Nov 9 at 20:14
  • 1
    \$\begingroup\$ @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. \$\endgroup\$ – Theraot Nov 10 at 16:18
2
\$\begingroup\$

Entity movement can be influenced by a variety of sources such as local player, network, or even AI so this is why I often find it easier to treat it as state that is mutated across multiple systems.

Lets take this component structure:

struct Movement 
{
  // -1/+1 implies move either direction along given axis, 0 implies no movement
  short x, y, z;
}

If your entity has a PlayerComponent, there would be an associated PlayerControllerSystem that would read the input captured by the InputSystem and translate that input into actions. Typically the controller would maintain a map of key<->action mappings that would allow a game to be customized allowing the player to setup keybindings as they wish.

In this example, lets assume that WASD are all mapped to their generally accepted movement bindings of forward, left, back, and right respectively.

It would be the job of the PlayerControllerSystem to review the input captured and based on key presses, it would update the Movement component's x, y, and z values accordingly to indicate that the player wishes to perform some movement in that direction.

You would then have your StatusEffectSystem that checks to determine whether the entity happens to be frozen during this frame. If the player is frozen, this system would then set the Movement component's x, y, z all to 0 for this frame. If the player is not frozen, no component changes occur.

After all systems that influence movement have had a chance to contribute their interpretation of entity state, your MovementSystem then iterates all entities that contain a Movement component and based on the values in x, y, and z; that direction gets turned into a movement vector based on interpolation and then gets applied to the entity.

The benefit here is there is no need for any other special component, you avoid cache misses in the MovementSystem all together. You treat each system as either one that simply mutates current state or reacts to current state; e.g. a pipeline.

Also in order to avoid coupling between systems, I mentioned querying input above to see if keys are pressed; this can be handled by moving that state into a component as well. In my implementation, we have a concept (which Overwatch also uses) called Singleton components. The idea is that there are certain components that the World maintains a reference to because there is only ever 1 instance and therefore it makes more sense for systems to query that component's state rather than having to know about a system that manages said state.

\$\endgroup\$
  • \$\begingroup\$ I thought about this solution, but this way you're iterating through entities which are not moving too. And as I said in a comment, this is a waste of processing power. Depending on how many entities with Movement you have in your game and how many of them aren't moving in a given frame, it could be that cache missing all moving entities performed better than cache hitting many of them which aren't moving... \$\endgroup\$ – Sepia Color Nov 14 at 22:40
  • \$\begingroup\$ ...I'm not so sure about you avoiding cache misses "all together" as you said. If your entities are in a map, copying the whole map data to each entity would be an insane waste of memory, so you'd better have a reference to the current map or - if the map is implemented as an entity - the map's entity id, in any case you'd have to jump to the memory address of the map's data causing a cache miss. \$\endgroup\$ – Sepia Color Nov 14 at 22:42
  • 1
    \$\begingroup\$ How you choose to store entity data obviously can have an influence on how you may mutate data. However, I don't understand your reference to map. Our component storage system allows us to iterate components in a cache friendly way in contiguous memory while also being able to fetch and iterate sibling components in a similar way. So for movement it boils down to a tight loop which calculates a vector3 of movement and we then apply said movement to the rigid body or non-physics controlled entity. \$\endgroup\$ – Naros Nov 18 at 17:53
  • 1
    \$\begingroup\$ In my experience, if your entity system consists of only the entities visible in the area of view around the player, chances are the majority of those that can move are often doing some form of movement frame-to-frame. There will be some which likely won't because their wander behavior may have entered a pause state for a few seconds, but generally speaking those are considerably fewer than those that are actively moving. \$\endgroup\$ – Naros Nov 18 at 18:01
  • \$\begingroup\$ I'm building a tile-based RPG where maps are comprised of tiles and some tiles are blocked for movement (i.e. entities can't move to those tiles), so MovementSystem needs to check if a tile is valid for movement before applying Movement to Position. Since many entities can be in a same map, it's memory-efficient to store map data somewhere and let each entity have a reference to the map it's currently in instead of storing the whole map data as a component for each entity... \$\endgroup\$ – Sepia Color Nov 19 at 23:04

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.