Adding an Action Replay to a Game - Unity Game Development Tutorial

In this Unity game development tutorial we're going to look at how we can add an action replay to our game. We'll look at how to add a standard replay, a rewind, a slow motion replay, and how to pause on any part of the replay.

You can either watch the video version below or continue reading for written instructions.

Right, we're going to start with the project we created in our 'Target Selection' tutorial.

In this tutorial, we created a tower of blocks that we can target with the mouse, before clicking the left mouse button to fire the ball.

Clicking the target to fire the ball

To add an action replay to our scene, we need to record what has happened in each frame. So the first thing we'll do is create a C# class to hold the information we want to record. To do this we'll click on the plus button on the project panel and select C# script.

We'll call this script ActionReplayRecord. We'll double click to open it in Visual Studio and change it to match the following.

using UnityEngine;

public class ActionReplayRecord
{
    public Vector3 position;
    public Quaternion rotation;
}

This class doesn't need to derive from MonoBehaviour so we've deleted this, and the Start and Update methods.

For our replay to work we're going to need to record the position of our objects, so we've added a public Vector3 field for this. We also want to record the rotation of our objects, so we've added another field for this.

Now we have a class to hold the information, we need to record this for each object every frame. To do this we'll save the script and switch back to Unity.

We'll select the ball in the Hierarchy, and then we'll click on 'Add Component' in the Inspector and search for the script component.

We'll call this script ActionReplay.

Creating the action replay script

Let’s double click the script to open it in Visual Studio.

To hold all the records for our replay, we'll create a list of our action replay records.

using System.Collections.Generic;
using UnityEngine;

public class ActionReplay : MonoBehaviour
{
    private List<ActionReplayRecord> actionReplayRecords = new List<ActionReplayRecord>();

    void Start()
    {

    ...

Now we need to add to this list every frame. The Update method might seem like the obvious place for this but the rate that this method is called varies depending on the frame rate.

Instead, we'll use the Fixed Update method which is called on a fixed interval. In here, we'll add a new record to the list and we'll store the current position and rotation of the object.

    ...

    void Update()
    {
    }

    private void FixedUpdate()
    {
        actionReplayRecords.Add(new ActionReplayRecord { position = transform.position, rotation = transform.rotation });
    }
}

Now we're recording the history of the object, we need a way to switch to replay mode. To know whether we're in replay mode, we’ll create a boolean field.

using System.Collections.Generic;
using UnityEngine;

public class ActionReplay : MonoBehaviour
{
    private bool isInReplayMode;
    private List<ActionReplayRecord> actionReplayRecords = new List<ActionReplayRecord>();

    ...

Then in the Update method we'll check for the 'R' key being pressed. If it is, we'll toggle the boolean field. So every time the 'R' key is pressed it will switch between normal and replay mode.

    ...

    void Update()
    {
        if (Input.GetKeyDown(KeyCode.R))
        {
            isInReplayMode = !isInReplayMode;
        }
    }

    ...
   

We'll then create an 'if else' statement to check whether we are coming in or out of replay mode.

    ...

    void Update()
    {
        if (Input.GetKeyDown(KeyCode.R))
        {
            isInReplayMode = !isInReplayMode;

            if (isInReplayMode)
            {
            }
            else
            {
            }
        }
    }

    ...

The first thing we need to do when we enter replay mode is to set the position and the rotation of our object back to the first frame we recorded, and when we exit replay mode, we want to set our object back to the last frame.

To help us with this, we'll create a method to set the transform of our object. We'll add an index parameter to our method so we know which recorded frame we want to set. 

    ...

    private void FixedUpdate()
    {
        actionReplayRecords.Add(new ActionReplayRecord { position = transform.position, rotation = transform.rotation });
    }

    private void SetTransform(int index)
    {
    }
}

In here, we’ll get the relevant record from our list, and then we'll set the position and rotation of our object to that of the stored record.

    ...

    private void SetTransform(int index)
    {
        ActionReplayRecord actionReplayRecord = actionReplayRecords[index];

        transform.position = actionReplayRecord.position;
        transform.rotation = actionReplayRecord.rotation;
    }
}

