Get_tree().create_timer()

Godot Version

4.5.1

Question

One of the common use cases for get_tree().create_timer() is to create some sort of delay. According to the documentation, create_timer() creates a new SceneTreeTimer and will be automatically freed. If the line of code that creates this timer can be called more than once, then wouldn’t it just be more efficient to have a Timer node in the scene and use that instead to avoid multiple instantiations and deallocations?

#in other words, instead of
await get_tree().create_timer(2).timeout

#do this instead
$Timer.start()
await $Timer.timeout

Other than that it’s quicker to type out “get_tree().create_timer()” compared to creating a node and referencing it, and it requiring 1 or 2 lines less of code, are there any other advantages to using create_timer()?

I’m a beginner to Godot but I keep seeing people use this function, so I don’t know… maybe I’m just missing something here.

Also, there are arguments we can pass to this function but it seems like we can also specify those same values in the inspector if we create a node instead.

Scene tree timers are generally meant for one-time fire-and-forget usage (e.g. a delay before a bomb goes off). So yeah, if you need a perpetual timer it totally makes more sense to use a node. Which one is more “efficient” would depend on the use case and what exactly you mean by “efficient”.

Also don’t use await with either type of timer. Instead, connect the timeout signal to a signal handling function.

4 Likes

Is it so things would be loosely coupled? Or is there another reasoning for it?

It’s to make code easier to understand and debug.

1 Like

Also, your assumption that it’s “more efficient” is erroneous. From the docs:

As opposed to Timer, it does not require the instantiation of a node.

This means you do not have the processing and memory overhead of creating a Node object. Godot is highly optimizaed by the Godot team, so I’m willing to bet that get_tree().timer() is going to use less resources that anything you could do - even if you were it do it in C++.

I strongly disagree with this.

To connect to a timer created by a get_tree().timer() you have to assign it to a SceneTreeTimer object - which gets dereferenced when it expires. this is just begging for bugs in your code. Plus you turn a one-line piece of code into a minimum of four just to accomplish the same task.

await get_tree().create_timer().timeout has some very specific uses. Typically waiting until you can execute the next line of code.

Example 1 - Waiting To queue_free()

An example in the project I’m working on this week. When anything blows up (it’s a space shooter), I want to wait until the explosion sound and the explosion particle effect complete before the node they are attached to disappear.

@onready var explosion_sound: AudioStreamPlayer2D = $"Explosion Sound"
@onready var explosion: GPUParticles2D = $Explosion
.
.
.
await get_tree().create_timer(max(explosion_sound.stream.get_length(), explosion.lifetime)).timeout
queue_free()

It would look like this is if I wanted to tie it to another function:

@onready var explosion_sound: AudioStreamPlayer2D = $"Explosion Sound"
@onready var explosion: GPUParticles2D = $Explosion
var scene_tree_timer: SceneTreeTimer
.
.
.
scene_tree_timer = get_tree().create_timer(max(explosion_sound.stream.get_length(), explosion.lifetime))
scene_tree_timer.timeout.connect(_on_scene_tree_timer_timeout)

func _on_scene_tree_timer_timeout() -> void:
	queue_free()

Example 2 - Quit Button

You just need a little time for the click sound to play before killing the program. (Or doing whatever the next thing is that wipes out the current screen. Like play a death sound.)

func _on_button_pressed() -> void:
	await get_tree().create_timer(0.25).timeout # Just enough time to hear the click sound.
	Game.quit() #Custom function, but it's instantaneous

Example 3 - Fading Music In/Out

_fade_out(music_player, fade_time)
await get_tree().create_timer(fade_time).timeout
music_player.set_stream(song)
music_player.play()

Example 4 - Removing/Adding Something in the UI with a Tween

func remove_missile() -> void:
	displayed_missiles -= 1
	var missile: TextureRect = missile_display.get_child(0)
	var tween: Tween = create_tween()
	tween.tween_property(missile, "material:shader_parameter/progress", 1.0, dissolve_speed)
	await get_tree().create_timer(dissolve_speed).timeout
	missile.queue_free()

Example 5 - Limiting the Rate of Fire for a Weapon/Ability

if Input.is_action_pressed("fire"):
	is_gun_ready = false
	shoot()
	await get_tree().create_timer(fire_rate).timeout
	is_gun_ready = true

Example 6 - Setting a Delay between Songs in Level Music

func _on_song_finished() -> void:
	await get_tree().create_timer(3.0).timeout
	level_playlist.pick_random().play()

Conclusion

In my opinion, you should always be creating an actual Timer (not a SceneTreeTimer) if you want to connect to a function on timeout. So you should use get_tree().create_timer() when you want to have your code wait asynchronously for time to pass before doing the next thing. It’s helpful in keeping your code together and readable.

