Home · Unity Tutorials

Creating a Replay System in Unity

A replay system is a great way to let players relive their best (or worst) moments of gameplay, improve their strategy, and more! In this tutorial, you’ll build a simple state-based replay system and learn about how replay systems work.

5/5 2 Ratings

Version

  • C# 3.5, Unity 2019.3, Unity

A video camera captures a live event

A replay system is a great way to let players relive their best, or worst, moments of gameplay, improve their strategy and more!

You could choose to use one from the Asset Store, but I’ll assume you’re reading this wearing a red bandana and with an army knife between your teeth, ready for a challenge. :]
In this tutorial, you’ll learn:

  • What a replay system is.
  • The difference between state-based and input-based replay systems.
  • How to implement a state-based replay system from scratch.
Note: This is an advanced-level tutorial, which assumes you’ve been using Unity for a while. You should also have a solid understanding of C#. Although the sample project uses Unity 2019.3, everything in this tutorial should work in older versions.

Getting Started

Download the project files by clicking the Download Materials button at the top or bottom of this tutorial.

Screenshot of demo project showing the main character, Birdy

Go to RW/Scenes and open the Main scene. It’s already set up with a sprite, a character controller, buttons for the replay system and some catchy 8-bit music.

Credits: The upbeat music is from PlayOnLoop. The platform sprites are from the Asset Store package Free 8-Bit Pixel Pack by Super Icon Ltd. Birdy is part of the Asset Store package Sprite Pack #1 – Tap and Fly by G.E. Team.

Press the Play button and give it a go. Control Birdy by using the left and right arrows and space bar to jump.

Click the Start recording / Stop recording and Start replay / Stop replay buttons. The buttons update when you click them, but the system doesn’t actually record or replay anything yet. That’s where you’ll come in. :]

What is a Replay System?

A replay system is an in-game system that lets the player record a gameplay sequence and replay it, just like recording a video with a phone and playing it again.

For the purposes of this tutorial:

  • Record means to capture a sequence of frames rendered by the game.
  • Replay means to play back the captured sequence, in the same order, from the beginning.
  • The system will only capture one sequence at a time. Pressing Record again will overwrite the previous sequence.
  • The system will record the sequence in RAM. This tutorial doesn’t cover saving and loading a sequence from the disk.
  • Player input will still be active during the replay playback. This won’t be a big issue anyway as you’ll update the states every frame.

Choosing the Approach: State-based or Input-based

There are two popular types of replay systems: State-based and input-based. While they do the same thing, each approach has its pros and cons. SO, what’s the difference?

What is State-based Replay?

When you record with a state-based system, you capture a sequence of states. A state is a snapshot of the properties of an entity.

For example, in a Unity game, you might want to record and replay the position of the player character and their awesome boomerang weapon.

At frame 1, you’d record these positions:

  • Player character: (X:0, Y:1, Z:0)
  • Boomerang: (X:0, Y:2, Z:1)

When Unity processes the next frame of gameplay, both objects move. So, for frame two you’d store these values:

  • Player character: (X:0, Y:1, Z:1)
  • Boomerang: (X:1, Y:1, Z:1)

If the player is Object 1 and the boomerang is Object 2, the memory would look like this:

Diagram of memory slots storing position of player and boomerang GameObjects

In a state-based system, you replay the states by reading the frames in order and applying the saved values to your GameObjects.

So, if you were replaying the data above, at frame one you’d assign these positions:

  • Player character: (X:0, Y:1, Z:0)
  • Boomerang: (X:0, Y:2, Z:1)

At frame two, you’d assign these positions:

  • Player character: (X:0, Y:1, Z:1)
  • Boomerang: (X:1, Y:1, Z:1)

And so on for as many frames as you’ve saved.

Pros and Cons of State-Based Replay

