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.
Sign up/Sign in
With a free Kodeco account you can download source code, track your progress, bookmark, personalise your learner profile and more!
Create accountAlready a member of Kodeco? Sign in
Sign up/Sign in
With a free Kodeco account you can download source code, track your progress, bookmark, personalise your learner profile and more!
Create accountAlready a member of Kodeco? Sign in
Sign up/Sign in
With a free Kodeco account you can download source code, track your progress, bookmark, personalise your learner profile and more!
Create accountAlready a member of Kodeco? Sign in
Contents
Creating a Replay System in Unity
25 mins
- Getting Started
- What is a Replay System?
- Choosing the Approach: State-based or Input-based
- What is State-based Replay?
- Pros and Cons of State-Based Replay
- What is Input-based Replay?
- Pros and Cons of Input-Based Replay
- Deterministic vs Non-Deterministic Game Engines
- Setting Up to Record
- Finding the Transforms
- Recording Action Frames
- What is a MemoryStream?
- Setting up a MemoryStream
- Starting at the Beginning
- Writing Transform Positions to the Stream
- Replaying Action Frames
- Starting at the Beginning
- Reading Positions From the Stream
- Recording the Direction
- Optimizing Memory Usage
- Capping Replay Duration
- Challenge: Skipping Frames
- Counting Frames
- Using the Counter
- Where to Go From Here?
- Reference
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:
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.
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);
Run the scene again, then record and replay your movements. Birdy is a real movie star!
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.
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.