Ultimately, I believe it’s important to use the option that makes your code more readable. It’s as simple as that. And if you are creating a Timer object in memory, you should be re-using that sucker.

2 Likes

#1! Totally agree, still I’d say scene tree timers shouldn’t be the first tool reached for. Using await versus connecting a function typically connecting is better. Less coroutine overhead and potential spiraling when functions are called again (as I’m sure we’ve seen in _process), but in one-off circumstances it is easier to read await.


Your first example could await either the audio stream player or the particle emitter, with .finished for either. Of course with an if statement to choose the longest delay if needed.

await explosion_sound.finished
# or
await explosion.finished
queue_free()

Again for the second example waiting on a sound should use .finished, but the entire game is quitting so performance is of little concern.

Your _fade_out could return a Signal if it uses a tween to fade out, such as tween.finished allowing you to await on that instead of creating another timer.

await _fade_out(music_player, fade_time)
music_player.set_stream(song)
music_player.play()

Similarly you should await the tween’s finished

Fire rate could be best expressed with a Timer node, which may be more upfront overhead to instantiate but could help prevent frame dependency, and repeating the timer is essentially free compared to create_timer().

if Input.is_action_pressed("fire") and timer.is_stopped():
	shoot()
	timer.start()

Similarly I’d recommend a Timer node as you may reuse this delay.

func _on_song_finished() -> void:
    timer.start()

func _on_timer_timeout() -> void:
    level_playlist.pick_random().play()

I think it’s also worth noting create_timer doesn’t obey pausing the game without assigning it’s second argument to false.

3 Likes

Context my friend, context. I didn’t say it to you. I said it to a person asking about the difference between a node and a scene tree timer. Even in a general context, using handlers instead of awaits is a good bug-proofing tactic due to increased mental load required to follow the await execution.

You don’t have to assign it to anything and SceneTreeTimer will be created and destroyed anyway. It also doesn’t have to increase the line count whatsoever:

get_tree().create_timer(1).timeout.connect(func(): print("one second passed"))

There are no “very specific uses” that cannot be done with signal handlers. If writing a short standalone handler bothers you, you can always stick in a lambda.

get_tree().create_timer(delay).timeout.connect(queue_free)

Same:

get_tree().create_timer(0.25).timeout.connect(Game.quit)

Rest of your examples are just more of the same, except for cases where you’re awaiting the wrong signal which @gertkeno already addressed.

The problem all this poses to a beginner who doesn’t fully understand coroutines is that those trivial examples tend to be quite misleading as to what is really happening with the execution flow, whereas using signal handlers is straightforward and makes it obvious what your code will be doing and when. On the basis of such trivial examples, the await is typically vaguely “understood” as some “pausing the code” mechanism, and then wrongly used in more complex situations, almost always leading to bugs and confusion.

Last but not least, signal connections can be cancelled if you change your mind due to some unexpected state change, whereas awaits, once uttered, will be sitting there waiting until the signal arrives or forever.

3 Likes

I bow to @gertkeno and @normalized and their greater knowledge in this area. Glad I posted though, as I learned new things and clarified my own knowledge! I did not know Tween had a finished signal.

My only question is with my second example. I very specifically used that because both explosion_sound and explosion are nodes where @export variables can be set. So a BigAsteroid and MediumAsteroid have different explode sounds and different explosion times as I tweaked them. I wrote the code so that I didn’t have to adjust it as I adjusted those values - or care which was longer.

I’m not sure how to await the longer of two signals. Though if there’s a way, I would love to be able to do it.

1 Like

One more point in favor of handlers vs coroutines. Connect them both to the same handler, whichever happens first - takes the prize.

1 Like

Ooh I thought of another one. I’m doing this because UserProfile is an Autoload and player.gd is a @tool script that runs before the Autoloads are ready. Is there another way to handle this?

func _ready() -> void:
	visibility_changed.connect(_on_visibility_changed)
	if not UserProfile.is_node_ready():
		await UserProfile.ready
		player_statistics = UserProfile.player_statistics

That makes sense. You’d just have to check if it’s already freed first in the handler or you’d run into an issue of trying to free something already outside the tree.

And while we are on the subject, does either of you know how to write code that ignores an Autoload that doesn’t exist? I can’t do it without getting an error.

Freeing the object will break all its signal connections so the other signal could never call the handler. Even if you don’t need to free the object you can simply disconnect both signals on the first call.

Ok. I’ll try it.

If you’re using autoloads for persistent/global data storage, you could use static data instead. That way you won’t need to worry if the thing is in the tree or not.

Ok, but I had the same problem with a curved test node trying to load the localization plugin before it was loaded.

Hard to help you with specifics without running the thing. The order of initialization in the scene tree is deterministic. You need to know it so you can put things in their proper place. Doing hacks with await may be fine in some cases though.

1 Like