The main advantages of a state-based system are:

  • Determinism: It’ll always give the same output, even if the underlying engine is non-deterministic.
  • Replay filtering: You’re able to select with great granularity what you want to record. For example, you could record and replay only the player position, while things like tree leaves and clouds still move independently.
  • Simplicity: It’s simple to implement.
  • No context needed: It’s easy to replay at anytime. You don’t have to set up a scene context first.

The main weakness is:

  • Memory footprint: If you start recording a lot of states, for example, a lot of frames, the replay’s memory usage of will skyrocket.

Time to look at the input-based approach to compare.

What is Input-based Replay?

When you record with an input-based system, you capture the initial state of your objects, then capture a sequence of inputs. Inputs might be touch gestures, joystick values or keypresses, depending on your game.

Instead of recording the state of individual GameObjects each frame, you record their state only once, in the very first frame of the game. Then you store the inputs that might alter their states.

For example, at the beginning of your game you’d store the positions of all the GameObjects you’re including in the replay. If your player moves to the right and then crouches in frame one, you’d record the information like this:

Diagram of memory slots storing input received in each frame

  1. At frame zero, which isn’t shown, you record the state of the entire scene.
  2. You record a directional input to the right and a directional input down at frame one.
  3. At frame two, you record a directional input down.
  4. You record a directional input down at frame three.

When you replay with an input-based system, first you set the entire state back to what it was at the beginning of the playback. Then, in each frame, you apply the inputs you recorded for that frame.

In this example, at frame zero, you’d reset the scene state, So, you’d read all of the states you recorded at frame zero and write the information back to your GameObjects. At frame one, you’d simulate a directional input to the right and down, at frame two a directional input down and so on for all the frames of input you’ve recorded.

Pros and Cons of Input-Based Replay

The main advantage of an input-based system is:

  • Memory footprint: Since you only record the inputs after the first frame, you store a lot less in memory than you would with a state-based system.

The main weakness is:

  • Relies on determinism: To use this approach, you need to have a deterministic game or game engine. This means when you replay the inputs you recorded, it needs to give exactly the same output every time.

Deterministic vs Non-Deterministic Game Engines

To decide what kind of replay system to create for your game, you need to consider the game engine you’re working with. If you’re working with a non-deterministic game engine, then an input-based system won’t deliver consistent results.

There’s good news and bad news. I’ll start with the bad news.

The physics in Unity are non-deterministic. If you store the same Unity scene three times, then apply the same inputs to physics-enabled objects, your replays will look different. This can get much worse if you have many inputs in a row, since the discrepancies add up.

If you want to use an input-based system for a physics-based game in Unity, you have to handle the physics yourself.

What’s the good news, you ask? You won’t need a physics degree for this tutorial! For the sake of simplicity, you’ll make a state-based replay system.

Phew, the theory is out of the way! Time to get to it.

Setting Up to Record

Take a look at the Main scene again. The UI panel has a ReplayPanelController component, which listens to events fired from ReplayManager.cs to update the states of the two buttons.

The two buttons trigger events in the ReplayManager where, you might have guessed, the replay logic resides.

creating a replay manager

ReplayManager.cs is the only file you need to edit to turn Birdy into a movie star. It contains the replay flow logic and the main methods the system needs.

Finding the Transforms

First, you need to decide what states you’ll record. Keep it simple and record the position of all the GameObjects in the Main scene.

You need to find all the transform components when the script runs for the first time and store them in an array.

So, declare an array at the top of ReplayManager.cs:

private Transform[] transforms;

Now add this to Start:

transforms = FindObjectsOfType<Transform>();

Now that you have the transforms, you need to store the position states as the player moves around.

Recording Action Frames

As mentioned above, you will save your replay into runtime memory only, so you don’t need to worry about creating save files. However, you do still need to be able to write and read states into the runtime memory. And C# has just the structure you need!

What is a MemoryStream?

A little birdy told me you can’t have enough long theory talks, so it’s time to cover a little more. :]

Got to get that theory in

