Instant Replay

This project was a specialization I did during my time at The Game Assembly over the course of 5 weeks at half-time, using C++. The result is a library that allows for an easy implementation of a replay feature. Source code can be found here.

Goals

My goal was to create a tool to make it possible to pause a live game simulation and then be able to play back a part of that simulation. The purpose was to facilitate high-level debugging for games.

I didn't want to make the replay functionality specifically for one application - modularity was important for this project. I decided to make it into a library because I wanted to make it as pain free as possible to add the replay functionality into a project. Once the library is linked to a project, it should be easy to select the data that should be saved/loaded. Ideally, there wouldn't be much code needed to get it working. I developed this tool with portability and usability in mind.

How It Works

There are two main functions in the library, one that saves all the tracked data, and one that loads them. A special variable class is responsible for handling what data is saved/loaded. These variables would copy their own data into a buffer when the user saved. Upon loading, the opposite would happen, reverting the variables to their previously saved state.

Adding replay functionality to variables

This variable class works by inheriting from a template argument and extending its functionality with replay code. Doing it this way makes it very easy to add replay functionality to existing code. All you have to do is replace the type of a variable with its replay counterpart. Through the magic of polymorphism, there is usually no further need to edit anything else in the code. One case where this is not true is when the memory layout of a variable is relevant, such as when sending data to the graphics card.

Note that it is not possible to inherit from fundamental types in C++, which would make this approach not work with e.g. ints or floats. For those types, it was enough to simply define conversion operators, since fundamental types have no member functions. Using template specializations and SFINAE, the correct implementation is selected, depending on whether the type is a fundamental type or not.

Using SFINAE to select the right implementation

Memory management

The dictionary is where all the variables are saved to. The variables are of different types and sizes, and must be stored in one big contiguous buffer to make it cache-friendly. To accomplish this, each variable requests a unique ID from the dictionary when created. The dictionary then allocates enough space in the buffer and saves the memory position together with the variable's size and ID. This information will allow the dictionary to find the right data chunk from the buffer, given an ID.

An important consideration is how much memory is available. If we have a game with a big world and a lot of objects, saving the 3D world positions of every object, every frame, consumes a large amount of memory very quickly. In order to optimize the memory usage, the tool only stores variables that have changed. This simple optimization is very effective, as there are usually many objects that don't change very often. I was considering adding compression, to further reduce the memory usage, but there wasn't enough time for it.

The use of a circular array allows the user to save only the last X frames, which sets a limit for how much memory the tool can use.