-
Notifications
You must be signed in to change notification settings - Fork 1
The Pitfalls of Parallelism
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:
- Player is pressing right and moves onto the pitfall
- 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.
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 |
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);
}
});
}
}