informa
/
Programming
Featured Blog

Implementing a replay system in Unity and how I'd do it differently next time

Due to the fact that certain aspects of Unity are non deterministic, building a replay system that only saves the game's input took a little bit of effort. Find out how to get around Unity's non deterministic systems for efficient replays.

While working on Sportsball I decided it would be a great feature to be able to show replays of matches. The instant replay feature is pretty popular not only in sports, but also competitive video games like Towerfall. Even 0Space has it, although by playing that game I realized that it’s very easy to make a replay system that doesn’t actually work. Although hilarious on occasion, as a player, I want to make sure you can show the world that amazing box pivot score I did to take the game at the last moment. Here’s how I went about implementing a deterministic replay solution in Unity version 4.2 and 4.3, requiring only recording and replaying the player’s inputs.
 

STEP 1: RESEARCH


Since I’ve never implemented a replay system before, and it’s something that many games have done, I figured I’d learn from past examples. My main source of information came from this Gamasutra article. It gave me a good overview of the two main approaches to building a replay system: saved state and deterministic.
 

Saved state - Save the (important) data of all objects each frame. Replays are played back by restoring the state (of variables) of recorded objects.


Pros:

  • Easily jump to any point of a replay

Cons:

  • Large file size
     

Deterministic - You save only the initial state of all objects, then player inputs each frame. Replays are played back by restoring the state of the game’s input each frame.


Pros:

  • Incredibly small amount of memory usage.

  • Required for certain types of online implementation.


Cons:

  • Requires the simulation to occur perfectly every time.

  • Changes in your game will break previous replays.
     

I decided to go with a deterministic solution as I was planning on targeting consoles with Sportsball (with us eventually settling on the Wii U, which has 1GB of memory available). Later on I watched a talk by Jonathan Blow, discussing the time rewinding mechanic in Braid. To my surprise, he implemented a saved state approach. I figured that would be far more challenging due to the fact that the game could record hours of gameplay on an XBox 360. He decided to embrace the challenge of optimizing the data saved, instead of fighting with maintaining a deterministic game. This was also the approach of The Bridge as I learned after talking with its developer Ty Taylor. The systems they implemented were great, but not ideal for Sportsball. With up to 80 objects being recorded each frame, it may have been a larger challenge optimizing that aspect of the system. You’ll have to make a call for your own game.
 

I also want to note that the Unity asset store has EZ Replay Manager for saving replays. After using their demo, I didn’t like that the game appeared to play back in a lower framerate. Maybe I could have gotten the replays running at 60FPS with enough effort. But, I decided to create my own because I was interested in learning the system inside and out.

 

STEP 2: NON DETERMINISM IN UNITY
 

For the most part, making a deterministic game shouldn’t be too difficult these days. With the IEEE standard for floating point numbers, you can (mostly) ensure that even with floats your simulation will run the same on a single machine. But, in short, you need to make sure that with a given set of inputs, your simulation will do the exact same thing every time you run it. Unfortunately, Unity has some issues doing this out of the box.
 

Random.Range(): This function is great in that every time you run your game, it will give you a different random number. But, that’s not what we want. I need to ensure that a random number I pick will be the same random number each time. Unity has a Random.seed variable, which will ensure it uses the same seed for calculating a random number. But, even this wasn’t enough for me. I discovered that if multiple objects accessed Random.Range() with a seed set at the beginning, those future random numbers were not guaranteed to be deterministic. So I made a more brute force approach. I created a public static function that allowed me to get a deterministic random number:
 

public static int GetDeterminedRandomNumber(int min, int max){

       UnityEngine.Random.seed = staticSeedNumber;

       int num =  UnityEngine.Random.Range(min, max);

       UnityEngine.Random.seed = staticSeedNumber;

       return num;

   }

 

This gets me a random value, but then resets the seed for this frame. I can’t be positive on this (so please correct me if I’m wrong) but I think Unity randomizes the seed after each use of Random.Range(), resulting in non deterministic results, even if you set the seed. So before each Update frame in my game, I set the staticSeedNumber and, through the use of GetDeterminedRandomNumber() I make sure to use that same seed for the whole frame. For Sportsball, the staticSeedNumber is the time in milliseconds that the match was started subtracted by how long in milliseconds the match has been running. This means that each frame of the replay has a unique seed. This was also an easy way for me to get a unique seed for each replay, as I was already storing when the matches start.

 

EDIT: As noted to me by Reddit user Dest123, this is not a very helpful function! It will always returns the same number given the same range. Instead, this function is not necessary. The seed should be set at the beginning of the replay, and every call to Random.Range() MUST happen in the same order. More details on the reddit post here.

 

OnCollision(): Through my experiments I discovered that Unity’s built in collision system is not deterministic. I would have collisions happen on one frame or another. Normally they are very close (within a frame) but not exact. Because Unity’s raycasts also use this collision system, I was out of luck.
 

