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. By Teddy Engel.

Leave a rating/review
Download materials
Save for later
Share
You are currently viewing page 2 of 4 of this article. Click here to view the first page.

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.

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

creating a replay still just a birdy

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

Teddy Engel

Contributors

Teddy Engel

Author

Margaret Moser

Tech Editor

Aleksandra Kizevska

Illustrator

Ben MacKinnon

Final Pass Editor

Over 300 content creators. Join our team.