wrote a simple replay system for Godot 4 by recording inputs instead of state — surprisingly clean

302 views 0 replies

Been wanting a replay system for my Godot 4 project for a while, mostly for bug reporting, but a death replay feature also felt achievable. Started down the state-snapshot path and got buried fast. Serializing every node's relevant state every N frames is a mess to maintain once your entity count grows.

Switched to input recording instead and it's honestly way simpler. You record the full input snapshot each tick plus the initial RNG seed, then replay by feeding recorded inputs back into your normal game loop. If your simulation is deterministic, you get accurate replays basically for free.

Core of what I have:

class_name ReplaySystem
extends Node

enum Mode { NONE, RECORDING, REPLAYING }

var mode: Mode = Mode.NONE
var recorded_frames: Array[Dictionary] = []
var replay_index: int = 0
var initial_seed: int = 0

func start_recording() -> void:
    recorded_frames.clear()
    initial_seed = randi()
    seed(initial_seed)
    mode = Mode.RECORDING

func stop_recording() -> void:
    mode = Mode.NONE

func start_replay() -> void:
    replay_index = 0
    seed(initial_seed)
    mode = Mode.REPLAYING

func record_frame(input_snapshot: Dictionary) -> void:
    if mode == Mode.RECORDING:
        recorded_frames.append(input_snapshot)

func get_replay_frame() -> Dictionary:
    if mode == Mode.REPLAYING and replay_index < recorded_frames.size():
        var frame = recorded_frames[replay_index]
        replay_index += 1
        return frame
    return {}

The input snapshot is a dictionary of action names to values, booleans for digital and floats for analog. I route everything through an InputManager autoload so all game systems read from there instead of calling Input directly. During replay, InputManager returns recorded frames instead of real input. Nothing else in the game knows it's happening.

The big catch: your simulation has to be deterministic. Godot's built-in physics isn't, so I'm running a purely kinematic character controller. Also had to audit everywhere I call randf() or randi() to make sure it all flows through the same seeded stream. Found two places I'd forgotten about.

Once the foundation was in, death replays took maybe an afternoon to hook up. I'm saving replays to disk as JSON for bug reports and it's been genuinely useful already. Anyone else gone this route in Godot and hit determinism gotchas I haven't stepped on yet?

Moonjump
Forum Search Shader Sandbox
Sign In Register