Flukz — open-source shmup community resource open-source GPL devlog  ·  about
// indie shmup & game-dev resource

Game Programming · June 30, 2026

Save and Replay Systems in Shmups: Recording Inputs for Deterministic Playback

A replay in a shmup is not a video. It is a compressed record of the player's inputs and the initial state of the simulation. On playback, the game re-runs the entire simulation from that starting state, feeding the recorded inputs on the correct frames, and produces the same sequence of events the original play session did. This is elegant and compact — a five-minute run fits in a few kilobytes — but it requires something the game may not yet have: full determinism.

The replay system is the strictest possible test of simulation consistency. Any source of non-determinism — a random number that does not replay identically, a floating-point calculation that depends on execution order, a timer that reads wall-clock time — will cause the replay to diverge from the original, usually within the first thirty seconds. Implementing replays is therefore not an audio or serialization problem; it is first and foremost a disciplined engineering constraint on how the simulation is written.

Determinism preconditions

The simulation must be purely functional given its inputs. Every piece of game state that changes must be determined by the previous state plus the player's input on that frame. The three common violators are:

What the replay file contains

Once the simulation is deterministic, the replay file is small. The minimum required contents:

// Replay file header (binary or JSON) { "version": 3, "game_version": "0.9.4", "rng_seed": 2847361902, "character": "ship_alpha", "difficulty": "hard", "stage": 3, "duration_frames": 18342, "inputs": [ { "frame": 0, "held": ["right", "fire"] }, { "frame": 1, "held": ["right", "fire"] }, { "frame": 14, "held": ["fire", "up"] }, ... ] }

Run-length encoding of the input stream (storing only frames where the input state changes, not a record on every frame) is the key to keeping file size manageable. A five-minute run at 60 fps is 18,000 frames, but most of those frames have identical input to the previous one. The change-only format shrinks the average input log to a few hundred entries.

File versioning and forward compatibility

A replay recorded against version 0.9.4 of the game will not play back correctly on version 1.0.0 if anything affecting the simulation changed between the two versions. Enemy move speed, bullet pattern parameters, collision hitbox sizes — any change to these makes old replays invalid. There are two approaches:

The strict approach is to embed the full game version in the replay header and refuse to play back replays from incompatible versions. This is honest but means replays become unplayable after patches. The archival approach is to version the simulation itself separately from the game build and ship older simulation modules alongside the current one, switching based on the replay's embedded simulation version. This is significantly more engineering effort but produces replays that remain valid indefinitely — a meaningful feature for competitive communities.

Most indie shmups use the strict approach and accept that replays have a limited lifespan. Communicating this clearly to players (a visible expiry warning in the replay metadata) is better than silently showing a desynchronized playback that the player interprets as a bug.

Divergence detection

Even with a carefully deterministic simulation, subtle bugs can cause replays to diverge in specific conditions. A hash check at regular intervals catches this early: during a replay, hash the full simulation state every 300 frames and compare against hashes stored in the replay file at record time. When a mismatch is detected, the replay system can log which frame the divergence occurred on and which state fields differ, rather than silently producing wrong output until the playback is visually obviously wrong.

Practical limits of long replays

Replays longer than about ten minutes introduce a usability problem: seeking. A user who wants to watch a specific boss fight from a 40-minute run cannot jump to minute 30 — the replay must re-simulate from frame zero to reach that point, which takes real time at game speed. Snapshot checkpoints solve this: every five minutes of simulation, record a full state snapshot. Seeking to a checkpoint then replaying forward from there takes at most five minutes of simulation time instead of forty.

State snapshots are much larger than the input log — a full simulation snapshot might be 50–200 KB depending on entity count — but the total overhead for a 40-minute run with snapshots every 5 minutes is seven snapshots plus the input log, which is still well under a few megabytes. For a competitive community where watching top scores and verifying records is a regular activity, the seeking capability the snapshots enable is worth every byte.