You’ve probably heard some form of this pearl of programming wisdom: Choosing the right data structure is winning half the battle. It’s never been truer than here!

You need to save a position, three float values, for every GameObject every frame, and then apply those values to the original transforms when you replay. Is there a simple data structure that stores ordered numerical values though? I wonder…

An array, of course! An array would work fine here but C# provides an even more suitable data structure called MemoryStream.

A MemoryStream lets you Write, or save, to it, Read, or load, from it, and Seek to a different position, making it easy to move to the beginning. Sounds quite similar to the controls you would expect on a replay, doesn’t it?

Use it as your main data structure. You’ll use a BinaryWriter to write to the MemoryStream and a BinaryReader to read from the stream.

Setting up a MemoryStream

First, add this requirement at the top of ReplayManager.cs:

using System.IO;

Now declare these three variables at the top of the class:

private MemoryStream memoryStream = null;
private BinaryWriter binaryWriter = null;
private BinaryReader binaryReader = null;

You need to initialize all three, that is, create empty instances, before you use them. You could do this in Start, but that would be a waste if the player never actually clicks Start Recording. Instead, set them up the first time the player starts recording.

Declare another variable at the top of ReplayManager:

private bool recordingInitialized;

Now, add the following new method before StartRecording:

private void InitializeRecording()
{
    memoryStream = new MemoryStream();
    binaryWriter = new BinaryWriter(memoryStream);
    binaryReader = new BinaryReader(memoryStream);
    recordingInitialized = true;
}

Notice that you pass a MemoryStream as an argument to the BinaryWriter and BinaryReader. You do this because the binary helpers need to know what you’re writing to or reading from.

Now invoke your new method at the beginning of StartRecording:

if (!recordingInitialized)
{
    InitializeRecording();
}

This code initializes the stream so you can read and write information, but it doesn’t set a specific stream position for writing.This means you’ll keep writing or reading from the end of the stream forever! Not good.

Starting at the Beginning

Every time the player starts a new recording, three things need to happen:

  1. You need to reset the size of memoryStream to zero, to clear the previous recording.
  2. The position in memoryStream needs to be set to the beginning.
  3. The position in binaryWriter needs to be set to the beginning.

To take care of the first item, add this immediately after the if statement above to reset memoryStream‘s size:

else
{
    memoryStream.SetLength(0);
}

Now you only need to reset the position. Find StopReplaying and add this method underneath:

private void ResetReplayFrame()
{
    memoryStream.Seek(0, SeekOrigin.Begin);
    binaryWriter.Seek(0, SeekOrigin.Begin);
}

This sets the internal position to zero bytes from the beginning, which is exactly what you want.

Finally, call your new method in StartRecording, right below the code you added above and above recording = true;.

ResetReplayFrame();

Writing Transform Positions to the Stream

Now the fun bit: Actually recording the transform positions.

You might have noticed that ReplayManager has a FixedUpdate method but no Update. FixedUpdate lets you control the length of a recording frame, so you can reproduce game state consistently. Determinism, remember?

Every frame, FixedUpdate will call UpdateRecording if the game is currently recording, or UpdateReplaying if the game is playing back the recording.

All you need to do is go through the list of stored Transform components and use binaryWriter to write the x, y, and z values for the position of each one.

First, add this new method at the end of ReplayManager:

private void SaveTransform(Transform transform)
{
    binaryWriter.Write(transform.localPosition.x);
    binaryWriter.Write(transform.localPosition.y);
    binaryWriter.Write(transform.localPosition.z);
}

Right above that, add this method to loop over the transforms and call SaveTransform for each one:

private void SaveTransforms(Transform[] transforms)
{
    foreach (Transform transform in transforms)
    {
        SaveTransform(transform);
    }
}

Finally, add this to UpdateRecording:

SaveTransforms(transforms);

That’s all you need to save the transform position states! Take a moment to run the scene and make sure your code doesn’t generate any errors.

