We've discussed how coroutines can be a great tool to employ while making games in our article: Coroutines - A useful tool in every developer's toolbelt. Now we'll have a look at four ways in which a coroutine can be useful using code examples in the Godot game engine.
Note: A project containing the examples below is available on our Github account. The code examples were written in GDScript using Godot version 3.5.
Using a coroutine as a timer
This was the example detailed in the article mentioned above, now applied to Godot. It consists of starting a coroutine, pausing execution for three seconds, and ending it with a print
statement. Essentially emulating a countdown timer or time delay.
extends Node2D #Extending a 2D or 3D node is not relevant func wait_and_print() -> void: yield(get_tree().create_timer(3), "timeout") print("Waited for three seconds!") func _ready(): wait_and_print()
Using a coroutine for animation
In this example I created a coroutine to fade the opacity of a sprite through time until it gets completely transparent. The project setup is just a main 2D node with the script below attached to it and a sprite named "Icon" as a child. As for the code this time the yield
statement pauses execution and then resumes in the following frame. This behaviour is similar to the Godot's _process()
method, however a coroutine is more convenient because using _process()
you would need to control the start and end of the fade with conditional checks as this method is always running.
extends Node2D func fade() -> void: var range_value :float = -100 for x in range(range_value, 1): var alpha_value := x/range_value var color := Color(1, 1, 1, alpha_value) $Icon.self_modulate = color yield(get_tree(), "idle_frame") func _ready(): fade()
Using a coroutine for a regular occurring task
You can use a coroutine for a task that needs to occur on a time interval or for a task that could be executed less frequently than every frame for performance reasons, such as a function that checks for the current position of an enemy, for example.
Using the same sprite as before, I changed the code to make it blink this time around. The new code makes the sprite repeatedly appear and disappear within a one second time interval.
Here's how this code works:
When entering play mode the _ready()
method is called initiating the toggle_visibility()
coroutine. The coroutine enters an infinite loop set by the statement while(true)
. Inside the while
loop the sprite's visibility option is inverted (toggled). The execution stops for a second with the yield(get_tree().create_timer(1), "timeout")
statement and then resumes entering the next iteration of the while loop.
extends Node2D func toggle_visibility() -> void: while(true): $Icon.visible = !$Icon.visible yield(get_tree().create_timer(1), "timeout") func _ready(): toggle_visibility()
I made this coroutine endless by using an infinite while loop but you could of course add a way to end it. The easiest way to do this would be to change the condition for the while loop like I did in the example below. The coroutine can now be stopped by using the keyboard keys bound to the ui_cancel
action of the input map (escape by default).
extends Node2D var cancel := false func toggle_visibility() -> void: while(!cancel): $Icon.visible = !$Icon.visible yield(get_tree().create_timer(1), "timeout") func _ready(): toggle_visibility() func _process(_delta): if (Input.is_action_just_pressed("ui_cancel")): cancel = true
Creating an animation chain using coroutines
In this example I trigger a new animation after the first one finishes, creating an animation sequence. Just like in the "Using a coroutine for animation" example above I used the opacity fade for both animations. You can, of course, sequence totally different kinds of animations and behaviours. Since I wanted to apply the same fade to the additional sprite, I just adapted the coroutine to be run a second time.
extends Node2D var faded :bool = false func fade(sprite :Sprite) -> void: var range_value :float = -100 for x in range(range_value, 1): var alpha_value := x/range_value var color := Color(1, 1, 1, alpha_value) sprite.self_modulate = color yield(get_tree(), "idle_frame") if (!faded): fade($Icon2) faded = true func _ready(): fade($Icon1)
The additional sprite was just duplicated from the existing one and kept as a child of the main node containing the script.
Here's an explanation of the code above:
I made the coroutine receive the desired sprite node to fade as an argument. The coroutine is started with the first sprite node named "Icon1". After the fade completes, the coroutine is started again inside the conditional check, this time with the second sprite "Icon2". I've put this conditional check here to prevent an unwanted infinite loop as without it the coroutine would continue to trigger itself with the second sprite over and over again.
Additional info
In the examples above to control the "yielding" stage of the coroutine, I used the yield(get_tree().create_timer(n), "timeout")
and the yield(get_tree(), "idle_frame")
statements. The former yields after the defined number of seconds by creating a SceneTree timer, the latter yields on every frame update.
The string values "timeout" and "idle_frame" are called signals. You can create your own signals to resume coroutines, however that goes beyond the scope of this article and you probably won't need more than what's detailed here while developing your projects.
Below are some yielding signals that I think are important for you to know about. They can be useful while using coroutines to control physics, camera behaviours and more.
physics_frame - Waits until the next fixed frame update.
frame_pre_draw - Emitted at the beginning of the frame, before the VisualServer updates all the Viewports.
frame_post_draw - Emitted at the end of the frame, after the VisualServer has finished updating all the Viewports.
Note that frame_pre_draw and frame_post_draw are VisualServer signals. You can use them like this:
yield(VisualServer, "frame_pre_draw")
yield(VisualServer, "frame_post_draw")
As an additional note:
Using coroutines on nodes that can be destroyed (removed from the scene) during gameplay may not be the best option, as a yielded function will still try to resume even if the instance that contains it is gone, creating runtime errors. In those cases you can use the timer node as an alternative to the SceneTree timer used in the previous examples. You can also implement custom functionality to deal with this problem (like creating a queue for safe destruction).
Conclusion
We've gone over four ways in which you can make use of coroutines in your projects. I hope you found the examples useful and easy to understand. During the development of your projects and now armed with this knowledge you will probably find instances where coroutines could be the perfect solution for your specific problem.
Do you have any questions or want to share your thoughts with us? Let us know in the comments!