How I implemented Replay system

Also posted in Udemy Q&A.
This took me some thinking.
Would love to hear ideas on how to simplify code or implementation.

GameReplay.cs

using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityStandardAssets.CrossPlatformInput;

public class GameReplay : MonoBehaviour
{
    private const int bufferFrames = 1000;
    private GameKeyFrame[] keyFrames = new GameKeyFrame [bufferFrames];

    private GameManager gameManager;
    private Rigidbody rigidBody;

    private bool recordToggle;  // This tracks when user switches between Record and Playback
    private int startKeyFrame;  // Frame recording begin at
    private int totalKeyFrame;  // How many frames were recorded
    private int counterKeyFrame; // Current position in Playback


    // Use this for initialization
    void Start ()
    {
        rigidBody = GetComponent<Rigidbody>();
        gameManager = GameObject.FindObjectOfType<GameManager>();
        recordToggle = !gameManager.recording;
    }

	
	// Update is called once per frame
	void Update ()
    {    
        if (gameManager.recording)  
        {
            Record();
        }
        else
        {   
            Playback();
        }              
    }

    private void Playback()
    {
        if (recordToggle != gameManager.recording)  // Started playback
        {
            recordToggle = gameManager.recording;
            rigidBody.isKinematic = true;

            int endKeyFrame = Time.frameCount;
            if ( (endKeyFrame - startKeyFrame) >= bufferFrames) // Recorded past buffer limit.  Playback entire buffer
            {
                startKeyFrame = endKeyFrame + 1;
                totalKeyFrame = bufferFrames;
            }
            else  // Only playback recorded portion
            {               
                totalKeyFrame = endKeyFrame - startKeyFrame;
            }
            
            counterKeyFrame = 0;
        }

        int keyFrame = (startKeyFrame + counterKeyFrame) % bufferFrames;
        Debug.Log("keyFrames[" + keyFrame + "].keyTime = " + keyFrames[keyFrame].keyTime) ;

        transform.position = keyFrames[keyFrame].keyPosition;
        transform.rotation = keyFrames[keyFrame].keyRotation;

        counterKeyFrame++;
        if (counterKeyFrame >= totalKeyFrame) // Replay from beginning if exceeded recorded portion
        {
            counterKeyFrame = 0;
        }
    }


    private void Record()
    {        
        if (recordToggle != gameManager.recording)  // Started recording
        {
            recordToggle = gameManager.recording;
            rigidBody.isKinematic = false;
            startKeyFrame = Time.frameCount;
        }

        int keyFrame = Time.frameCount % bufferFrames;
        keyFrames[keyFrame] = new GameKeyFrame(Time.time, transform.position, transform.rotation);
    }

}


/// <summary>
/// Store time, position, rotation of object
/// </summary>
public struct GameKeyFrame
{
    public float keyTime;
    public Vector3 keyPosition;
    public Quaternion keyRotation;

    public GameKeyFrame(float time, Vector3 pos, Quaternion rot)
    {
        keyTime = time;
        keyPosition = pos;
        keyRotation = rot;
    }
}
1 Like

I also made my own Replay system, mine is a little shorter. Here is the code:

using System.Collections;
using System.Collections.Generic;
using UnityEngine;

public class ReplaySystem : MonoBehaviour {

	private const int bufferFrames = 1000;
	private MyKeyFrame[] keyFrames = new MyKeyFrame [bufferFrames];
	private Rigidbody rigidBody;
	private GameManager gameManager;
	private int endFrame = 0;
	private bool endFrameCaptured = false;
	private int multiplier = 0;

	void Start () {
		rigidBody = GetComponent<Rigidbody>();
		gameManager = GameObject.FindObjectOfType<GameManager>();
	}
	
	void Update () {
		if (gameManager.recording){
			Record();
		}else{
			PlayBack();
		}
	}
	
	void PlayBack (){
		rigidBody.isKinematic = true;
		
		if (!endFrameCaptured){
			endFrame = Time.frameCount;
			endFrameCaptured=true;
		}
		
		
		int frame = Time.frameCount % bufferFrames;
		multiplier = frame/endFrame;
		int playBackFrame = (endFrame < bufferFrames ? frame - (endFrame*multiplier) : frame);
		
		Debug.Log (playBackFrame);
		
		transform.position = keyFrames[playBackFrame].position;
		transform.rotation = keyFrames[playBackFrame].rotation;
		
	}