Next we'll go back to our Update method and call this method. If we are entering replay mode, we'll set the transform to the first frame, and if we are exiting replay mode we'll set the transform back to the last frame.

    ...

    void Update()
    {
        if (Input.GetKeyDown(KeyCode.R))
        {
            isInReplayMode = !isInReplayMode;

            if (isInReplayMode)
            {
                SetTransform(0);
            }
            else
            {
                SetTransform(actionReplayRecords.Count - 1);
            }
        }
    }

    ...

At the moment we're recording every frame, even when we're in replay mode. This isn't what we want, so we'll change our FixedUpdate method to check we aren't in replay mode before adding to our list.

    ...

    private void FixedUpdate()
    {
        if (isInReplayMode == false)
        {
            actionReplayRecords.Add(new ActionReplayRecord { position = transform.position, rotation = transform.rotation });
        }
    }

    ...

One other thing we need to do is prevent our object responding to physics when we’re in replay mode. To do this we need access to the Rigidbody component of the object. 

We’ll create a field to store this.

using System.Collections.Generic;
using UnityEngine;

public class ActionReplay : MonoBehaviour
{
    private bool isInReplayMode;
    private Rigidbody rigidbody;
    private List<ActionReplayRecord> actionReplayRecords = new List<ActionReplayRecord>();

    ...

And we'll get this component in the Start method.

    ...

    void Start()
    {
        rigidbody = GetComponent<Rigidbody>();
    }

    ...

Now, in the Update method, when we enter replay mode, we'll set the IsKinematic property of the Rigidbody to true. 

    ...

