---
title: "Rollback and resimulation"
url: https://recursiveai.org/onephysics/cookbook/rollback/
description: "Build a rollback loop with OnePhysics: a ring of snapshots, a late input that rewinds five frames, quiet resimulation, and a check that the result equals…"
version: "0.4.0-preview.1"
---

# Rollback and resimulation

Build a rollback loop with OnePhysics: a ring of snapshots, a late input that rewinds five frames, quiet resimulation, and a check that the result equals a world that had the input on time.

A stack of ten boxes and a ball. The ball is pushed at frame 60; at frame 120 a peer’s input for frame 115 arrives late.

```csharp
using RecursiveAI.OneMath;
using RecursiveAI.OnePhysics;
using RecursiveAI.OnePhysics.Physics2D;

const float dt = 1f / 60;
var world = new PhysicsWorld2D(new WorldOptions2D { WorldId = 1 });
world.AddBox(new Vector2F(20, 1), at: new Vector2D(0, -0.5), type: BodyType.Static);
for (int i = 0; i < 10; i++) world.AddBox(new Vector2F(0.8f, 0.8f), at: new Vector2D(0.05 * i, 0.5 + i));
var ball = world.AddCircle(0.5f, at: new Vector2D(-6, 1));
int sounds = 0;
ball.OnCollisionEnter += c => sounds++;
world.Warmup();                                   // steps, snapshots and restores no longer allocate

const int Ring = 8;
var ring = new byte[Ring][];
var lengths = new int[Ring];
for (int i = 0; i < Ring; i++) ring[i] = new byte[world.GetSnapshotSizeBound()];

int frame = 0;
void Advance(Vector2F? push = null)
{
    int slot = frame % Ring;
    world.TrySnapshot(ring[slot], out lengths[slot]);
    if (push is { } v) ball.LinearVelocity += v;  // this frame's input, if any
    world.Step(dt);
    frame++;
}

for (int i = 0; i < 120; i++) Advance(i == 60 ? new Vector2F(12, 0) : null);
Console.WriteLine($"frame {frame}: hash {world.ComputeHash():X16}, sounds {sounds}");

// A peer's input for frame 115 arrives late: a second push. Roll back five frames and resimulate.
int target = frame - 5;
world.Restore(ring[target % Ring].AsSpan(0, lengths[target % Ring]));
frame = target;
using (world.SuppressCallbacks())                 // handlers stay quiet while frames replay
{
    for (int i = 0; i < 5; i++) Advance(frame == 115 ? new Vector2F(0, 6) : null);
}
ulong corrected = world.ComputeHash();
Console.WriteLine($"frame {frame} after rollback: hash {corrected:X16}, sounds {sounds}");

// A second world fed the same inputs from the start reaches the same bits.
var replay = new PhysicsWorld2D(new WorldOptions2D { WorldId = 1 });
replay.AddBox(new Vector2F(20, 1), at: new Vector2D(0, -0.5), type: BodyType.Static);
for (int i = 0; i < 10; i++) replay.AddBox(new Vector2F(0.8f, 0.8f), at: new Vector2D(0.05 * i, 0.5 + i));
var ball2 = replay.AddCircle(0.5f, at: new Vector2D(-6, 1));
for (int f = 0; f < 120; f++)
{
    if (f == 60) ball2.LinearVelocity += new Vector2F(12, 0);
    if (f == 115) ball2.LinearVelocity += new Vector2F(0, 6);
    replay.Step(dt);
}
Console.WriteLine($"replayed from scratch: hash {replay.ComputeHash():X16}, equal: {replay.ComputeHash() == corrected}");
```

```text
frame 120: hash A30B068101DD5CBF, sounds 3
frame 120 after rollback: hash 3013CDBC50D11693, sounds 3
replayed from scratch: hash 3013CDBC50D11693, equal: True
```

## The pieces

- **`Warmup()`**, after the bodies exist, sizes storage and exercises the snapshot, restore and hash paths, so steady-state steps, `TrySnapshot` and `Restore` allocate nothing.
- **`GetSnapshotSizeBound()`** is an upper bound on the snapshot size while no storage grows, so the ring is allocated once. `TrySnapshot` writes into it and returns false if it is too small.
- **`Restore`** validates the whole snapshot first and changes nothing if it is invalid. A `WorldId` you set keeps `Body` handles valid across restores, and in a freshly built world.
- **`SuppressCallbacks()`** keeps handlers quiet while frames replay, so the impact sound doesn’t play twice; polled events are still produced.
- **`ComputeHash()`** is what peers exchange every few frames to catch a desync.

## Good to know

Inputs must be applied the same way on every peer and in every replay. Writing `LinearVelocity += (0, 0)` on frames without input is not a no-op: it turns `-0` into `+0`, a different bit pattern, and the hashes part ways. Apply an input only when there is one, as `Advance` does.

The whole loop, `OptionChanges` and handle rules are in [rollback and lockstep](https://recursiveai.org/onephysics/docs/rollback/).
