Turn and action resolution - ECS style


So using ECS is pretty straightforward for real time games, as long as you don’t mess up the execution order of systems too much. But how do you model turn based games using ECS?

The code snippets below are written using Friflo.ECS, a C# library, but most of the concepts easily apply to others, like Flecs, or Bevy. I embedded code as images not because I hate my readers, but because Itch code formatting is nonexistent, which makes it unreadable. The full code is available here.

In my first attempt I was following the tutorial pretty closely: saving actions to a queue, then dequeing and executing them in FIFO order using a polymorphic Execute method on each action and passing it an entire mutable world. This was not using basically any ECS features at all and I didn’t like the “here’s a mutable world, do whatever” approach, so I wanted to try a more “idiomatic” ECS approach to see if there were any advantages.

For my refactoring I divided everything into 2 phases that are run in sequence every tick:

UpdatePhases.ProgressTurns.Add(new TickEnergySystem());
UpdatePhases.ApplyActions.Add(new ProcessActionsSystem());

Let’s see them in detail.

Processing character turns

The first phase (ProgressTurns) is responsible for finding the next character who should act (via the TickEnergySystem) and then producing one or more actions for that character using a bunch of other systems.

I implemented the energy system described in Bob’s article where characters each recharge their turns separately and one character can take multiple turns while another recharges. Each character entity has an Energy component that tracks these values:

struct Energy() : IComponent {
  int Current = 0;
  required int GainPerTick;
  int AmountToAct = 10;
}

Once a character has enough energy to act, a self explaining CanAct tag is added to its entity by TickEnergySystem. Queries are cached in normal code, but more explicit in the examples to see what data they are operating on.

image.png

Once a character can act, multiple systems can pick it up and create actions for it, for example a system to randomly move entities that have a GridPosition component and the tags Enemy && CanAct

image.png

TurnsManagement.QueueAction() is just creating a new entity with a MovementAction component and the IsActionWaiting and optional IsActionBlocking tags that we will see soon. This is basically using ECS to create a queue of actions to be executed, rather than the typical list in the previous implementation.

Executing actions

During the ApplyActions phase we take queued actions and resolve them. Actions are any kind of component with one or more of the following tags:

image.png

The following system does some maintenance on current actions and prepares new ones for execution:

image.png

And then systems can query for specific actions and execute them. For example a movement action:

image.png

Conclusion

As much as I hate flowcharts, here is an excuse for me to try out Excalidraw by showing you how systems transform entities: ecs-turns.png

Is it all a bit overengineered for the sake of idiomatic ECS? Probably, yes. Compared to the simple, no ECS method (provided in the beginning) these are the things I can think of:

Advantages:

  • Actions are more explicit in the data they manipulate since they leverage systems now.
  • Action data and logic are separated between components and systems
  • Multiple systems can potentially react to a character getting ready or an action being resolved (think visuals or audio). This is similar to adding multiple observers to some of the state changes in a more decoupled way. In the simple method you would have to add some sort of event pattern or use delegates to achieve the same thing.

Disadvantages:

  • The code is more complex than the initial, simple method
  • I haven’t handled some edge cases, like order of execution if multiple actions are enqueued in the same system run. This would be trivial with the simple method.
  • Systems that resolve actions are always run (think polling), so they will do an empty run even if there are no actions to execute. Performance is not an issue here, but it does feel less elegant than just calling what you need, when you need it (the simple method is conceptually similar to event based)

Some notes on other ECSs:

  • In Flecs there would be less boilerplate for declaring systems, and command buffers are implcitly used by default, so the examples would be easier to read.
  • In Bevy it would be possible to pass in parameters to systems and pipe data between systems, so it should be easier to follow the “normal” approach and translate it into manual system calls. That way you get both the tightly controlled execution of the first, and the extra bits of ECS systems.

Leave a comment

Log in with itch.io to leave a comment.