Hello, I made a deterministic replay system that store every player's input in a singleton. It works but the replay is not always the same as the original.

Here is the code, it's no rocket science:

extends Node

enum State {STOP, RECORD, PLAY}

# h_ for history (we record in here)
var h_time := Array()
var h_inputs := Array()
var h_start: int

# r_ for replay (we read in here)
var r_time := Array()
var r_inputs := Array()
var r_start: int

var state

# keep track of pressed keys so we can release them manually at end
var pressed : Dictionary = {}

func _ready() -> void:
	change_state(State.STOP)


func _input(event: InputEvent) -> void:
	if state == State.RECORD:
		h_time.append(OS.get_ticks_usec() - h_start)
		h_inputs.append(event)

		match event.get_class():
			"InputEventKey":
				pressed[event.scancode] = event.is_pressed()



func _physics_process(_delta: float) -> void:
	if state == State.PLAY:
		var ticks_usec = OS.get_ticks_usec() - r_start
		while !r_time.empty() && r_time.front() <= ticks_usec:
			r_time.pop_front()
			Input.parse_input_event(r_inputs.pop_front())


func record():
	seed(0)
	change_state(State.RECORD)
	h_inputs = Array()
	h_time = Array()
	h_start = OS.get_ticks_usec()

func stop():
	change_state(State.STOP)


func play():
	seed(0)
	change_state(State.PLAY)

	# start with everything released
	# (or issue will rise when replay multiple times and player has a key pressed at end)
	for scancode in pressed:
		if pressed[scancode]:
			var ev = InputEventKey.new()
			ev.scancode = scancode
			ev.physical_scancode = scancode
			ev.pressed = false
			Input.parse_input_event(ev)

	# copy history in replay
	r_start = OS.get_ticks_usec()
	r_inputs = h_inputs.duplicate()
	r_time = h_time.duplicate()


func change_state(enum_value):
	print("[REPLAY] ", State.keys()[enum_value])
	self.state = enum_value


func is_playing():
	return self.state == State.PLAY

I do not have a single RNG in the game except for the camera which can shake a bit (I removed it and still have same issue). Is there something obvious I missed? How would you have done it?

Hi there! I'm working on a replay system for my game and I decided to record all the player's inputs in a singleton. Then I just have to play the inputs again to replay. It works fine except for one thing.

If the player triggers the end of my level with a key pressed (right key for example). The replay will start as if the right key is pressed! Which is wrong and mess up completely the replay.

Player releases keys before ending: replay is fine

Player do not release keys before ending: replay is broken

I'm playing the inputs with the Input.parse_input_event/1 func. I'm using get_tree().reload_current_scene() before replaying to start from a clean state.

Is there a way to reset the inputs state? or should I manually track every key press in order to release them at end?

I've merged the 2 topics since they seem to be virtually the same.

@doobdargent said:

If the player triggers the end of my level with a key pressed (right key for example). The replay will start as if the right key is pressed! Which is wrong and mess up completely the replay.

Then perhaps you need to implement a state for clearing registered inputs?

Sorry about the double post, I was sure the 1st one got lost. I managed to fix my issues.

  • About the inputs state, I created a Replay.release_inputs/0 func that I call whenever I quit the replay. The function kept track of the pressed and just play the release of what is still pressed.

  • About the physics, I changed my var ticks_usec = OS.get_ticks_usec() - r_start lines to:

cursor += delta * 1000000
ticks_usec = cursor

Instead of relying on get_ticks_usec/0 I calc my own tick via delta. It seems to work. My replay are looking good! (I am not 100% sure because it's kind hard to test)

Cheers

There is or was a bug or limitation regarding OS.get_ticks_usec() in an HTML5 export. The low three digits were always returned as 0, so it was equivalent to using OS.get_ticks_msec(). I don't know if that applies to your situation.

I ran into that issue because RandomNumberGenerator::randomize() uses OS.get_ticks_usec(), and I was misusing the former.

Godot's built-in physics engines are not deterministic. You won't get the same output for a given set of inputs unless you write your own deterministic physics engine in GDScript (preferably using fixed-point math only, for reproducibility across platforms and CPU architectures).

To avoid issues with the lack of determinism (and your replays breaking when your game is updated), I recommend storing transforms (positions/rotations) instead of inputs. If this is for a competitive game, you could store both the transforms and inputs in the same replay file to make cheating easier to detect. That said, due to the lack of determinism, this won't be suited for automated replay verification (but it can still be useful for manual human verification).

7 months later