Time Game 1 - Implementing Time Travel (Replay system)

This is my current progress of a time travel puzzle game created with Unity3D



I will briefly outline how I implemented time travel in Unity3d.
As the title suggests, this method will be the same if you are trying to create some sort of replay system.

Record

The first thing we need to do is to record our players movement.
The easiest thing to do seems to be to record the players position and rotation (Transform).

For this we create an object which will be used to hold the recorded information.

RecordingData.cs
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
public class RecordingData
{
public float RecordedRate { get; set; }
private List<RecordData> recordedData = new List<RecordData>();

public struct RecordData
{
public Vector3 Position { get; set; }
public Quaternion Rotation { get; set; }
}

public void AddDataLine(RecordData data)
{

recordedData.Add(data);
}

public int pointer = 0;
public void MoveToStartOfData()
{

pointer = 0;
}

public RecordData? GetNextDataLine()
{
var count = recordedData.Count;
RecordData? data = null;
if (count > 0 && pointer < count && pointer >= 0) // not empty and pointer is within range and pointer not negative
{
data = recordedData[pointer];
}
pointer++;
return data;
}
}

Note

This can be extended to record more information in the future (maybe Scale?)

We then need to call a method which records the data.

TakeSnapShot()
1
2
3
4
5
6
7
8
9
10
11
private RecordingData recordingData = new RecordingData();
public void TakeSnapshot()
{

var t = Transform;
var data = new RecordData()
{
Position = t.position,
Rotation = t.rotation
};
recordingData.AddDataLine(data);
}

We need to call the TakeSnapshot method at a known interval.
I have used coroutines to ensure a consistent sample of data.
In the below example we are using 0.05 seconds which is a rate of 20fps.
This seems to offer a smooth enough playback rate (discussed later in this article).

Record Coroutine
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
public bool DoRecording = true;
void StartRecording()
{

DoRecording = true;
StartCoroutine(RecordingTimer(0.05f, 0f));
}

//RECORDING LOOP
IEnumerator RecordingTimer(float sampleRate)
{

recordingData.RecordedRate = sampleRate;
while (DoRecording) // Repeat until DoRecording is false
{
TakeSnapshot();
yield return new WaitForSeconds(recordingData.RecordedRate / Time.timeScale);
}
}

Note

DoRecording can be set to false to stop recording.

Replay

Then we just need to play this data back. Again this is done with a coroutine.

Playback Coroutine
1
2
3
4
5
6
7
public bool DoPlayback = true;
public void StartPlayback()
{

DoPlayback = true;
recordingData.MoveToStartOfData(); //Move to start of playback
StartCoroutine(PlaybackTimer());
}

This is the same as the recording loop except it calls Next SnapShot().

Playback Loop
1
2
3
4
5
6
7
8
9
//PLAYBACK LOOP
IEnumerator PlaybackTimer()
{

while (DoPlayback) // Repeat Until DoPlayback is false
{
NextSnapshot();
yield return new WaitForSeconds(recordingData.RecordedRate / Time.timeScale);
}
}

The NextSnapshot method simply retrieves the desired transform of the object.

NextSnapshot()
1
2
3
4
5
6
7
8
9
10
11
12
13
private RecordingData.RecordData? _desiredMoveLocation = null;

//MOVE DESIRED LOCATION TO NEXT LOCATION
private void NextSnapshot()
{

var data = recordingData.GetNextDataLine();

if (_desiredMoveLocation.HasValue && !data.HasValue) //If we previously had move location and now we do not
{
//END OF REPLAY - YOU MAY WANT TO REMOVE THE REPLAY OBJECT FROM SCENE NOW
}
_desiredMoveLocation = data;
}

We then move the object each update frame using the Lerp function (this helps keep the movement smooth).

Move Object
1
2
3
4
5
6
7
8
9
10
11
12
13
public void Update()
{

if (_desiredMoveLocation.HasValue)
{
var playbackRate = recordingData.RecordedRate;
var amount = Math.Abs(Time.deltaTime) / playbackRate;
var p = Vector3.Lerp(Transform.position, _desiredMoveLocation.Value.Position, amount);
var r = Quaternion.Lerp(Transform.rotation, _desiredMoveLocation.Value.Rotation, amount);

Transform.position = p;
Transform.rotation = r;
}
}

This is not the only way

There are many many ways of doing this. I am outlining how I did it.
I have simplified my implementation for the purposes of this post (to outline roughly how it was done).
Any feedback or improvements are welcome :)

Have a question?

Feel free to ask me anything (I have overlooked some internal workings in this post).
I also have a Unity3d Forum WIP post for this game prototype (in progress):
https://forum.unity3d.com/threads/wip-time-game-time-travel.435684/

More Info?

I have not gone into any detail on how the replay data can be saved and loaded between games (I have implemented this in my prototype).
I can try and write a post outlining this if this is of interest?


Comments: