Tuesday, August 9, 2016

How to Build a Prince-Of-Persia-Style Time-Rewind System, Part 2 (end)

How to Build It

In order to test this system, we need a simple game where we can test it. Let's create one!

The Player
Create a cube in your scene, this will be our player-character. Then create a new C#-script calls Player.cs and adapt the Update()-function to look like this:
  1. void Update()
  2. {
  3.     transform.Translate (Vector3.forward * 3.0f * Time.deltaTime * Input.GetAxis ("Vertical"));
  4.     transform.Rotate (Vector3.up * 200.0f * Time.deltaTime * Input.GetAxis ("Horizontal"));
  5. }
This will handle simple movement via the arrow keys. Attach this script to the player cube. When you now hit play you should already be able to move around.

Then angle the camera so that it views the cube from above, with room on its side where we can move it. Lastly, create a plane to act as floor and assign some different materials to each object, so that we're not moving it inside of a void. It should look like this:


Try it out, and you should be able to move your cube using the WSAD and arrow-keys.

The TimeController
Now create a new C#-script called TimeController.cs and add it to a new empty GameObject. This will handle the actual recording and subsequent rewinding of the game.

In order to make this work, we will record the movement of the player character. When we then press the rewind button we will adapt the player coordinates. To do so start by creating a variable to hold the player, like this:
  1. public GameObject player;
And assign the player-object to the resulting slot on the TimeController, so that it can access the player and its data.

Then we need to create an array to hold the player data:
  1. public ArrayList playerPositions;
  2. void Start()
  3. {
  4.     playerPositions = new ArrayList();
  5. }
What we will do next is continuously record the position of the player. We will have the position stored of where the player was in the last frame, the position where the player was 6 frames ago, and the position where the player was 8 seconds ago (or however long you will set it to record). When we later hit a button we'll go backward through our array of positions and assign it frame by frame, resulting in a time-rewinding feature.

First, let's save the data:
  1. void FixedUpdate()
  2. {
  3.     playerPositions.Add (player.transform.position);
  4. }
In the FixedUpdate()-function we record the data. FixedUpdate() is used as it runs at a constant 50 cycles per second (or whatever you set it to), which allows for a fixed interval to record and set the data. The Update()-function meanwhile runs depending on how many frames the CPU manages, which would make things more difficult.

This code will store the player-position of each frame in the array. Now we need to apply it!

We'll add a check to see if the rewind button was pressed. For this, we need a boolean variable:
  1. public bool isReversing = false;
And a check in the Update()-function to set it according to whether we want to rewind the gameplay:
  1. void Update()
  2. {
  3.     if(Input.GetKey(KeyCode.Space))
  4.     {
  5.         isReversing = true;
  6.     }
  7.     else
  8.     {
  9.         isReversing = false;
  10.     }
  11. }
To make the game run backward, we will apply the data instead of recording. The new code for recording and applying of the player position should look like this:
  1. void FixedUpdate()
  2. {
  3.     if(!isReversing)
  4.     {
  5.         playerPositions.Add (player.transform.position);
  6.     }
  7.     else
  8.     {
  9.         player.transform.position = (Vector3) playerPositions[playerPositions.Count - 1];
  10.         playerPositions.RemoveAt(playerPositions.Count - 1);
  11.     }
  12. }
And the entire TimeController-script like this:
  1. using UnityEngine;
  2. using System.Collections; 
  3. public class TimeController: MonoBehaviour
  4. {
  5.     public GameObject player;
  6.     public ArrayList playerPositions;
  7.     public bool isReversing = false; 
  8.     void Start()
  9.     {
  10.         playerPositions = new ArrayList();
  11.     } 
  12.     void Update()
  13.     {
  14.         if(Input.GetKey(KeyCode.Space))
  15.         {
  16.             isReversing = true;
  17.         }
  18.         else
  19.         {
  20.             isReversing = false;
  21.         }
  22.     }   
  23.     void FixedUpdate()
  24.     {
  25.         if(!isReversing)
  26.         {
  27.             playerPositions.Add (player.transform.position);
  28.         }
  29.         else
  30.         {
  31.             player.transform.position = (Vector3) playerPositions[playerPositions.Count - 1];
  32.             playerPositions.RemoveAt(playerPositions.Count - 1);
  33.         }
  34.     }
  35. }