creating a replay still just a birdy

Still just a bird jumping around, but we haven’t broken anything, yet!

Replaying Action Frames

Your game already records everything correctly when you press Start Recording. But you can’t see your system working yet! It’s time to replay.

Starting at the Beginning

When the playback starts, you need to reset the position you’re reading from in memoryStream. Otherwise the internal position will always be at the end of the stream, where there’s nothing to read!

You already created a method called ResetReplayFrame that does this, so you only need to call it. Add this at the beginning of StartReplaying:

ResetReplayFrame();

Reading Positions From the Stream

Every frame, you loop over the entire list of transform components. For each one, you look at memoryStream and read the three floats representing its position. Then you use those values to update the transform’s localPosition.

First add this method at the end of the class:

private void LoadTransform(Transform transform)
{
    float x = binaryReader.ReadSingle();
    float y = binaryReader.ReadSingle();
    float z = binaryReader.ReadSingle();
    transform.localPosition = new Vector3(x, y, z);
}

This method sets the position of one transform. You call binaryReader.ReadSingle() three times to read the values for x, y and z from memoryStream. Then you set the transform’s local position using those values.

Now you need to loop over the transforms and load them in the same order they were added to the array. Add this method above LoadTransform:

private void LoadTransforms(Transform[] transforms)
{
    foreach (Transform transform in transforms)
    {
        LoadTransform(transform);
    }
}

Finally, call your loading logic in UpdateReplaying:

LoadTransforms(transforms);

This code starts from the beginning of the memory stream and loads each frame in order, reproducing exactly what the game was doing during recording. Awesome!

But when it reaches the end of the memory stream, it just keeps going, because you haven’t told it to stop. You might say it’s a little … bird-brained. :]

If you played back a replay now, you would start to get EndOfStreamException errors:
creating a replay bird-brained-memoryStream

Fix it by adding this code at the beginning of UpdateReplaying:

if (memoryStream.Position >= memoryStream.Length)
{
    StopReplaying();
    return;
}

Now run the scene. Start recording and move Birdy around the level. Stop recording and click Start Replay to see all Birdy’s moves play back like you recorded them.

creating a replay recording and replaying

Give yourself a high five!

You might notice a small issue: The system doesn’t record the direction Birdy is facing! Time to fix that.

Recording the Direction

It’s quite simple to add more elements to save and load so your replay becomes more precise. Be mindful, though, that every new element you track means the replay will use more memory every frame.

If you open CharacterController2D.cs, you can see that Birdy’s direction is updated by multiplying the localScale.x property of the transform by 1, to face right or -1, to face left. So, to record the direction Birdy is facing, you need to save and load the scale as well as the position.

To store the scale each frame, add this to the bottom of SaveTransform:

binaryWriter.Write(transform.localScale.x);
binaryWriter.Write(transform.localScale.y);
binaryWriter.Write(transform.localScale.z);

Then add this at the end of LoadTransform to read and apply the scale when you replay:

x = binaryReader.ReadSingle();
y = binaryReader.ReadSingle();
z = binaryReader.ReadSingle();
transform.localScale = new Vector3(x, y, z);
Note: Order matters. Always write and read replayable properties in the same order, or the result will get mixed up when you replay. In this case, you write position before scale, so when you play it back you read and load the position first, then read and load the scale.

Run the scene again, then record and replay your movements. Birdy is a real movie star!

creating-a-replay-facing-the-right-way

Optimizing Memory Usage

You now have everything you need for a working replay system. But this is an intentionally small example. If you need to track even a few more properties in a state-based system, you may need to save and load much more information.

Time to take a look at some simple optimizations.

Capping Replay Duration

Even if you don’t intend to do any deep optimization, limiting the replay to a specific number of frames is easy and eliminates the risk of running out of memory if your player leaves the game running.

