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 3 of 4 of this article. Click here to view the first page.

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.

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.