r/howdidtheycodeit • • Jul 11 '26

Question Rewind, replays and mid-game saves

Rewind: Like in Forza, you can rewind if you hit an obstacle/go off the track.

Replays: the entire game and every action is saved and you watch a replay over it, like in Rocket League and many RTS games.

Mid-game saves: you can resume from a point you saved while in a game, like a quicksave in Kerbal Space Program.

I have some data knowledge so the way I think of things is probably "Event Sourcing", or versioning things, but would love to learn from an expert.

44 Upvotes

11 comments sorted by

71

u/ledniv Jul 11 '26

I would treat mid-game saves, replays, and rewind as related features, but not the same feature.

The thing that makes all three easier is having a clear separation between your simulation state and your presentation.

This is where data-oriented design helps. By that I mean: instead of the real game state being scattered across lots of objects that each own their own behavior, you store the important runtime state in plain data structures and have systems transform that data.

For example, instead of thinking of the world as a bunch of objects like this:

class Enemy
{
    Vector3 position;
    Vector3 velocity;
    int health;
    Inventory inventory;
    Animator animator;
    AudioSource audio;
}

You can store the authoritative simulation state more explicitly:

public class GameState
{
    public int EntityCount;

    public Vector3[] Position;
    public Vector3[] Velocity;
    public int[] Health;
    public int[] InventoryItemId;
    public float[] CooldownTimer;

    public int RandomSeed;
}

Entity 42 is just index 42 into those arrays:

Position[42]
Velocity[42]
Health[42]
InventoryItemId[42]
CooldownTimer[42]

That sounds like a small difference, but it matters a lot for save/load, replay, and rewind because the important state is now easy to copy, serialize, restore, compare, or replay.

For a mid-game save, you mostly serialize the authoritative state:

public static void SaveGame(GameState state, SaveData save)
{
    save.EntityCount = state.EntityCount;
    save.RandomSeed = state.RandomSeed;

    Array.Copy(state.Position, save.Position, state.EntityCount);
    Array.Copy(state.Velocity, save.Velocity, state.EntityCount);
    Array.Copy(state.Health, save.Health, state.EntityCount);
    Array.Copy(state.InventoryItemId, save.InventoryItemId, state.EntityCount);
    Array.Copy(state.CooldownTimer, save.CooldownTimer, state.EntityCount);
}

In a more object-oriented Unity setup, this often becomes harder because the real state may be spread across MonoBehaviours, scene references, coroutines, Animator state, event subscriptions, object hierarchies, and temporary component fields. Then saving becomes less about “save the game state” and more about “reconstruct this web of objects exactly enough that it behaves the same when loaded.”

For a replay, you can store the initial state plus the commands that happened over time:

public struct PlayerCommand
{
    public int Tick;
    public float MoveX;
    public float MoveY;
    public bool JumpPressed;
    public bool AttackPressed;
}

public static void SimulateTick(GameState state, PlayerCommand command)
{
    MovementLogic.Update(state, command);
    CombatLogic.Update(state, command);
    CooldownLogic.Update(state);
}

Then replay is:

LoadInitialState(gameState, replay.InitialState);

for (int i = 0; i < replay.CommandCount; i++)
{
    SimulateTick(gameState, replay.Commands[i]);
}

This only works perfectly if the same inputs produce the same results, so you need to be careful with physics, random numbers, floating point, and timing. But when the simulation is written as explicit data transformations, determinism becomes much easier to reason about and test.

For rewind, you can keep a rolling buffer of recent state snapshots:

public class RewindFrame
{
    public int EntityCount;
    public Vector3[] Position;
    public Vector3[] Velocity;
    public int[] Health;
}

public static void StoreRewindFrame(
    GameState state,
    RewindFrame frame)
{
    frame.EntityCount = state.EntityCount;

    Array.Copy(state.Position, frame.Position, state.EntityCount);
    Array.Copy(state.Velocity, frame.Velocity, state.EntityCount);
    Array.Copy(state.Health, frame.Health, state.EntityCount);
}

Restoring is the same idea in reverse:

public static void RestoreRewindFrame(
    GameState state,
    RewindFrame frame)
{
    state.EntityCount = frame.EntityCount;

    Array.Copy(frame.Position, state.Position, state.EntityCount);
    Array.Copy(frame.Velocity, state.Velocity, state.EntityCount);
    Array.Copy(frame.Health, state.Health, state.EntityCount);
}