This is probably the largest challenge of implementing a deterministic solution in Unity: you have to program your own collision from scratch. Fortunately, there is tons of documentation on programming collision. It was definitely a struggle for me, as math isn’t my strong suit. But, I was able to build a solid collision system with Miguel Gomez's article on Simple Intersection Tests for Games. And if you’re like me and not super familiar with linear algebra, I suggest checking Wildbunny’s Vector Maths primer. There are many articles on collision, but since Sportsball is a precise 2D competitive game, I liked Miguel’s approach the most. His collision implementation, by using sweep tests, has the ability to predict collisions before they occur. By detecting collisions from sweeping the potential area of contact, we can prevent high speed objects from skipping over collisions when moving too fast.
 

Once you’ve converted the math over, you need to actually decide when to perform your collisions. I used the following loop:
 

void Update(){

       GetPlayerInput(); // this takes player actions from wherever it arrives from (controllers, saved replay data, or network packets). This will call functions on the objects in the scene that should move. For example, if the player presses jump, this is where the player object will be told to set its velocity to what it should be if jumping.

       CheckCollisions(); // based off the input from the player, we can determine now what we want to allow. For example, if the player presses the jump button, which sets the player’s velocity to a very high number in y, we will perform a collision check based off of the desired velocity. In this way, we can check if the player will collide against anything and set the player’s velocity so that it will move flush to the colliding object, as opposed to going through it and having to correct next frame.

       object.UpdateAt(); // this is where we actually apply the object’s new velocity calculated after limiting it due to any collisions.

   }
 

To reiterate, each update we take the player’s intention, we limit the ability for the player to move based on any possible collisions, and then we perform the results of the interactions. By using this loop, we can ensure that objects don’t pass through each other, even at high velocities (if you implemented a sweeping collision check).
 

Time.deltaTime: Many games prefer to be run framerate independent. This means that no matter how fast or slow your computer is running, the simulation always updates at the same pace. (If you’re unfamiliar with the concept, there is a pretty good discussion on Stack Exchange.) Since we need each update loop to perform the same way every time, we can’t rely on Time.deltaTime, because we don’t know that a frame will take the same amount of time to process the next time we run the game (in fact it almost certainly won’t be the same time). My solution was to lock the game simulation to the frame rate. So, for my game, every object that needs to be recorded implements an UpdateAt(int deltaTime) function. I pass through an amount of milliseconds I want to pass in that function when I call update, which allows me to guarantee that the simulation updates consistently each time. You’ll want to set this number based on how long your frames are, and vary it to adjust the speed at which the objects in your game world move.
 

As a note here, you could go ahead and pass Time.deltaTime through the UpdateAt() function if you wanted to. You’d have to record what the deltaTime for that update loop was each frame, so you could use the exact same time to update your objects during the replay (which you should do anyway to record time stretching effects). I didn’t like this implementation because when replaying the game, it would feel like the game was sometimes playing faster or slower, and wasn’t a smooth playback. This is due to the fact that my computer, at that instant, might be running slightly slower or faster than what it was running at when recording. Combine this with issues such as garbage collection stutters and you’ll have a messy replay.
 

Transform: Although stated above that you should be able to use floats on the same system, I wasn’t finding this to be successful. Unity’s transforms store position, rotation, and scale as floats. Because of the articles I read, and the errors I was getting, I eventually decided to implement my own transform component that only used integers. During LateUpdate(), the component would set the game object’s transform to the value of the integer transform.
 

After implementing this I found that I still had deterministic issues unrelated to transforms. So, it’s my theory that you could actually have a deterministic system even using floats (if you’ve done this, please let me know!). But, I haven’t tested that. Additionally, by ensuring that all values I record are not floats, I avoid any possible cross platform issues with any hardware that doesn’t implement the same floating point standard. Something to consider if you want to do cross platform online play.
 

STEP 3: IMPLEMENTATION

So now that we’ve avoided the non deterministic issues of Unity, how do we actually go about recording and playing back all this data?

First, we are going to implement a save state replay system as a debug feature. Although I’ve already decided I want the game to be deterministic, it’s very hard to test such a thing just by watching the game play through. I certainly don’t have the ability to determine if a ball bounces off of a platform on one frame or the next with my own eyes; and such an issue would cause a butterfly effect that I might not notice till minutes later in the replay.

By implementing a save state system, we can compare the simulation with our saved data and see if anything is off on the exact frame that we get out of sync. Not only will this prove our game is deterministic, but it’s also a great way to debug your game. Have an issue that’s hard to reproduce? No worries, you have the data saved to reproduce it exactly the same way every time.
 

Latest Jobs

Sucker Punch Productions

Bellevue, Washington
08.27.21
Combat Designer

Xbox Graphics

Redmond, Washington
08.27.21
Senior Software Engineer: GPU Compilers

Insomniac Games

Burbank, California
08.27.21
Systems Designer

Deep Silver Volition

Champaign, Illinois
08.27.21
Senior Environment Artist
More Jobs   

CONNECT WITH US

Register for a
Subscribe to
Follow us

Game Developer Account

Game Developer Newsletter

@gamedevdotcom

Register for a

Game Developer Account

Gain full access to resources (events, white paper, webinars, reports, etc)
Single sign-on to all Informa products

Register
Subscribe to

Game Developer Newsletter

Get daily Game Developer top stories every morning straight into your inbox

Subscribe
Follow us

@gamedevdotcom

Follow us @gamedevdotcom to stay up-to-date with the latest news & insider information about events & more