Time rewinding mechanic tutorial


Do you find time rewinding mechanics in games interesting? Do you want to make something similiar for your project but don't know how? Don't worry, it's not as hard as one might think. Let's review the process I used in Rewound, a short jam game. You might want to try it out to get a grasp what we're going after. Example code is in Godot's Gdscript, but principles apply to other engines and languages like Unity easily and example code is easy to read even for a novice programmer.

Principle

The working principle behind this mechanism is simple: for every x amount of time save objects' current state into an array. When rewinding time, traverse the array backwards setting object state.

Minimal Example

The Framework

First of all we need a way to track time. For this we can use a global variable or singleton (in Godot it's the same thing). In code, I'll make singleton called TimeControl that has a variable called time (to make a singleton go to Project Settings -> AutoLoad). Increase this variable every frame by delta (Time.deltaTime in Unity). This results the variable holding time since beginning in seconds.

Since we want to be able rewind time, we can multiply delta by variable. Let's call it time_speed. If time_speed is 1, time flows normally. If it's negative, time rewinds. For testing purposes,  we'll add logic to reverse time with pressing space key.

Last, but not least, we'll add a variable to control how often objects save states. This is explained more throroughly further down.

# time_control.gd
extends Node

# Tracks the flow of time in seconds 
var time = 0.0 
# Current direction of time (1 = forward, negative = backward) 
var time_speed = 1.0
# How many times a second object states get saved
var history_rate = 30

func _physics_process(delta: float) -> void:
    # TODO: make use actual game logic to affect time_speed
    if Input.is_key_pressed(KEY_SPACE):
        time_speed = -1.0
    else:
        time_speed = 1.0
    # Max 0 makes sure time won't go negative - we don't want objects to try get states from before they were created
    time = max(0, time + delta * time_speed)

Now we can add logic to track objects. We are going to make it an component we can add to any object. This is something I didn't do for jam game, and in the end it cost me a lot of time since I just copy-pasted the logic to every object I needed to track. Keeping code DRY (Don't Repeat Yourself) is a key to good maintability (something short jam games don't really strive for).

The process is simple: in each frame we check if time has elapsed a certain amount (for example 1/30th of second). If yes, then save the current state of object into an array. We are using stack type of array here, so whatever gets saved last is read first (in Godot stacks are same thing as arrays, but in C# you'd use Stack<object>). If time is less than it was last time, we'll retrieve last saved state instead. If time hasn't changed, we'll leave both history and current state untouched.

But what exactly is this current_state variable? Every object has different needs, so we won't enforce any data type. Saving and interpreting the state is left for the actual object using Trackable component, so let's return this question a bit later.

So how we determine the flow of time? The concept here is to take snapshots of state at regular intervals. For that, we can cast time (floating point number in seconds) into integer number. This way we get discrete number which can be easily compared. After all, we cannot save continuous representation of object states, we need to draw line somewhere. This is where history_rate comes in. If we simply cast time into int, we'll get one snapshot per second. For most use cases, this results too jaggy movement. But if we multiply this number before casting, we'll get more concrete steps per second. We'll call this current_frame (not to be confused with actual frames that get drawn). In the example we have history_rate = 30, so we take 30 snapshots a second. For example frame number 90 would be 3 seconds into game.

# trackable.gd
class_name Trackable
extends Node

# Save or retrieve current state from here
var current_state
# A LIFO (Last In First Out) stack of states
var history = []
# Used to track changes in time
var last_frame = 0

func _physics_process(delta: float) -> void:
    # Calculate how many steps from beginning has elapsed
    var current_frame = int(TimeControl.time * TimeControl.history_rate)
    # Save or retrieve object state
    while current_frame > last_frame:
        history.push_front(current_state)
        last_frame += 1
    while last_frame > current_frame and history.size() > 0:
        current_state = history.pop_front()
        last_frame -= 1

Now our framework is ready! Let's put it into use.

Test Object

Create an object you can interact with, for example a sprite or kinematic body. I'll add some simple movement code for testing purposes.

First we add Trackable as a child to the object (you can do this via editor but I'll add it in code).

Every time object is processed, we'll check the flow of time. Only if time is forwarding, we'll handle input and update our state. If the flow is reversed, we retrieve the state from Trackable component instead. In this example, state is simply object's position.

# test_object.gd
extends Node2D

var trackable = Trackable.new()

func _ready() -> void:
    add_child(trackable)

func _physics_process(delta: float) -> void:
    if TimeControl.time_speed > 0:
        # TODO: Implement actual game logic
        update_movement(delta)
        # Save current state
        trackable.current_state = global_position
    else:
        # Retrieve and apply state
        var state = trackable.current_state
        if state != null:
            global_position = state

func update_movement(delta: float) -> void:
        var movement = Vector2(
            Input.get_action_strength("ui_right") - Input.get_action_strength("ui_left"),
            Input.get_action_strength("ui_down") - Input.get_action_strength("ui_up")
        ) * 100
        global_position += movement * delta

Try running the example now and see if everything works right - use arrows to move around and space to rewind time.

Improvements

Complex State

Usually you want to save more data than just position of object, for example animation frame, rotation, health and whatever you want to change over time. Using state variable as a dictionary it's very easy and intuitive. Let's modify our example to save rotation as well. Below is just the snippet we need to adjust:

# test_object.gd
        # Save current state
        trackable.current_state = { pos = global_position, rot = global_rotation }
    else:
        # Retrieve and apply state
        var state = trackable.current_state
        if state != null:
            global_position = state.pos
            global_rotation = state.rot

Of course, you need to add some logic to rotate player in order to appreciate the effect. I added simply global_rotation += delta for each frame.

This way you can add as many properties to your state as object in hand requires, and all time related logic is neatly left inside Trackable object.

Slow Motion / Fast Forward (Or Reverse)

You might also want to add more time manipulation functions in your game. It's easy - in fact, reversing time at different speed works out of the box. Just set time_speed = -3 for example, and observe simulation going three times as fast in reverse (this is used in Rewound). Forward time, however, needs some changes in code. What needs to be done in particular depends on your project. For this example, all that is needed is to pass update_movement function delta * time_speed instead of delta. You can then set time_speed = 0.5 for half speed.

If you use rigid bodies,  you need to make additional changes to the code, which are out of scope for this tutorial.

Selective Effect

In Rewound not all objects are bound by time. If an object touches the Time Bubble, it will stay in place. The way I did it was by just discarding retrieved states if conditions matched.  Then, if the protection went away and player rewound time even further back, the object would follow it's original path in sync with everything. Notice how only one line has changed (here we trigger the effect with control button, you might want to check if overlapping with certain area):

# test_object.gd
        # Retrieve and apply state
        var state = trackable.current_state
        if state != null and not Input.is_key_pressed(KEY_CONTROL):
            global_position = state.pos
            global_rotation = state.rot

You might want something different for your game, however. Many felt this mechanic was a bit confusing and I see the problem - the fact that you are rewriting the future of the object and then rewinding to earlier timeline, so to speak, causes object to jump into earlier position. One could try just freezing the object in place, but not retrieve any states in the mean time. You'll need to modify the Trackable class to handle this one:

# trackable.gd
class_name Trackable
extends Node

# Save or retrieve current state from here
var current_state
# A LIFO (Last In First Out) stack of states
var history = []
# Used to track changes in time
var last_frame = 0
var frozen = false

func _physics_process(delta: float) -> void:
    # Calculate how many steps from beginning has elapsed
    var current_frame = int(TimeControl.time * TimeControl.history_rate)
    if not frozen:
        # Save or retrieve object state
        while current_frame > last_frame:
            history.push_front(current_state)
            last_frame += 1
        while last_frame > current_frame and history.size() > 0:
            current_state = history.pop_front()
            last_frame -= 1
    # Ensure frames matching even if object is frozen or has no history left!
    last_frame = current_frame

You can now change the value of frozen from the object when certain conditions are met. You also might want to allow for negative time inside TimeControl class by removing the max expression, since there now isn't absolute beginning point .

Performance Concerns

If your game has lots of moving objects, time manipulation mechanics may cause some performance issues. Usually this isn't the case and you don't need to optimize if there isn't any problems. Some problem comes from memory allocation - if your game has 100 objects, each save 64 bytes of information (for example 3D transformation matrix) 60 times a second, you end up saving around 1 MB of data every three seconds. Under the hood game might be reallocating the arrays multiple times as they grow out of initial capacity.  There are few ways to counter this.

First and foremost, think how much fidelity you need in movement - in Rewound, I use rate of 10 times per second. Because the reverse function runs at the speed of -3, objects appear to move smoothly at 30 fps. You might go even further and add some interpolation logic to smoothen movement.

Second, if you have a time limit in your game you can allocate the array in beginning. You can then use current_frame as an index to array instead of pushing and popping values from stack. This could pave a way towards making jumps in time.

Third, you can limit the amount of time you can go backwards. This could be easiest to do with circular arrays, however I haven't tried it out.

Conclusion

Time manipulation mechanics in games are very fun way to create unique interactions and intriguing puzzles. For such a powerful mechanic, it's surprisingly simple, consisting of simple framework of two code files and a few lines of extra code for each object using this framework.

I hope you have found this tutorial helpful. If you made a project with these techniques, you can drop a comment below :)

Any feedback is also welcome, from fixing grammatic errors to actual implementation notices.

All the code in this tutorial is free to use under CC0 license. No need to attribute if you don't want to.

Get Rewound

Leave a comment

Log in with itch.io to leave a comment.