Implementing a Turn-Based Game in an Entity Component System with SPECS-Task

I’ve heard from a few people who are just getting started with entity component systems (ECS) that implementing logic for a turn-based game seems more complicated than it should be. I thought that seemed odd, but I just recently ran into this problem myself. While certainly not insurmountable, implementing turn-based logic in an ECS just doesn’t feel great. I think the reason is that no one likes to implement a loop via distributed state machines.

Consider this pseudo-code that you might expect for a turn-based game.

current player = first player
while game is not over:
actions = current player decides what to do
for action in actions:
player does action
current player = next player

That’s super simple! But wait, how would you do this in an ECS? Well… each of your players (or NPCs) is probably an entity. Then each action they could do is probably a component that’s only present while they’re doing that action. Then for a sequence of actions they’ll need another component. Oh and you’ll need a system for each of those components to run through the state machine of doing that action and presenting it to the player in a nice way. Each of those systems probably has to wait on some core subsystems to finish animations, physics updates, etc. Don’t even get me started on letting multiple players take their turns at the same time! You start to see how it’s state machines all the way down.

This isn’t totally surprising though. State machines are essential! They’re just not as nice as our higher-level abstractions that we used in the pseudo-code. Loops! Why can’t we loop like that in our ECS?

The reason is that an ECS is a set of systems that dispatch independently; you can’t tell system B to run while you’re already inside running system A.

I’m currently making a game using the Amethyst engine, which (as of writing) uses the SPECS ECS crate. There you have Systems which are essentially just functions with access to some set of Resources called a SystemData. All of the systems get bundled up into a Dispatcher which schedules them to prevent concurrent access to mutable resources. Each System gets to run only once on each call of Dispatcher::dispatch. Only the dispatcher gets to decide when systems run! Because of this model, you can’t wait for system B while inside of running system A; that would cause a deadlock.

One thing you can do is send an event to system B to tell it to do something. Then system B can message you back when it’s done. But this isn’t really any better than what I described above. The systems still have to care about intercommunication and manage the state of entities relative to the events that concern them.

Can we do better? I think so. In fact, there is already a programming paradigm that fits our situation rather well: multitasking! With task systems and futures, you can compose all of the tasks you want, forking and joining to create a graph of operations that take big chunks of time to complete.

But how can multitasking work in an ECS? You can’t just spawn a task in a closure that needs access to some Resources. Those Resources are locked down by the World and Dispatcher. And how would you even join a task if you can’t block? It’s true that we have to work inside of these limitations. All tasks have to run in some System. And we really can’t just block in the usual sense.

My strategy, which I’ve developed and released in the new specs-task crate, is to use graphs of entities to represent task ordering, similar to the fork-join model.

Generally any time you want to do some work in SPECS, you need access to some SystemData. And if you want that work done in the background of your current context, you need a separate System to do that work. If the work must be done over several iterations of a System::run, then it makes the most sense to put that work on a stateful resource, like a Component of some Entity. This concept is encoded in a simple TaskComponent trait, which is essentially a Component that gets to run like a System, but only when it needs to.

Then the system that runs your task works like this.

Now we at least have a generic way of running background work. Just implement the TaskComponent trait for your type of work, and create a component of that type on some entity. (Don’t forget to register your TaskRunnerSystem<T> on the Dispatcher ).

But what is the TaskProgress being used there?

This is a bit of metadata stored in a component on each task entity. It tracks whether the task is blocked, running, or complete. To allow for task parallelism, the TaskProgress implements interior mutability with an AtomicBool.

Remember the graph that represents task ordering? This is represented as a MultiEdge component that stores the entity IDs of children tasks which must complete before their parent can run. All tasks start blocked, and there is a separate system to unblock them, called the TaskManagerSystem. This is where the proverbial magic happens. The TaskManagerSystem traverses the task graph to see if any tasks can be unblocked. If you’re interested in seeing how this works, take a look at the source code here.

I’ll leave you with a real example of how I’m using the specs-task crate in my own game, which is in fact not turn-based, but allows every actor to play simultaneously in discrete steps. The task abstraction is nonetheless very useful.

The ChoreographyData is an object that can take a script of actions and create the task graph that implements the proper ordering of the actions. In order to do this, it uses the TaskManager object self.task_man to create and compose the tasks.

Notice how there are no state machines (if statements) required to specify the task graph! Just a declarative specification, and one if statement to check if all of the actions are complete.

Indie game developer

Get the Medium app

A button that says 'Download on the App Store', and if clicked it will lead you to the iOS App store
A button that says 'Get it on, Google Play', and if clicked it will lead you to the Google Play store