Skip to content

The Pitfalls of Parallelism

Mike edited this page Oct 17, 2017 · 1 revision

Parallelism in games is hard.

Let's step through an example game: Rouge Dungeon Explorer.

The game contains two systems:

  • When a directional key is pressed, the player moves 1 tile in that direction
  • If the player is standing on a pitfall, they die

Luckily, our systems run sequentially. The movement system precedes the pitfall check.

Our execution flow looks like:

  1. Player is pressing right and moves onto the pitfall
  2. Player is standing on the pitfall and falls to their death

Bad for the player, good for our code.

The next step is to run all the game systems in parallel for that sweet performance boost. With both systems running in separate threads, the order they run is no longer guaranteed.

Let's step through the possible execution orders across two game loops:

Order System Outcome
1 Movement Player is pressing right and moves onto the pitfall
2 Pitfall Player is standing on the pitfall and falls to their death
3 Movement The player is already dead
4 Pitfall The player is already dead
Order System Outcome
1 Pitfall Player is not standing on the pitfall
2 Movement Player is pressing right and moves onto the pitfall
3 Pitfall Player is standing on the pitfall and falls to their death
4 Movement The player is already dead
Order System Outcome
1 Pitfall Player is not standing on the pitfall
2 Movement Player is pressing right and moves onto the pitfall
3 Movement Player is pressing right and moves off the pitfall
4 Pitfall Player is not standing on the pitfall

Oops! It's totally possible for the player to walk over the pitfall without falling. Parallelizing our game systems added the worst kind of bug. The game code looks flawless and passes all unit tests, but sometimes the game just doesn't work.

The Lambda Solution

Lambda is a game engine built for parallelism. Each iteration of the game loop is split into two phases: one where the systems determine what data to modify and how to change it, and one where all the modifications take place.

Since the game systems only read state, they can safely run in parallel. The modifications are grouped by data they update, and then run in parallel.

The problematic execution flow is solved using the Lambda approach:

Order System Outcome
1 Pitfall Player is not standing on the pitfall
2 Movement Player is pressing right, schedule the player to move right
3 Data Update Player is moved onto pitfall
4 Movement Player is pressing right, schedule the player to move right
5 Pitfall Player is standing on the pitfall, schedule the player to fall
6 Data Update Player is killed, player is moved

Programming Parallel Game Logic

Lambda provides set, create, and destroy methods for modifying game state. By only allowing state to be changed through these methods, the two phase game loop is enforced. As a developer, writing parallel, non-conflicting game systems is the same as writing sequential code.

A look at the movement system from our example:

public class Movement extends GameSystem {

    @Override
    public void update() {
        findAll(Player.class).forEach(player -> {
            Component position = event.get(Player.POSITION);

            if (Keyboard.RIGHT) {
                set(position, Position.X, position.get(Position.X) + 1);
            }
        });
    }
}