    void Update()
    {
        if (Input.GetKeyDown(KeyCode.R))
        {
            isInReplayMode = !isInReplayMode;

            if (isInReplayMode)
            {
                SetTransform(0);
                rigidbody.isKinematic = true;
            }
            
            ...

This will stop the object responding to forces and collisions.

When we exit replay mode we’ll set this value back to false.

    ...

    void Update()
    {
        if (Input.GetKeyDown(KeyCode.R))
        {
            isInReplayMode = !isInReplayMode;

            if (isInReplayMode)
            {
                SetTransform(0);
                rigidbody.isKinematic = true;
            }
            else
            {
                SetTransform(actionReplayRecords.Count - 1);
                rigidbody.isKinematic = false;
            }
        }
    }

        
    ...

This script should now look as follows.

using System.Collections.Generic;
using UnityEngine;

public class ActionReplay : MonoBehaviour
{
    private bool isInReplayMode;
    private Rigidbody rigidbody;
    private List<ActionReplayRecord> actionReplayRecords = new List<ActionReplayRecord>();

    void Start()
    {
        rigidbody = GetComponent<Rigidbody>();
    }

    void Update()
    {
        if (Input.GetKeyDown(KeyCode.R))
        {
            isInReplayMode = !isInReplayMode;

            if (isInReplayMode)
            {
                SetTransform(0);
                rigidbody.isKinematic = true;
            }
            else
            {
                SetTransform(actionReplayRecords.Count - 1);
                rigidbody.isKinematic = false;
            }
        }
    }

    private void FixedUpdate()
    {
        if (isInReplayMode == false)
        {
            actionReplayRecords.Add(new ActionReplayRecord { position = transform.position, rotation = transform.rotation });
        }
    }

    private void SetTransform(int index)
    {
        ActionReplayRecord actionReplayRecord = actionReplayRecords[index];

        transform.position = actionReplayRecord.position;
        transform.rotation = actionReplayRecord.rotation;
    }
}

Let's save this script and switch back to Unity. 

We'll press play to try out what we have so far. If we click on a target, the ball will be fired and knock down the boxes. If we now press the 'R' key to enter replay mode, the ball returns to its original position.

Entering action replay mode

While we're in this mode, we can't move the ball and selecting a target has no effect. If we press 'R' to return to normal mode, the ball goes back to the position it was in before we entered replay mode, and we can move the ball again.

Exiting action replay mode

Currently our replay is only affecting the ball. Let's stop the game to apply it to our boxes as well. 

To do this, we'll select one of the boxes in the Hierarchy. We can then Click the Select Prefab button to find the prefab for the box.

Selecting the prefab for the box

We'll then click Add Component and add our Action Replay script. By adding the script to the prefab it will add it to all of our boxes.

Adding the action replay script to the box prefab

Let's press play to try this out.

We'll knock over the boxes again. Now if we switch to replay mode it resets the boxes as well, and if we exit replay mode, the boxes go back to where they were.

Entering and exiting action replay mode with script attached to boxes

So now we have everything in place to enter and exit replay mode, the next thing we want to do is actually play the replay.

We'll stop the game and switch back to our script.

What we want to be able to do is step through all our recorded transforms and replay them. To be able to do this, we'll add a new field to store the current index we're showing.

using System.Collections.Generic;
using UnityEngine;

public class ActionReplay : MonoBehaviour
{
    private bool isInReplayMode;
    private Rigidbody rigidbody;
    private List<ActionReplayRecord> actionReplayRecords = new List<ActionReplayRecord>();
    private int currentReplayIndex;

    ...    

We'll then set this in our SetTransform method.

    ...

    private void SetTransform(int index)
    {
        currentReplayIndex = index;

        ActionReplayRecord actionReplayRecord = actionReplayRecords[index];

        transform.position = actionReplayRecord.position;
        transform.rotation = actionReplayRecord.rotation;
    }
}

In our FixedUpdate method we'll add an 'else' statement. In here we'll add 1 to the current index to get the next index. We'll then call our SetTransform method with this new index to update to the next frame.

    ...

    private void FixedUpdate()
    {
        if (isInReplayMode == false)
        {
            actionReplayRecords.Add(new ActionReplayRecord { position = transform.position, rotation = transform.rotation });
        }
        else
        {
            int nextIndex = currentReplayIndex + 1;

            SetTransform(nextIndex);
        }
    }

    ...

At the moment, this code will keep adding 1 to the index forever. We need to stop it from going over the number of frames we have stored in our list. To do this, we'll add an 'if' statement to check if the next index is less than the number of records we have. Only if the next index is valid will we call the SetTransform method.

    ...

    private void FixedUpdate()
    {
        if (isInReplayMode == false)
        {
            actionReplayRecords.Add(new ActionReplayRecord { position = transform.position, rotation = transform.rotation });
        }
        else
        {
            int nextIndex = currentReplayIndex + 1;

            if (nextIndex < actionReplayRecords.Count)
            {
                SetTransform(nextIndex);
            }
        }
    }

    ...

The script should now look as follows.

using System.Collections.Generic;
using UnityEngine;

public class ActionReplay : MonoBehaviour
{
    private bool isInReplayMode;
    private Rigidbody rigidbody;
    private List<ActionReplayRecord> actionReplayRecords = new List<ActionReplayRecord>();
    private int currentReplayIndex;

    void Start()
    {
        rigidbody = GetComponent<Rigidbody>();
    }

    void Update()
    {
        if (Input.GetKeyDown(KeyCode.R))
        {
            isInReplayMode = !isInReplayMode;

            if (isInReplayMode)
            {
                SetTransform(0);
                rigidbody.isKinematic = true;
            }
            else
            {
                SetTransform(actionReplayRecords.Count - 1);
                rigidbody.isKinematic = false;
            }
        }
    }

    private void FixedUpdate()
    {
        if (isInReplayMode == false)
        {
            actionReplayRecords.Add(new ActionReplayRecord { position = transform.position, rotation = transform.rotation });
        }
        else
        {
            int nextIndex = currentReplayIndex + 1;

            if (nextIndex < actionReplayRecords.Count)
            {
                SetTransform(nextIndex);
            }
        }
    }

    private void SetTransform(int index)
    {
        currentReplayIndex = index;

        ActionReplayRecord actionReplayRecord = actionReplayRecords[index];

        transform.position = actionReplayRecord.position;
        transform.rotation = actionReplayRecord.rotation;
    }
}

Let's save the script and switch back to Unity.

We'll press play to try this out. We’ll knock over some boxes again and we’ll press 'R' to enter replay mode. Now we get an action replay of the boxes being knocked over!

TODO

This has given us the basic replay functionality. Next we're going to have a look at how we can add rewind, slow motion and pause to our replay.

Let's stop the game and switch back to the script.

We want to be able to control the speed and the direction of the replay, so we'll add a float field for the index change rate.

using System.Collections.Generic;
using UnityEngine;

public class ActionReplay : MonoBehaviour
{
    private bool isInReplayMode;
    private Rigidbody rigidbody;
    private List<ActionReplayRecord> actionReplayRecords = new List<ActionReplayRecord>();
    private float currentReplayIndex;
    private float indexChangeRate;

    ...

Then in our update method we'll initially set this to zero.

    ...

    void Update()
    {
        if (Input.GetKeyDown(KeyCode.R))
        {
            isInReplayMode = !isInReplayMode;

            if (isInReplayMode)
            {
                SetTransform(0);
                rigidbody.isKinematic = true;
            }
            else
            {
                SetTransform(actionReplayRecords.Count - 1);
                rigidbody.isKinematic = false;
            }
        }

        indexChangeRate = 0;
    }

    ...
   

We'll then check whether the right arrow key is being pressed. If it is, we want the replay to play in the forward direction so we’ll set the change rate to 1.

    ...

    void Update()
    {
        if (Input.GetKeyDown(KeyCode.R))
        {
            isInReplayMode = !isInReplayMode;

            if (isInReplayMode)
            {
                SetTransform(0);
                rigidbody.isKinematic = true;
            }
            else
            {
                SetTransform(actionReplayRecords.Count - 1);
                rigidbody.isKinematic = false;
            }
        }

        indexChangeRate = 0;

        if (Input.GetKey(KeyCode.RightArrow))
        {
            indexChangeRate = 1;
        }
    }

    ...

We'll then check whether the left arrow key is being pressed. If it is, we want the replay to play in reverse so we'll set the change rate to -1. 

        ...

        if (Input.GetKey(KeyCode.RightArrow))
        {
            indexChangeRate = 1;
        }

        if (Input.GetKey(KeyCode.LeftArrow))
        {
            indexChangeRate = -1;
        }
    }

    ...

    

We'll then check whether the left shift key is being pressed. If it is, we want the replay to play at half speed, so we'll multiply the change rate by 0.5.

        ...

        if (Input.GetKey(KeyCode.LeftArrow))
        {
            indexChangeRate = -1;
        }

        if (Input.GetKey(KeyCode.LeftShift))
        {
            indexChangeRate *= 0.5f;
        }
    }

    ...

Now in our FixedUpdate method, we'll use the change rate to calculate our next index position.

    ...

    private void FixedUpdate()
    {
        if (isInReplayMode == false)
        {
            actionReplayRecords.Add(new ActionReplayRecord { position = transform.position, rotation = transform.rotation });
        }
        else
        {
            int nextIndex = currentReplayIndex + indexChangeRate;

            if (nextIndex < actionReplayRecords.Count)
            {
                SetTransform(nextIndex);
            }
        }
    }

    ...

We need to change this variable to a float as we can now change the index by half a frame when replaying in slow motion. 

    ...

    private void FixedUpdate()
    {
        if (isInReplayMode == false)
        {
            actionReplayRecords.Add(new ActionReplayRecord { position = transform.position, rotation = transform.rotation });
        }
        else
        {
            float nextIndex = currentReplayIndex + indexChangeRate;

            if (nextIndex < actionReplayRecords.Count)
            {
                SetTransform(nextIndex);
            }
        }
    }

    ...

We'll also need to change the parameter in our method to accept a float.

    ...

    private void SetTransform(float index)
    {
        ...

And we'll change our current index field to match.

using System.Collections.Generic;
using UnityEngine;

public class ActionReplay : MonoBehaviour
{
    private bool isInReplayMode;
    private Rigidbody rigidbody;
    private List<ActionReplayRecord> actionReplayRecords = new List<ActionReplayRecord>();
    private float currentReplayIndex;
    private float indexChangeRate;

    ...

Then, to get the record from our list, we'll need to cast the index back to an integer. This will round the index down to a whole number.

    ...

    private void SetTransform(float index)
    {
        currentReplayIndex = index;

        ActionReplayRecord actionReplayRecord = actionReplayRecords[(int)index];

        transform.position = actionReplayRecord.position;
        transform.rotation = actionReplayRecord.rotation;
    }
}

The final thing we need to do is add another check in the FixedUpdate method. As we can now run in reverse, we need to check that the next index is greater or equal to zero.

    ...

    private void FixedUpdate()
    {
        if (isInReplayMode == false)
        {
            actionReplayRecords.Add(new ActionReplayRecord { position = transform.position, rotation = transform.rotation });
        }
        else
        {
            float nextIndex = currentReplayIndex + indexChangeRate;

            if (nextIndex < actionReplayRecords.Count && nextIndex >= 0)
            {
                SetTransform(nextIndex);
            }
        }
    }

    ...

This script should now look as follows.

using System.Collections.Generic;
using UnityEngine;

public class ActionReplay : MonoBehaviour
{
    private bool isInReplayMode;
    private Rigidbody rigidbody;
    private List<ActionReplayRecord> actionReplayRecords = new List<ActionReplayRecord>();
    private float currentReplayIndex;
    private float indexChangeRate;

    void Start()
    {
        rigidbody = GetComponent<Rigidbody>();
    }

    void Update()
    {
        if (Input.GetKeyDown(KeyCode.R))
        {
            isInReplayMode = !isInReplayMode;

            if (isInReplayMode)
            {
                SetTransform(0);
                rigidbody.isKinematic = true;
            }
            else
            {
                SetTransform(actionReplayRecords.Count - 1);
                rigidbody.isKinematic = false;
            }
        }

        indexChangeRate = 0;

        if (Input.GetKey(KeyCode.RightArrow))
        {
            indexChangeRate = 1;
        }

        if (Input.GetKey(KeyCode.LeftArrow))
        {
            indexChangeRate = -1;
        }

        if (Input.GetKey(KeyCode.LeftShift))
        {
            indexChangeRate *= 0.5f;
        }
    }

    private void FixedUpdate()
    {
        if (isInReplayMode == false)
        {
            actionReplayRecords.Add(new ActionReplayRecord { position = transform.position, rotation = transform.rotation });
        }
        else
        {
            float nextIndex = currentReplayIndex + indexChangeRate;

            if (nextIndex < actionReplayRecords.Count && nextIndex >= 0)
            {
                SetTransform(nextIndex);
            }
        }
    }

    private void SetTransform(float index)
    {
        currentReplayIndex = index;

        ActionReplayRecord actionReplayRecord = actionReplayRecords[(int)index];

        transform.position = actionReplayRecord.position;
        transform.rotation = actionReplayRecord.rotation;
    }
}

Let's save our script and switch back to Unity.

We'll press play to try it out and we'll fire the ball at the boxes again.

Then we'll press 'R' to enter replay mode. Now, when we hold down the right arrow it will play our replay as normal.

Playing the action replay in the forward direction

If we press the left arrow, our replay will play in reverse!

Playing the action replay in reverse

If we hold down the shift key while pressing the right arrow the replay will play at half speed.

Playing the action replay in slow motion

And if we let go of the right arrow, the replay will pause.

Pausing the action replay

We've now created a functioning replay system, but there's lots more we could do. For a start, we'd want to add some UI elements to show the player they are in replay mode, and allow them to control the replay by clicking buttons rather than using the keys.

We'll look into how to do this and other ways to improve the replay system in a future tutorial, so please subscribe so you don’t miss it.

That covers everything for this tutorial. We hope that you found it useful. Please leave any questions or feedback in the comments below, and don't forget to subscribe to get notified when we publish our next post.

Thanks.

Comments

Popular posts from this blog

Rotating a Character in the Direction of Movement - Unity Game Development Tutorial

Changing the Colour of a Material - Unity Game Development Tutorial

Creating Terrain from Heightmaps - Unity Game Development Tutorial