Inconsistent timer behavior

Godot Version

4.4

Question

I noticed today that timers have extremely inconsistent behavior - specifically during the first second or so of the project running. I use this method in my Lib singleton to handle delays as a multiple of the global Lib.time:

func sleep(ticks: float = 1.0) -> void:
var start = Time.get_ticks_msec()
await get_tree().create_timer(ticks * self.time).timeout
var end = Time.get_ticks_msec()
print("Expected time elapsed: %s, actual time elapsed: %s seconds" % [ticks * self.time, ((end - start) / 1000.0)])

The timing and print behavior is just there for debugging of course, usually it’s just the second line. Lib.time is 0.8 by default, changed to 0.3 for certain unit tests. In said unit test, when I run for i in range(10): await Lib.sleep(), I get the following output:

Expected time elapsed: 0.3, actual time elapsed: 0.066 seconds
Expected time elapsed: 0.3, actual time elapsed: 0.303 seconds
Expected time elapsed: 0.3, actual time elapsed: 0.297 seconds
Expected time elapsed: 0.3, actual time elapsed: 0.303 seconds
Expected time elapsed: 0.3, actual time elapsed: 0.303 seconds
Expected time elapsed: 0.3, actual time elapsed: 0.303 seconds
Expected time elapsed: 0.3, actual time elapsed: 0.303 seconds
Expected time elapsed: 0.3, actual time elapsed: 0.305 seconds
Expected time elapsed: 0.3, actual time elapsed: 0.301 seconds
Expected time elapsed: 0.3, actual time elapsed: 0.303 seconds

This behavior is very consistent; every time I run it the first delay is ~0.06-0.08 seconds, and every subsequent delay is 0.3 +/- 0.005 seconds. I also tried running the same loop in my main code instead of in unit testing; in this case Lib.time was set to 0.8, the first delay was ~0.65 seconds, and every subsequent delay was 0.8 +/- 0.01 seconds. The minor variations are not a problem but being off by 0.2 seconds is a major problem. Why do timers do this?

await uses coroutines which are scheduled, not theaded. Godot processes a frame of gameplay, at the end checks the scheduled coroutines. If the frame took 2 seconds, then the fastest time that await could be re-evaluated is 2 seconds.

If you care about repeating a timer, use a Timer Node with “One Shot” off, this node will carry over lost time when it’s scheduled to run.

Should’ve clarified - the goal isn’t actually to repeat a timer, just to get consistent behavior when creating timers. The for loop is purely to show this inconsistency.

It makes sense that await only gets checked after each frame, but why would that cause the first timer to go faster instead of slower?

How do you define and set self.time?

It defaults to 0.8 in Lib’s definition, then the unit test changes it to 0.3 before this for loop.

I found a way to force consistent behavior. Lib._init() now includes the following lines:

timer = Timer.new()
add_child(timer)

And Lib.sleep has been changed to:

func sleep(ticks: float = 1.0) -> void:
timer.start(ticks * time)
await timer.timeout

This was inspired by your suggestion to not use oneshot timers. I still don’t fully understand why this gets more consistent behavior, but it does.

Side note: this method also seems to guarantee a delay greater than the nominal time, specifically between 0.302-0.304 seconds (always within this range after 50 tests), where the previous oneshot method gave times from 0.295-0.305 seconds. No idea why that difference occurs but it could be useful to someone.

1 Like

This topic was automatically closed 30 days after the last reply. New replies are no longer allowed.