Waiting for Godot

When I was writing my first game in Godot, I wanted to add some simple cut-scenes. First the scene fades in. After a few seconds the bad guy enters. Yet another few seconds later the spotlight turns on and shines on him. Etc, etc.

Of course, I could have made one big Animation that handles all of this, but I wanted something more modular. I also wanted to introduce delays in other parts of the game for cinematic or comical effects. I started adding many Timer elements to all my scenes for each delay and connecting their "timeout" signals to many functions in the code. But that was ugly, and not what I wanted. Basically, what I wanted was something like the "sleep(time)" function I know from other languages.

It turns out that there is a one-liner that gives you that behaviour (to some extent), but it is a one-liner with a lot of baggage.


The one-liner is as follows:
yield(get_tree().create_timer(1), "timeout")
If you place this line in your function, the lines below it will only start executing one second later. There is quite a bit to unpack here, since basically every word is non-trivial:
  • yield is a keyword indicating that the current function stops running for now. If used without arguments, the function "itself" and in its current state is returned to the calling function. The current function only continues if the calling function chooses to:
    func yielding_func():
        print("Before")
        yield
        print("After")
    
    func _run():
        var func_state = yielding_func()
        func_state.resume()
    
    The standard GDScript documentation explains yield in more detail. In the one-liner above, we use the fact that yield can be combined with signals:
    yield(object, signal)
    That is, instead of waiting for the calling function to call resume on the function state, the function waits until the specified object emits the specified signal. A common usage of this is to wait for an animation to be finished:
    $AnimationPlayer.play("player_dies")
    yield($AnimationPlayer, "animation_finished")
    
  • get_tree() returns the Scene Tree that the Node to which this script is attached is a part of. Presumably, whenever this script is running, the Node is in a Scene Tree which is currently "active" in the game. So any modification to the Scene Tree returned from this call will have a direct impact on the game.
  • create_timer() creates a special type of timer called a SceneTreeTimer. This instance is managed by the Scene Tree itself. That is, it is started automatically by the Scene Tree upon creation and the instance is removed from the Scene Tree after the "timeout" signal has been emitted. As you can see in its documentation, your own script has no control over the timer except to check how much time is left.
  • "timeout", finally, is the name of the signal that we wait for the SceneTreeTimer to emit. Once received, the function continues.
An important remark: because we use yield, the calling function will continue its execution.
Since this is a rather subtle point and may not be exactly what you have in mind when using this one-liner, here is an example that hopefully drives this point home.

Suppose that, now that we know of this trick, we want to make a nice abstract function sleep to keep our code clean:
func sleep(duration):
    yield(get_tree().create_timer(duration), "timeout")

func _run():
    print("Let's wait for...")
    sleep(2.5)
    print("...Godot.")
While you may expect the result of this code to be: "Let's wait for..." <nothing for 2.5 seconds> "...Godot.", it will actually be: "Let's wait for..." "...Godot." <nothing for 2.5 seconds>. The reason is that after the SceneTreeTimer was created in the sleep function, the sleep function yielded and returned control to the calling _run function, which resumed its execution right away. After 2.5 seconds the sleep function would continue its execution, if there was any.

In short, there is not really any way to get sleep-like behaviour any shorter than the one-liner on top of this page. You could abstract away the get_tree().create_timer(duration) part but I doubt that will make your code any cleaner.

No comments:

Post a Comment

Most popular posts in the last month: