5: Rollback System
rollback.RmdThis article provides a description of why, when, and how the game rolls back to stay in sync with inputs. This system isn’t very relevant for the current iteration of rcade, but I might rewrite the article when I add online play.
1. Overview
Suppose two players are playing a game online. Their computers are exchanging information about what buttons they’re pressing to keep both their games in sync.
But now, one of the players’ internet falters for a second. That player tries to jump; but by the time their computer sends “player 1 jumped” to the other player, it’s been a whole second since they tried to jump. Now what!? We can’t just keep running the game, or totally different things would happen on the players’ screens.
The traditional workaround was to make this scenario impossible by pausing both players’ games any time internet lag occurred. The problem: pausing games randomly makes for an awful, choppy experience!
So modern games use a different solution, that never pauses or interrupts the game. When a game realizes something was received late like this, the game engine rewinds the game to before the input happened, and then fast-forwards it back to the present, repeating any inputs that should have happened. This is called a rollback.
This technique creates a smooth experience that solves our problem. It has a few limitations1, but it’s usually much better than the old solution.
1.1 Online?
“Wait a minute. Playing online? rcade doesn’t do online!”
But it’s designed to! And sometime, I’ll make an addon package that lets you do it.
The rollback system isn’t really relevant for singleplayer games. I built it into the engine because:
It’s good practice and makes for a more robust engine.
It makes the engine easily extendable to multiplayer.
I was as little worried that reading from the
inputs.csvfile might take too long for inputs to feel snappy. Luckily, it’s as instantaneous as everything else.
So, rcade structures itself in a way that makes rollbacks convenient and effective. How?
2. Rolling Back
We’ve just received an input that was supposed to have already happened. To resolve this, all we have to do is restore a previous gamestate, and rerun the game back to the present! This will process the input at the correct time now that we have it.
Functionality to restore a previous version is baked into the engine. The RAM continually refreshes2 a copy of itself from 2 seconds prior3 to use for this purpose.
This copy is restored with ram.rollback(), which just
calls utils::modifyList to overwrite the RAM with values
from its backup.
Once this version is restored, the game is two seconds or so behind
the present. Luckily, the timing system (explained in detail in
vignette("timing")) automatically deals with this situation
by catching it up to the present as quickly as possible. As it catches
up, inputs are reapplied when they should have occurred.
All of this works because inputs are stored in one
dataframe—RAM$inputs (which isn’t rolled back)—and the game
only cares about what inputs correspond to the current frame of RAM.
See vignette("inputs") or ?inputs.process
for more details on the Input system.
3. RNG
The RAM stores its own RNG just like an R session. Backups restore RNG state, so rollbacks will experience the same random events that happened originally, as you’d expect.
The RAM RNG can be set manually with ram.set_rng().