Godot and GDScript - The Wonders of Yielding

So I’ve recently been acquainting myself with the Godot game engine, starting to mess around with it some time around when version 3.0 was in alpha, then using 2.1 to build my first small jam game with it, unfortunately didn’t finish but did end up learning a lot in the process!

ld39 jam game, pew pew LD39 game pictured

Over the course of the time following, I’ve been starting progressively more and more projects in it, now using 3.0.2 since the release of 3.0 a while back, developing a jam game called vroom vroom kaboom for LD40 as well as another small jam game (yet another puzzle game), called dimensional scarcity for a later jam called Heart Jam.

But also the latest and most long running project (started during alpha 2 of Godot 3.0), is called memento mori and is a joint effort between me and a friend by the name jammybread (who is a wonderful individual who has done roughly half the art and basically all of the narrative on the game), a small narrative-focused game, not very gamey at all.

The Crux Of It All

Aside from all the details above, the point is that in the game, there are a lot of linear sequences of fades and transitions that end up being expressed as a series of callbacks.

Something like this:

func fade_first_thing():
    fade.connect("tween_completed", self, "fade_second_thing",
        [], CONNECT_ONESHOT)
    fade.interpolate_property(...) # params omitted
    fade.start()
    
func fade_second_thing():
    fade.connect("tween_completed", self, "fade_third_thing",
         [], CONNECT_ONESHOT)
    fade.interpolate_property(...) # params omitted
    fade.start()
    
func fade_third_thing():
    # ... you see where this is going, yes?

Now you might already be thinking, “but profan, is there not a linear way to express this logically linear spaghetti? where are your coroutines?”
.. and yes! It so happens that there is a way, and we have exactly coroutines3 of sorts in GDScript in Godot too!

The Actual Thing

So this is a real sample of code from my code, yesterday I was mulling over this exact problem and thinking “every time I change this, I have to add yet another function.. yet it’s only a linear sequence of operations”, and I came to the realization that we had the tool! We have yield1 !

I went from this:

func _start_pressed():
    
    Game.fade_out_music(3.0)
    
    tw_clouds.interpolate_method(
        self, "_bg_clouds_fade", bg_clouds.modulate.a, 0.0, 2.5,
        Tween.TRANS_QUAD, Tween.EASE_IN_OUT)
    tw_clouds.start()
    
    tween.interpolate_property(
        main, "modulate:a", main.modulate.a, 0.0, 1,
        Tween.TRANS_QUAD, Tween.EASE_IN_OUT)
    tween.connect(
        "tween_completed", self, "_on_start_fade_out_menu",
        [], CONNECT_ONESHOT)
    tween.start()
    
func _on_start_fade_out_menu(obj, key):
    tween.interpolate_method(
        self, "_bg_moon_fade", bg_moon.modulate.a, 0.0, 1,
        Tween.TRANS_QUAD, Tween.EASE_IN_OUT)
    tween.connect(
        "tween_completed", self, "_on_start_fade_mountain",
         [], CONNECT_ONESHOT)
    tween.start()

func _on_start_fade_mountain(obj, key):
    tween.interpolate_method(
        self, "_bg_end_fade", bg_yama.modulate.a, 0.0, 1.0,
        Tween.TRANS_QUAD, Tween.EASE_IN_OUT)
    tween.connect("tween_completed", self, "_on_start_fade_end")
    tween.start()

func _on_start_fade_end(obj, key):
    SceneSwitcher.goto_scene(Game.Scenes.BEDROOM)

… to the much leaner, much nicer to modify:

func _start_pressed():
    
    Game.fade_out_music(3.0)
    
    tw_clouds.interpolate_method(
        self, "_bg_clouds_fade", bg_clouds.modulate.a, 0.0, 2.5,
        Tween.TRANS_QUAD, Tween.EASE_IN_OUT)
    tw_clouds.start()
    
    tween.interpolate_property(
        main, "modulate:a", main.modulate.a, 0.0, 1,
        Tween.TRANS_QUAD, Tween.EASE_IN_OUT)
        tween.start()
    
    yield(tween, "tween_completed")
    tween.interpolate_method(
        self, "_bg_moon_fade", bg_moon.modulate.a, 0.0, 1,
        Tween.TRANS_QUAD, Tween.EASE_IN_OUT)
    tween.start()
    
    yield(tween, "tween_completed")
    tween.interpolate_method(
        self, "_bg_end_fade", bg_yama.modulate.a, 0.0, 1.0,
        Tween.TRANS_QUAD, Tween.EASE_IN_OUT)
    tween.start()
    
    yield(tween, "tween_completed")
    SceneSwitcher.goto_scene(Game.Scenes.BEDROOM)

Because this is inherently a visual thing we’re doing here, lets show the work!
fade sequence shown ingame

So what is this demonic construction really?

Building on the system of signals in Godot, yield in GDScript basically suspends execution of the given function where the yield occurs, where when the signal fires that the yield hooked into, control is transferred back into the function and it continues from where it stopped, neat, isn’t it?

If you’re looking for a deeper understanding of coroutines themselves in other contexts, I refer to the further reading section below, in a games context the Godot page is already quite interesting, but the others may offer deeper insight too, as in different programming languages/environments, the concept is introduced slightly differently, but the meaning of what a coroutine is remains the same, so the knowledge can stay with you.

Misc

Just to clarify, yield can be used without any parameters to simply transfer control back to the caller, the Godot article and the wikipedia source elaborates on coroutines in a more general context, but the workflow in Godot with combining signals and yield is what I wanted to highlight!

Thanks for reading, until next time!

Further Reading

1. coroutines with yield - godot docs

2. In C# coroutines are expressed with the aid of async/await, but the concept remains the same.

3. coroutines on Wikipedia, many languages to be found here