A good value for maximum length depends on your game: It could be ten seconds or two minutes. For this tutorial, cap replays at six seconds. Assuming the game runs at 60 frames per second, you need to record 60 frames a second for six seconds, or a total of 360 frames.

Add these two variables at the top of ReplayManager.cs:

private int currentRecordingFrames = 0;
public int maxRecordingFrames = 360;

Add this at the bottom of UpdateRecording to keep track of how many frames you’ve recorded:

++currentRecordingFrames;

Then go to the top of the same method and add this:

if (currentRecordingFrames > maxRecordingFrames)
{
    StopRecording();
    currentRecordingFrames = 0;
    return;
}

Each time the method is called, it checks to see if you’ve already recorded too many frames. If so, it stops the recording and exits. Otherwise, it proceeds and records the next frame.

Run the scene again and click Start Recording. After a few seconds, you’ll see that the button text automatically resets and Start Replay becomes active, indicating the recording stopped itself. Click Start Replay and you’ll see the replay only captures the first six seconds of gameplay.

creating a replay time limit

Challenge: Skipping Frames

Another way to reduce the memory footprint is to skip some frames as you record.

For example, skipping every other frame will reduce the footprint by 50 percent, a huge savings when the replay gets lengthy. This is a balancing act: The more frames you skip, the less accuracy you have in the replay. But often skipping one or even two frames won’t make much of a difference from the player’s perspective.

Time for a challenge! How would you implement skipping frames? Click Solution to find out.

Hint: You need to skip the frames in both recording and replay, otherwise the replay looks like it’s fast-forwarded!

[spoiler title=”Solution”]

Counting Frames

Declare two variables in ReplayManager.cs:

public int replayFrameLength = 2;
private int replayFrameTimer = 0;

You start replayFrameTimer at whatever number of frames you want to count. As each Unity frame passes, you subtract one. When replayFrameTimer reaches zero, you record, or replay, a frame and start the replayFrametimer again. This lets you skip frames.

First, add these methods below StopReplaying:

private void ResetReplayFrameTimer()
{
    replayFrameTimer = replayFrameLength;
}

private void StartReplayFrameTimer()
{
    replayFrameTimer = 0;
}

Whenever you start to replay or record, you’re already calling ResetReplayFrame so you start from the beginning of the memory stream. Now, you also need to reset the frame timer to zero so you immediately record or replay the first frame.

Using the Counter

Look in both StartReplaying and StartRecording and add this after the call to ResetReplayFrame:

StartReplayFrameTimer();

Next, find UpdateRecording and replace the call to SaveTransforms with this:

if (replayFrameTimer == 0)
{
    SaveTransforms(transforms);
    ResetReplayFrameTimer();
}
--replayFrameTimer;

This code checks if replayFrameTimer is ready for the next save, at zero. If so, you save the transforms and set the timer back to the total length, two.

Now you’ll do the same in UpdateReplaying. Replace the call to LoadTransforms in that method with this:

if (replayFrameTimer == 0)
{
    LoadTransforms(transforms);
    ResetReplayFrameTimer();
}
--replayFrameTimer;

Run the scene, record some gameplay and play it back. Notice the result is still pretty smooth. Update the Replay Frame Length on the Replay Manager component to five and try again. A lot less smooth!
[/spoiler]

Congratulations on completing the tutorial! You now have the power to control time itself!

Where to Go From Here?

You can download the completed project files by clicking the Download Materials button at the top or bottom of the tutorial.

In this tutorial, you’ve learned how to implement a simple state-based replay system along with some small optimizations. There are many interesting places to go from here. You could store your replay information in a file, implement an input-based system or try integrating other components like the Animator.

It’s up to you!

Here are some resources that may help you on your way:

Feel free to comment below if you have any comments or questions! :]

Reference

Replay System Using Unity
Implementing a replay system in Unity and how I’d do it differently next time
Developing Your Own Replay System

Average Rating

5/5

Add a rating for this content

2 ratings

More like this

Contributors

Comments