Also, don't forget to add a check to the player-class to see if the TimeController is currently rewinding or not, and only move when it is not reversing. Otherwise, it might create buggy behavior:
  1. using UnityEngine;
  2. using System.Collections; 
  3. public class Player: MonoBehaviour
  4. {
  5.     private TimeController timeController; 
  6.     void Start()
  7.     {
  8.         timeController = FindObjectOfType(typeof(TimeController)) as TimeController;
  9.     }     
  10.     void Update()
  11.     {
  12.         if(!timeController.isReversing)
  13.         {
  14.             transform.Translate (Vector3.forward * 3.0f * Time.deltaTime * Input.GetAxis ("Vertical"));
  15.             transform.Rotate (Vector3.up * 200.0f * Time.deltaTime * Input.GetAxis ("Horizontal"));
  16.         }
  17.     }
  18. }
These new lines will automatically find the TimeController-object in the scene on startup and check it during runtime to see if we are currently playing the game or rewinding it. We can only control the character when we are currently not reversing time.

Now you should be able to move around the world, and rewind your movement by pressing space. If you download the build package attached to this article and open TimeRewindingFunctionality01 you can try it out!

But wait, why does our simple player-cube keep looking in the last direction we left them in? Because we didn't get around to also record its rotation!

For that you need another array to keep its rotation-values, to instantiate it at the beginning, and to save and apply the data the same way we handled position-data
  1. using UnityEngine;
  2. using System.Collections; 
  3. public class TimeController: MonoBehaviour
  4. {
  5.     public GameObject player;
  6.     public ArrayList playerPositions;
  7.     public ArrayList playerRotations;
  8.     public bool isReversing = false;    
  9.     void Start()
  10.     {
  11.         playerPositions = new ArrayList();
  12.         playerRotations = new ArrayList();
  13.     }     
  14.     void Update()
  15.     {
  16.         if(Input.GetKey(KeyCode.Space))
  17.         {
  18.             isReversing = true;
  19.         }
  20.         else
  21.         {
  22.             isReversing = false;
  23.         }
  24.     }     
  25.     void FixedUpdate()
  26.     {
  27.         if(!isReversing)
  28.         {
  29.             playerPositions.Add (player.transform.position);
  30.             playerRotations.Add (player.transform.localEulerAngles);
  31.         }
  32.         else
  33.         {
  34.             player.transform.position = (Vector3) playerPositions[playerPositions.Count - 1];
  35.             playerPositions.RemoveAt(playerPositions.Count - 1);   
  36.             player.transform.localEulerAngles = (Vector3) playerRotations[playerRotations.Count - 1];
  37.             playerRotations.RemoveAt(playerRotations.Count - 1);
  38.         }
  39.     }
  40. }
Try it out! TimeRewindingFunctionality02 is the improved version. Now our player-cube can move backward in time, and will look the same way it did when it was at that moment.

Conclusion

We have built a simple prototype game with an already usable time-rewinding system, but it is far from done yet. In the next part of this series we'll make it much more stable and versatile, and add some neat effects.

Here is what we still need to do:

Only record every ~12th frame and interpolate between the recorded ones to save on the huge data load
Only record the last ~75 player positions and rotations to make sure the array doesn't become too unwieldy and the game doesn't crash
We'll also take a look at how to extend this system past just the player-character:

Record more than just the player
Add an effect to signify rewinding is happening (like VHS-blurring)
Use a custom class to hold player position and rotation instead of arrays
Written by Matthias Zarzecki

If you found this post interesting, follow and support us.
Suggest for you:

Unity 5 Professional Guide - Develop a 2D Arkanoid Game!

Unity 5 Professional Guide - Mastering C# Programming!

Make VR Games in Unity with C# - Cardboard, Gear VR, Oculus

Learn to Code by Making Games - The Complete Unity Developer

Start Learning Unity3d by Making 5 Games from Scratch

No comments:

Post a Comment