In game development, "waiting" and "pausing" processes occur frequently. For example, waiting for an animation to finish, waiting for a network response, or resuming processing after a certain time. Async processing and coroutines are essential concepts for handling these efficiently without stopping the main loop (frame processing).
In Godot Engine's GDScript, the await keyword was introduced to handle async processing simply yet powerfully. This article covers everything from basic await usage to the underlying coroutine mechanics, performance considerations, and migration from yield used in Godot 3.
Why Async Processing and await Matter
Games need to run smoothly at all times. If some process (like loading a huge file or complex calculations) occupies the main thread for too long, the game freezes and user experience is severely damaged. This is called blocking.
Async processing with await is a powerful weapon for avoiding this blocking. await pauses function execution at that point and returns control to the Godot engine until waiting completes. Meanwhile, the engine can continue other processes (rendering, input handling, physics calculations, etc.), so the game doesn't freeze. When the awaited event (like a signal emission) occurs, the engine automatically resumes function execution from where it stopped.
Functions with this "pause and resume" mechanism are called coroutines. In GDScript, the moment you use await inside a function, that function automatically becomes a coroutine.
Basic Usage of await
await is used to wait for signals or the completion of other coroutines.
1. Waiting for Signals
The most common usage is waiting for signals emitted from nodes.
# Example: Apply damage after attack animation finishes
func _on_attack_button_pressed():
$AnimationPlayer.play("attack")
# Wait for AnimationPlayer's `animation_finished` signal
await $AnimationPlayer.animation_finished
# Animation finished, now execute damage calculation
print("Attack animation finished! Damage check!")
2. Waiting for a Duration (Timer)
To pause processing for a specified time, combine with the timer creation feature provided by SceneTree.
# Example: Spawn enemies after 3 seconds
func spawn_enemies_after_delay():
print("Game started! Enemies will appear in 3 seconds.")
# Create a SceneTreeTimer and wait for its `timeout` signal
await get_tree().create_timer(3.0).timeout
print("Time's up! Spawning enemies.")
# Enemy instantiation logic here
3. Waiting for Other Coroutines to Complete
await can also wait for other functions that are themselves coroutines.
# Function that loads resources asynchronously and waits for completion
func load_level_async() -> void:
print("Starting level data load...")
await get_tree().create_timer(2.0).timeout # Dummy load time
print("Level data load complete.")
# Game start sequence
func start_game():
# First fade in the UI (coroutine)
await fade_in_ui()
# Then load level data asynchronously (coroutine)
await load_level_async()
# All preparations complete, enable player control
print("Game Start!")
func fade_in_ui():
var tween = create_tween()
tween.tween_property($UI/CanvasLayer, "modulate:a", 1.0, 1.0)
await tween.finished
Note about Tween and
await: In cases where the Tween completes instantly—such as when duration is 0 or the target value is already reached at start—thefinishedsignal may not be emitted. In such cases,awaitwaits forever, so consider adding condition checks beforehand or implementing timeout handling.
The Relationship Between await and Coroutines
Functions containing await are automatically treated as coroutines. A coroutine is a function that can pause execution and resume later.
Coroutine Characteristics:
| Characteristic | Description |
|---|---|
| Cooperative | Processing switches are made explicitly by the programmer using the await keyword. |
| Pause and Resume | Pauses at await points and automatically resumes when the awaited target completes. |
| Stack Preservation | Even when paused, the execution context (stack), including local variables, is preserved. |
Migrating from Godot 3's yield to await
From Godot Engine 4.0 onwards, the method for writing async processing changed significantly. The yield keyword used in Godot 3 was deprecated and replaced with the await keyword.
Basic Migration Patterns
Godot 3.x (yield) | Godot 4.x (await) | Notes |
|---|---|---|
yield(object, "signal_name") | await object.signal_name | Signal waiting is the simplest migration pattern. |
yield(get_tree().create_timer(time), "timeout") | await get_tree().create_timer(time).timeout | Timer waiting is also written as signal waiting. |
yield(func_call(), "completed") | await func_call() | When waiting for async function completion. |
Practical Migration Example
# Godot 3.x (yield)
func wait_and_do_something():
yield(get_tree().create_timer(2.0), "timeout")
print("2 seconds passed!")
# Godot 4.x (await)
func wait_and_do_something():
await get_tree().create_timer(2.0).timeout
print("2 seconds passed!")
Common Mistakes and Best Practices
| Common Mistake | Best Practice |
|---|---|
Using await every frame inside _process | await inside _process pauses processing for one or more frames, causing unintended delays. Use await for one-time sequence processing triggered by specific events, and handle per-frame checks with state variables (State Machine) instead. |
await after node deletion | If a node being awaited is deleted with queue_free() during waiting, processing won't resume and causes errors. Check node existence with is_instance_valid(node) before waiting, or also await the node's tree_exiting signal. |
Confusing signal connection with await | await is for one-time waiting. To do something every time a button is pressed, connect the button_down signal to call a corresponding function. |
| Ignoring return values | Some signals pass arguments. await returns signal arguments, so use var result = await object.signal_name. |
Performance Considerations and Alternative Patterns
await Overhead
Creating and managing coroutines with await has a small cost. When hundreds or thousands of objects run coroutines simultaneously, scheduling overhead accumulates and can affect performance.
Alternative Pattern: Thread Class
For truly heavy processing (complex AI calculations, large-scale data processing, file I/O), await may cause slight stuttering on the main thread. For such CPU-intensive tasks, it's best to use the Thread class to run them on a background thread.
await (Coroutine) | Thread (Thread) | |
|---|---|---|
| Purpose | Cooperative multitasking on main thread (waiting) | Parallel processing in background (heavy computation) |
| Thread | Main thread only | Separate from main thread |
| Implementation | Just use await keyword | Create Thread object and call function |
| Caution | Processing itself runs on main thread | Accessing main thread data requires care (may need Mutex) |
Alternative Pattern: Direct Signal Connection
For persistent responses to events, connecting signals directly with connect is the standard approach, not await.
# Connecting signals (recommended event handling)
func _ready():
$MyButton.button_down.connect(_on_my_button_pressed)
func _on_my_button_pressed():
print("Button was pressed!")
Summary
The await keyword in Godot Engine is a powerful tool for handling async processing and coroutines in GDScript.
| Concept | Keyword | Role |
|---|---|---|
| Async Processing | await | Enables waiting and resuming without blocking the main thread. |
| Coroutine | Function containing await | A function that can pause and later resume execution. Enables concise complex sequences. |
When migrating from Godot 3 to Godot 4, replacing yield with await is required, but await enables more intuitive and modern async processing syntax.