Game Replays from Scratch: A Beginner's Guide

Posted by Gemma Ellison
./
July 14, 2025

Let’s face it: most tutorials on game replays dive headfirst into optimization and complex data structures before you’ve even grasped the fundamentals. That’s a recipe for confusion and frustration. We’re going to do things differently. This isn’t about building a production-ready, hyper-efficient system. This is about understanding how replays work at their core. We’re ditching the premature optimization and embracing clear, commented code that you can actually follow. Prepare to build a functional replay system from scratch, and, more importantly, understand why it works.

Recording Game Data: Position and Rotation

The heart of any replay system is the ability to capture the state of the game world over time. In its simplest form, this means recording the position and rotation of the objects you want to replay. Let’s focus on the player character.

Here’s the crucial part: we need to store this data in a way that allows us to recreate the game state later. A simple list or array of TransformSnapshot objects will do.

using UnityEngine;
using System.Collections.Generic;

[System.Serializable]
public struct TransformSnapshot
{
    public Vector3 position;
    public Quaternion rotation;
    public float timestamp;
}

public class ReplayRecorder : MonoBehaviour
{
    public List<TransformSnapshot> recordedData = new List<TransformSnapshot>();
    public float recordingInterval = 0.05f; // Record every 0.05 seconds
    private float timer = 0f;

    void Update()
    {
        timer += Time.deltaTime;
        if (timer >= recordingInterval)
        {
            RecordFrame();
            timer -= recordingInterval;
        }
    }

    void RecordFrame()
    {
        TransformSnapshot snapshot;
        snapshot.position = transform.position;
        snapshot.rotation = transform.rotation;
        snapshot.timestamp = Time.time; // Use Time.time for accurate timing
        recordedData.Add(snapshot);
    }

    public void ClearRecording()
    {
        recordedData.Clear();
    }
}

Common Pitfall: Forgetting to use Time.time for the timestamp. Time.deltaTime is useful for movement, but Time.time gives you the actual time since the game started, crucial for accurate replay timing.

Attach this script to the player GameObject. Every recordingInterval seconds, it will store the current position, rotation, and timestamp into the recordedData list. Consider setting recordingInterval carefully; too high, and the replay will be jerky, too low, and you risk performance issues (more on that later, or rather, not in this article!).

Smooth Playback: Interpolation is Key

Simply setting the position and rotation of the GameObject directly from the recorded data will result in a choppy, unpleasant replay experience. The solution? Interpolation. We need to smoothly transition between recorded snapshots.

using UnityEngine;
using System.Collections.Generic;

public class ReplayPlayer : MonoBehaviour
{
    public List<TransformSnapshot> replayData;
    private int currentFrame = 0;
    private float playbackStartTime;

    public void StartReplay(List<TransformSnapshot> data)
    {
        replayData = data;
        currentFrame = 0;
        playbackStartTime = Time.time; // Store the replay start time
    }

    void Update()
    {
        if (replayData == null || replayData.Count == 0) return;

        float replayTime = Time.time - playbackStartTime; // Calculate time since replay started
        int nextFrame = currentFrame + 1;

        if (nextFrame >= replayData.Count)
        {
            Debug.Log("Replay Finished!");
            return; // Replay finished
        }

        TransformSnapshot currentSnapshot = replayData[currentFrame];
        TransformSnapshot nextSnapshot = replayData[nextFrame];

        // Interpolate only if there is a next frame, and replayTime is between the current and next timestamps
       if (replayTime >= currentSnapshot.timestamp && replayTime <= nextSnapshot.timestamp)
        {
            float lerpFactor = Mathf.InverseLerp(currentSnapshot.timestamp, nextSnapshot.timestamp, replayTime);
            transform.position = Vector3.Lerp(currentSnapshot.position, nextSnapshot.position, lerpFactor);
            transform.rotation = Quaternion.Slerp(currentSnapshot.rotation, nextSnapshot.rotation, lerpFactor);
        } else {
            //If it's outside the timeframe, move to the next frame
            currentFrame++;
             if (currentFrame + 1 >= replayData.Count)
            {
                Debug.Log("Replay Finished!");
                return; // Replay finished
            }
        }
    }
}

Explanation:

  1. Mathf.InverseLerp: This function calculates a lerpFactor (a value between 0 and 1) representing how far along we are between the currentSnapshot.timestamp and the nextSnapshot.timestamp, based on the current replayTime.

  2. Vector3.Lerp and Quaternion.Slerp: These functions perform the interpolation, smoothly transitioning between the position and rotation values of the two snapshots based on the lerpFactor. Slerp is crucial for rotations as it avoids gimbal lock issues that Lerp can cause with Quaternions.

  3. We increment currentFrame when replayTime exceeds nextSnapshot.timestamp.

Attach this script to a new GameObject (don’t attach it to the original player). This object will represent the “ghost” or replay of the player. Make sure to copy the recordedData from the ReplayRecorder to the ReplayPlayer's replayData property before calling StartReplay().

Challenge: Handling Variable Frame Rates. The recordingInterval is an approximation. The actual time between frames might vary. This is why using timestamps and InverseLerp is crucial for smooth playback regardless of recording frame rate variations.

Basic Analysis: Comparing Replays

Replay systems aren’t just for visual playback; they’re a goldmine for analysis. Imagine comparing a player’s performance across multiple runs.

Here’s a simple example: calculate the total distance traveled during a replay.

public class ReplayAnalyzer
{
    public static float CalculateDistanceTraveled(List<TransformSnapshot> replayData)
    {
        if (replayData == null || replayData.Count < 2) return 0f;

        float totalDistance = 0f;
        for (int i = 1; i < replayData.Count; i++)
        {
            totalDistance += Vector3.Distance(replayData[i - 1].position, replayData[i].position);
        }
        return totalDistance;
    }
}

You can then use this function after the replay has finished. Consider integrating other metrics such as jump count, time spent in specific areas, or enemy encounters to create a rich profile of the player’s performance.

Actionable Insight: Visualize the replay data. Create a heat map showing where the player spent the most time, or a graph plotting their speed over time. Visualizations can reveal patterns that are difficult to spot from raw data.

Where to Go From Here

This is a basic replay system. There are countless ways to expand upon it. Here are a few ideas:

  • Network Replays: Record and playback data from networked games. This introduces challenges related to data synchronization and latency.
  • Optimizations: Implement data compression to reduce the size of the replay files.
  • Rewinding: Allow players to rewind the replay. This requires storing more data and implementing a more complex interpolation system.

This article gives you a solid foundation to build upon. Experiment, iterate, and don’t be afraid to break things. The best way to learn is by doing. Now go build something awesome!