Journal Entry

Rewinding the Frame: Implementing Deterministic Reverse-Time Mechanics in Unity

Leon Nduati
Rewinding the Frame: Implementing Deterministic Reverse-Time Mechanics in Unity

When gamers think about time manipulation in video games—from Braid to Prince of Persia—it feels magical. You miss a jump, hold down a key, and watch your character effortlessly glide backward through their own mistakes while shards of broken glass fly back together onto the table.

When you're the engineer tasked with building that mechanic inside a 2D Unity physics engine, the magic evaporates into a brutal arithmetic problem.

In K2Y: Kernel 2 Year, our puzzle-platformer built in collaboration with Hyperlúdica Studio, reverse-time isn’t a scripted cutscene. It is an active gameplay pillar where puzzles require rewinding moving platforms, falling hazards, and player momentum to navigate recursive rooms.

Here is what happens when you have to run a physics simulation backward without tearing your framerate to ribbons.


1. Why Rigidbody2D Hates Going in Reverse

Unity’s PhysX/Box2D engine is fundamentally forward-marching. Every physics step (FixedUpdate), the engine takes the current velocity vectors, applies acceleration and gravity, checks for collider intersections, resolves impulses, and writes back new positions.

You cannot simply tell Box2D: “Run the last frame in reverse.” The mathematical solver is non-deterministic in reverse; floating-point rounding errors and impulse damping mean that if you invert velocity, objects don't retrace their steps—they drift, wobble, and clip through solid geometry.

To achieve exact, deterministic rewinds, you have to seize control of the object’s reality.

// Struct representing an immutable state snapshot at fixed delta intervals
public struct TimeSnapshot
{
    public Vector2 Position;
    public float Rotation;
    public Vector2 Velocity;
    public float AngularVelocity;
    public bool IsGrounded;
    public byte AnimationStateHash;
}

When time is moving forward, every rewindable entity records its physical state into a circular buffer every FixedUpdate (every 0.02s).


2. The Garbage Collector Nightmare of Recording Everything

The naïve approach to state recording is a List<TimeSnapshot>. Every frame, you do snapshots.Add(newSnapshot). If the list exceeds five seconds of gameplay (250 frames), you call snapshots.RemoveAt(0).

Do that on three player clones and twenty falling debris blocks in a WebGL browser build, and your game will stutter every three seconds.

Why? Because shifting array elements in memory and continuously allocating struct wrappers triggers Unity’s Mono/IL2CPP Garbage Collector. In WebGL, when the GC pauses the main thread for 16ms, you drop a frame. When it pauses for 32ms, the player misses a jump and closes the tab.

The fix was a strict zero-allocation ring buffer:

public class CircularTimeBuffer
{
    private readonly TimeSnapshot[] _buffer;
    private int _head = 0;
    private int _count = 0;
    private readonly int _capacity;

    public CircularTimeBuffer(int capacity)
    {
        _capacity = capacity;
        _buffer = new TimeSnapshot[capacity];
    }

    public void Record(TimeSnapshot snapshot)
    {
        _buffer[_head] = snapshot;
        _head = (_head + 1) % _capacity;
        if (_count < _capacity) _count++;
    }

    public bool TryRewind(out TimeSnapshot snapshot)
    {
        if (_count == 0)
        {
            snapshot = default;
            return false;
        }

        _head = (_head - 1 + _capacity) % _capacity;
        snapshot = _buffer[_head];
        _count--;
        return true;
    }
}

By pre-allocating an array of 300 structs on Awake() and advancing integer pointers, allocations drop to zero bytes per frame during active gameplay. The memory footprint remains completely flat.


3. The Audio Problem: Scrubbing Without Popping

Visual rewinds are only half the battle. If your visuals rewind smoothly but the game audio continues playing forward happily, the illusion breaks instantly.

In K2Y, we routed audio through FMOD Studio.

When the player triggers the rewind key:

  1. Active music and sound effects have their pitch parameters ramped down smoothly from 1.0 to -1.0 using a DSP pitch-shifter.
  2. The playback head is scrubbed backward along the track timeline.
  3. A low-pass filter immediately rolls off frequencies above 800 Hz, adding a muffled, aquatic weight to the reverse soundscape.

If you don't crossfade audio buffers during the direction flip, the audio hardware encounters a phase discontinuity, resulting in an ear-piercing digital "click." By applying a 10ms micro-fade during state transition, the rewind sounds like an old magnetic cassette tape being violently spun in reverse.


4. Mechanics That Mirror Theme

In K2Y, rewinding time wasn't just a gimmick to forgive bad platforming. It was an allegory for memory—the obsessive human urge to revisit a moment that has already passed, hoping you can make it turn out differently.

Engineers often treat game mechanics as isolated technical puzzles. But the best mechanics are the ones where the code constraints directly reinforce what the game is trying to say.

End