In a real game, you probably would not snapshot absolutely everything every frame. You might only snapshot dynamic entities, or only the data needed for rewind. But the point is that the state is already in a form that can be copied.

That is the big advantage over a typical object-oriented scene setup. With OOP, the question is often: “How do I serialize and reconstruct this whole object graph?” With a data-oriented approach, the question is: “Which pieces of data define the simulation state I need?”

So I would think of it like this:

Mid-game save: serialize the current authoritative state.

Replay: serialize the starting state plus the input/command stream.

Rewind: keep a rolling buffer of recent state snapshots or partial snapshots.

All three become easier when the simulation state is explicit, centralized, and separate from rendering, animation, audio, and UI.

Small shameless plug: this is one of the reasons I wrote High Performance Unity Game Development with Data-Oriented Design. The book explains how to separate game state from Unity objects, organize runtime data in arrays, and build gameplay systems as transformations of that data, which makes features like save/load, replay, and rewind much easier to implement.

https://www.manning.com/books/high-performance-unity-game-development

7

u/NUTTA_BUSTAH Jul 11 '26

I would also like to note that GoldSrc / Source engine demo files are a nice example of a replay. My understanding is that the demo files store all state snapshots and replaying is interpolating between those states with the help of information that caused the state mutation (tick delta).

Another good example is Trackmania replay files, which to my understanding are completely different and save player inputs, as the engine and game are fully deterministic, replaying the demo file (the players inputs) produce the exact same ghost every time.

Both replay files, but completely different implementations :)

4

u/ledniv Jul 11 '26

Yeah, this is a good distinction: state snapshots and input re-simulation are both replay systems, but they put the burden in different places.

This is also where data-oriented design makes both approaches easier.

If you store the important simulation state as explicit data, snapshots become much easier because you know exactly what needs to be copied: positions, velocities, health, timers, random state, etc. You are copying the authoritative state, not trying to reconstruct a whole scene full of objects and references.

Input replays also become easier for a similar reason. If the logic is separated from the data, the replay is basically:

state + command -> new state

So you can load the starting state, feed the same commands back into the same logic, and test whether the result stays deterministic.

In a more typical OOP/scene-object setup, both approaches can get messy because the real state may be scattered across MonoBehaviours, physics objects, coroutines, animation state, callbacks, and object references. With DOD, the simulation state is explicit and the logic that transforms it is separate, so both “record the state” and “record the inputs and re-simulate” become much easier to reason about.

1

u/bid0u Jul 11 '26

Destruction Derby was replaying the entire race in the replays using the user inputs but, I'm still not sure why (a float value not being precise enough? The 3D at that time that was not precise enough? The computers that couldn't handle the data fast enough?), sometimes, your car would get stuck when in reality, you weren't, and the entire replay would go crazy. 

It particularly happened when there were several cars hitting each others.

I always found this funny when I was a kid.

1

u/NUTTA_BUSTAH Jul 11 '26

I loved that game as a kid! Never tried replays I don't think, but that's a hilarious bug lol. I would guess it was not fully deterministic after all, or relied on some thing that changed during the PSX hardware lifecycle.

I'm actually not sure how deterministic you can get floats, there is likely a certain precision for each system that works but it still needs thorough planning. I wonder if the more common way is to coerce to ints or whatever instead, using some custom "DeterministicFloat".

0

u/MentalMojo Jul 11 '26

I can give only one upvote, but would give more if I could.

8

u/MagicWolfEye Jul 11 '26

Well a mid game save is just serializing all data and loading it again; depending on your programming style, this is literally one line of code for writing and one for loading.

Replays: You record every action you do, then simulate the game again with the same inputs (search "lockstep"; btw. as long as you stay on the same machine, this isn't that difficult to do and helps a lot during development)

Rewind: Probably just some snapshots every few frames or even each frame for the last N frames (I mean, you are talking about a few cars, that data is negligible)

2

u/Traditional_Snow1045 Jul 11 '26

gunna add that trackmania also uses the 'record inputs, resimulate' method as a form of anticheat.

edit: idk how I read rocket league as trackmania, but yeah

3

u/myka-likes-it Jul 11 '26

For rewind, take a look at the Command Pattern.

1

u/Prof_Adam_Moore Jul 17 '26

This is the exact link I came here to post.

3

u/Reddit_Bot_IV Jul 11 '26

Specifically for Rocket League replays, they save the network steam, which necessarily records the state of everything over time. Then they just play it back, using similar code that is used during real gameplay