	void Record (){
		rigidBody.isKinematic = false;
		endFrameCaptured = false;
		int frame = Time.frameCount % bufferFrames;
		keyFrames [frame] = new MyKeyFrame (Time.time, transform.position, transform.rotation);
	}
}

public struct MyKeyFrame {
	
	public float frameTime;
	public Vector3 position;
	public Quaternion rotation;
	
	public MyKeyFrame (float time, Vector3 pos, Quaternion rot){
		frameTime = time;
		position = pos;
		rotation = rot;
	}
	
}

Thanks for sharing!

My added complexity is to playback only the frames that are actually recorded.
Example: Record Buffer set to 5 minutes, and users only saves 10 seconds.
Without the extra code, the user would view the entire 5 minute buffer.

The anal part of me also did not like updating rigidBody unnecessarily each frame :slight_smile:

Regarding the replay of the recorded data, my code works the same.
If you only recorded for example 100 frames and the buffer size is 10000 frames,
then only the recorded 100 frames are played again and again :slight_smile:

I tested your code, and I did not get that behavior.
The easiest way to see this is to start/stop recording a few times in the same play session. You will notice that it when you switch to playback mode. It does not reset to the position for when you started recording.

For more in-depth, you have to put logging messages for both Play and Record modes. If you switch to Recording at frame 220, then it should return to 220 during playback.

Our scripts have different names, so it is easy enough to put both on Player/Cube and disable one, enable the other to see difference in behavior.

Oh, now I see what you meant. You were right, the codes behave differently. With a little tweak my code would be suitable in a Prince of Persia like game, where you can rewind time.
Yours is maybe for watching a replay of some action (which was the original goal :slight_smile: ).

HI guys,

If you could please explain to me the logic in the coding above from start to finish. I clearly don’t understand how modulus works. Also I copied your code verbatim and I am having trouble with it collecting the entire sample of my replay and starting from the very first keyFrame and watching my play. I jumped over the cube eight times but the best I could manage to capture was 3 jumps before a reset. Maybe I am doing something wrong? I am specifically referring to TorrqTb’s code.

Tim

I just went back into the code and played around with the bufferFrames that we set in the beginning of the code. Pretty obvious to understand that if we increase this number the amount of footage we can actually replay increases. I am still very interested in figuring out how the rest of the code works however. I have gone through it mathematically with a pen and a piece of paper but i still dont understand certain concepts such as totalKeyFrame = endKeyFrame - startKeyFrame; Why do we need to subtract endKeyFrame by 1? What does int keyFrame = (startKeyFrame + counterKeyFrame) % bufferFrames; mean?

Been awhile, first concept is envision you have an animation flipbook with 1000 pages.
And they are numbered 0 - 999.
When we are in record mode, each page is being written. When playback we have to track at what page the started playback, and what last page recorded was. So we can loop between those points.
Things get messy at edge cases, like recording started at page 990 and ended at 05.

totalKeyFrame = endKeyFrame - startKeyFrame; Is how we know how many frames were actually recorded. I am guessing I need an Absolute Value here, to handle the edge case I mentioned.

Here is Modulus explanation: https://stackoverflow.com/questions/2664301/how-does-modulus-divison-work

Explanation for: int keyFrame = (startKeyFrame + counterKeyFrame) % bufferFrames;
This is another edge-case. If you start recording at frame 990 for 20 frames. We need to playback 990 - 10.
startKeyFrame + counterKeyFrame = 1010
% 1000 = 10

Hi TorrqTB ,

I did not expect a reply from you so quickly I really do appreciate the help it means a lot. Anyways back to the explanation above. The only question I have now is that startKeyFrame + counterKeyFrame = 1010 that makes sense to me because we recorded for 20 frames. However we we modulus this result 1010 % 1000 we are left with the number 10 (the remainder). This is where I get confused because I thought we were trying to record for 20 frames and not for 10 frames. Sorry for being a drag and not getting the entire picture.

Tim

keyFrame represents the current frame we are displaying to the player during playback.
Therefore we need keyFrame to go 998, 999, 0, 1, … 10. As the bufferFrames has 0 - 999 frames.

Respectively the counterKeyFrame will loop from 0 - 19, during the same example. As this represents the actual number of frames we need to playback. Perhaps that is the 20 frames you are looking for?

Privacy & Terms