Game Development with Godot & GDScript Module 4 – The scene tree, nodes and signals
await, coroutines and timers
Pause GDScript functions with await, return values from coroutines, time events with create_timer and Timer nodes, and avoid the await mistakes that hang games.
What you will learn
- Use await to pause a function on a signal, and predict where its caller goes on
- Write coroutines that return values, and predict what await gives for a signal's arguments
- Choose between create_timer, a Timer node and frame signals, and handle pause and Engine.time_scale
- Prevent await mistakes such as freed targets, signals that come too early and a timer awaited without timeout
Before you start
What you’ll learn · Before you start
What you will learn
- Use await to pause a function on a signal, and predict where its caller goes on
- Write coroutines that return values, and predict what await gives for a signal's arguments
- Choose between create_timer, a Timer node and frame signals, and handle pause and Engine.time_scale
- Prevent await mistakes such as freed targets, signals that come too early and a timer awaited without timeout
Before you start
On this page
A half-second pause in a cutscene, written await get_tree().create_timer(0.5).timeout, lasted 30 frames at 60 frames
a second. Written without .timeout, it lasted 0 frames, and the next line of dialogue came at once, with no error to
say why. await lets a function stop in the middle and go on later, and that is how cutscenes, cooldowns and delayed
effects are written in GDScript.
This page shows what await does to a function and to the code that called it, what it gives back, and how timers,
frames, pause and time scale decide when a wait ends. Then come the ways a waiting function is caught out by a world
that changed while it waited.
await pauses a function, and its caller goes on
countdown(from) prints a number, waits for the next frame with await get_tree().process_frame, and repeats.
_ready() starts one countdown without await and a second one with it. A small counter numbers the frames from 1:
it is connected to the same process_frame signal first, so it has already counted each new frame when the
countdowns go on:
extends Node
# Two countdowns that wait one frame between numbers. The first runs on its own; _ready() waits for the second.
var frame := 0
func _enter_tree() -> void:
# Godot emits process_frame at the start of every frame. Connected first, this counter runs before any function
# that waits for the same signal, so frame already holds the new frame's number when they go on.
get_tree().process_frame.connect(func() -> void: frame += 1)
func _ready() -> void:
print("_ready: starts countdown(3) without await, ", when())
countdown(3)
print("_ready: goes on at once, then awaits countdown(2)")
var said: String = await countdown(2)
print("_ready: countdown(2) returned \"", said, "\" ", when())
await get_tree().process_frame
get_tree().quit()
func _process(_delta: float) -> void:
print("_process ", when())
func countdown(from: int) -> String:
for n in range(from, 0, -1):
print(" countdown(", from, "): ", n, " ", when())
await get_tree().process_frame
print(" countdown(", from, ") is done ", when())
return "go"
func when() -> String:
return "before the first frame" if frame == 0 else "in frame %d" % frame from session import files, run
files("project.godot", "countdown.tscn", "countdown.gd")
run("""
godot --headless --no-header --fixed-fps 60 countdown.tscn
""") Output
$ godot --headless --no-header --fixed-fps 60 countdown.tscn _ready: starts countdown(3) without await, before the first frame countdown(3): 3 before the first frame _ready: goes on at once, then awaits countdown(2) countdown(2): 2 before the first frame countdown(3): 2 in frame 1 countdown(2): 1 in frame 1 _process in frame 1 countdown(3): 1 in frame 2 countdown(2) is done in frame 2 _ready: countdown(2) returned "go" in frame 2 _process in frame 2 countdown(3) is done in frame 3 _process in frame 3
Recorded with Python 3.14.8 on macOS 26 arm64. To run it yourself: mise exec python@3.14.8 -- python3 countdown_session.py
A function that contains await is a coroutine: A function that contains await. At each await it pauses and hands control back to its caller, and it goes on from the same line when the signal it waits for is emitted; await on the call waits for its return value.. When it reaches an await, it pauses
and hands control back to whoever called it, and it goes on from the same line when the signal it waits for is
emitted. Step through the run with that in mind:
The drawing shows the whole scene; its 6 steps are listed below.
Every step as text
- countdown(3) without await. _ready() calls countdown(3) with no await. It prints 3, reaches its own await and hands control straight back, so _ready() goes on at once, before the first frame.
- countdown(2) with await. _ready() now calls var said = await countdown(2). The countdown prints 2 and pauses at its await; because _ready() awaits it, _ready() pauses too.
- Frame 1 begins. Godot emits process_frame at the start of frame 1. Both countdowns go on from their awaits; countdown(3) prints 2 and countdown(2) prints 1, then each waits again.
- Then the frame's _process. Code that waited for process_frame runs before the frame's _process(), which prints _process in frame 1 only after both countdowns have printed.
- countdown(2) returns. In frame 2 the loop of countdown(2) has nothing left. It prints done and returns "go", and the return goes to the code that awaited it.
- _ready() goes on. _ready() wakes up in frame 2 with said = "go". countdown(3), which nobody waits for, finishes on its own in frame 3.
countdown(3) printed 3 and paused, and _ready() went on at once, before the first frame. _ready() waited for
countdown(2), because it wrote await in front of the call: a caller pauses only if it awaits. When frame 1 began,
process_frame woke both countdowns, and only then did the frame’s _process() run. countdown(2) was done in frame
2, _ready() went on with its result, and countdown(3), which nobody waited for, finished in frame 3 on its own.
Key idea
await pauses only the function it is in. Its caller goes on at once unless it awaits the call too, and the paused
function goes on from the same line when its signal is emitted.
Checkpoint
Check: What await does to a function and its caller
A coroutine hands its result to the code that awaits it
A coroutine can return a value like any function; the caller gets it as the result of its await. await on a
signal gives the values that came with the emit. results.gd tries each case, and no_await.gd uses a coroutine’s
result without await:
extends Node
# What await gives back: a coroutine's return value, a signal's arguments, and a value that is neither.
signal bell_rang
signal coins_found(coins: int)
signal chest_opened(coins: int, by: String)
func _ready() -> void:
print("await roll_die(): ", await roll_die())
print("await double(4): ", await double(4))
ring_and_open.call_deferred()
print("await bell_rang: ", await bell_rang)
print("await coins_found: ", await coins_found)
print("await chest_opened: ", await chest_opened)
get_tree().quit()
func roll_die() -> int:
# A coroutine: it waits for the next frame, then returns.
await get_tree().process_frame
return 6
func double(n: int) -> int:
# No await inside, so not a coroutine: await gives its result straight away.
return n * 2
func ring_and_open() -> void:
bell_rang.emit()
coins_found.emit(5)
chest_opened.emit(5, "Asha") extends Node
# The result of a coroutine used without await.
func _ready() -> void:
var rolled: int = roll_die()
print(rolled)
func roll_die() -> int:
await get_tree().process_frame
return 6 from session import files, run
files("project.godot", "results.tscn", "results.gd", "no_await.gd")
# The second command checks a script that Godot refuses, on purpose.
run("""
godot --headless --no-header --fixed-fps 60 results.tscn
!godot --headless --no-header --check-only --script no_await.gd
""") Output
$ godot --headless --no-header --fixed-fps 60 results.tscn
await roll_die(): 6
await double(4): 8
await bell_rang: <null>
await coins_found: 5
await chest_opened: [5, "Asha"]
$ godot --headless --no-header --check-only --script no_await.gd
SCRIPT ERROR: Parse Error: Function "roll_die()" is a coroutine, so it must be called with "await".
at: GDScript::reload (res://no_await.gd:7)
ERROR: Failed to load script "res://no_await.gd" with error "Parse error".
at: load (modules/gdscript/gdscript_resource_format.cpp:46)
Recorded with Python 3.14.8 on macOS 26 arm64. To run it yourself: mise exec python@3.14.8 -- python3 results_session.py
| You await | It gives | In the run |
|---|---|---|
a coroutine, await roll_die() |
its return value, when it returns | 6, a frame later |
a function without await in it, await double(4) |
its return value, at once | 8 |
| a signal with no arguments | null |
<null> |
| a signal with one argument | that value | 5 |
| a signal with two or more | an Array of them | [5, "Asha"] |
The three signals were emitted one after the other in a single call, and each await still caught its own: emitting
a signal wakes a waiting function at once, inside the emit, so results.gd was already waiting for the next signal
when it came. Using a coroutine’s result without await is refused before the game runs:
Function "roll_die()" is a coroutine, so it must be called with "await". Calling a coroutine as a statement, as
_ready() did with countdown(3), is allowed, and starts it without waiting.
Checkpoint
Check: What await gives back
Timers, frames and time scale decide when a wait ends
Most waits in a game are for time. get_tree().create_timer(seconds) makes a
SceneTreeTimer: The one-shot timer that get_tree().create_timer(seconds) makes, awaited by its timeout signal. It needs no node, keeps counting while the game is paused unless created with false as the second argument, and cannot be stopped., a one-shot timer you await by its timeout signal; a Timer node
lives in the scene, can repeat and can be stopped. timers.gd measures both in frames, paused.gd pauses the game
for 30 frames while four waits run, and frame_order.gd sets up four kinds of wait in one frame to see where each goes
on:
extends Node
# Three ways to wait, measured in frames at --fixed-fps 60.
var frame := 0
var half_timeouts := 0
var tiny_timeouts := 0
func _enter_tree() -> void:
get_tree().process_frame.connect(func() -> void: frame += 1)
func _ready() -> void:
await get_tree().process_frame
var start := frame
print("frame ", frame, ": create_timer(0.25)")
await get_tree().create_timer(0.25).timeout
print("frame ", frame, ": it ran out, ", frame - start, " frames later")
Engine.time_scale = 2.0
start = frame
await get_tree().create_timer(0.25).timeout
print("with Engine.time_scale 2.0, the same timer took ", frame - start, " frames")
Engine.time_scale = 1.0
$Half.timeout.connect(func() -> void: half_timeouts += 1)
$Tiny.timeout.connect(func() -> void: tiny_timeouts += 1)
$Half.start()
$Tiny.start()
for i in 100:
await get_tree().process_frame
print("in the next 100 frames, Half (0.5 s) ran out ", half_timeouts, " times and Tiny (0.01 s) ",
tiny_timeouts, " times")
get_tree().quit() extends Node
# The game is paused for frames 1 to 30. Which waits of a quarter of a second keep counting?
var frame := 0
func _enter_tree() -> void:
get_tree().process_frame.connect(func() -> void: frame += 1)
func _ready() -> void:
process_mode = Node.PROCESS_MODE_ALWAYS
get_tree().paused = true
print("frame 0: the game is paused, and four waits of 0.25 s start")
get_tree().create_timer(0.25).timeout.connect(_report.bind("create_timer(0.25)"))
get_tree().create_timer(0.25, false).timeout.connect(_report.bind("create_timer(0.25, false)"))
$Pausable.timeout.connect(_report.bind("the Timer node"))
$Pausable.start()
_count_frames(15)
func _process(_delta: float) -> void:
if frame == 30:
print("frame 30: the game goes on")
get_tree().paused = false
elif frame == 60:
get_tree().quit()
func _count_frames(count: int) -> void:
for i in count:
await get_tree().process_frame
_report("15 frames of await process_frame")
func _report(which: String) -> void:
print(" ", which, " ran out in frame ", frame) extends Node
# In frame 2, four kinds of wait are set up at once. The prints show where each one goes on.
var frame := 0
func _enter_tree() -> void:
get_tree().process_frame.connect(func() -> void: frame += 1)
func _process(_delta: float) -> void:
if frame == 2:
print("frame 2, _process: sets up four waits")
_deferred.call_deferred()
get_tree().create_timer(0.001).timeout.connect(func() -> void: print(" a 0.001 s timer ran out, frame ", frame))
_after_physics()
_after_process_frame()
print("frame 2, _process ends")
elif frame == 3:
print("frame 3, _process")
get_tree().quit()
func _deferred() -> void:
print(" the deferred call runs, frame ", frame)
func _after_physics() -> void:
await get_tree().physics_frame
print(" after await physics_frame, the next physics tick: before frame 3's process_frame")
func _after_process_frame() -> void:
await get_tree().process_frame
print(" after await process_frame, frame ", frame) from session import files, run
files("project.godot", "timers.tscn", "timers.gd", "paused.tscn", "paused.gd", "frame_order.tscn", "frame_order.gd")
run("""
godot --headless --no-header --fixed-fps 60 timers.tscn
godot --headless --no-header --fixed-fps 60 paused.tscn
godot --headless --no-header --fixed-fps 60 frame_order.tscn
""") Output
$ godot --headless --no-header --fixed-fps 60 timers.tscn frame 1: create_timer(0.25) frame 16: it ran out, 15 frames later with Engine.time_scale 2.0, the same timer took 8 frames in the next 100 frames, Half (0.5 s) ran out 3 times and Tiny (0.01 s) 99 times $ godot --headless --no-header --fixed-fps 60 paused.tscn frame 0: the game is paused, and four waits of 0.25 s start 15 frames of await process_frame ran out in frame 15 create_timer(0.25) ran out in frame 16 frame 30: the game goes on the Timer node ran out in frame 45 create_timer(0.25, false) ran out in frame 45 $ godot --headless --no-header --fixed-fps 60 frame_order.tscn frame 2, _process: sets up four waits frame 2, _process ends the deferred call runs, frame 2 a 0.001 s timer ran out, frame 2 after await physics_frame, the next physics tick: before frame 3's process_frame after await process_frame, frame 3 frame 3, _process
Recorded with Python 3.14.8 on macOS 26 arm64. To run it yourself: mise exec python@3.14.8 -- python3 timers_session.py
A quarter of a second took 15 frames at 60 frames a second. With Engine.time_scale at 2.0 it took 8, because the
time scale: Engine.time_scale, how fast the game's clock runs compared with real time. At 2.0, timers and delta run twice as fast; audio playback does not change. The docs advise keeping it above 0.0. speeds up the game’s clock, and with it every timer and every delta; the Engine
class reference adds that audio is not affected. A repeating Timer of 0.01 s ran out 99 times in 100 frames, never
more than once a frame, where a perfect clock would count about 167: the Timer docs warn that a timer processes at most
once per frame and advise your own code for waits under about 0.05 s.
The pause changed more. The default create_timer(0.25) kept counting and ran out in frame 16, inside the pause, and
a loop of 15 await get_tree().process_frame ended in frame 15, because process_frame is emitted while the game is
paused. create_timer(0.25, false) and the pausable Timer node waited, and ran out in frame 45, 15 frames after the
pause ended. The last run shows where each kind of wait goes on inside the frames:
The drawing shows the whole scene; its 6 steps are listed below.
Every step as text
- Frame 2 sets up four waits. In frame 2, _process() calls _deferred.call_deferred(), makes a 0.001 s timer, and starts two coroutines, one waiting for physics_frame and one for process_frame. Then _process() ends.
- The deferred call. With every node processed, Godot runs the deferred calls. The run printed "the deferred call runs, frame 2" right after "_process ends".
- The timer. Then timers are updated. The 0.001 s timer is shorter than a frame, so it ran out at once, still in frame 2. No timer can run out between frames.
- The next physics tick. The next frame starts with its physics tick, before process_frame. The coroutine waiting for physics_frame went on there, before frame 3 began.
- process_frame, frame 3. Godot emits process_frame, and the other coroutine goes on, in frame 3, before any _process() of that frame.
- Then _process(). Only then does frame 3's _process() run. Code after an await of process_frame always runs before the frame's _process() calls.
Choose the way of waiting by what it must do. Answer the questions under the table:
Answer the questions above: the table then marks what fits your case and says why the rest does not.
| Way to wait | While the game is paused | Stopped early | Repeating | Engine.time_scale |
|---|---|---|---|---|
| create_timer() Written as: await get_tree().create_timer(0.25).timeout | Poor keeps counting: ran out in frame 16, inside the pause | Poor has no stop(); its timeout still comes | Mixed one wait only: make a new one each time | Good follows it: 8 frames at 2.0 instead of 15 |
| create_timer(…, false) Written as: await get_tree().create_timer(0.25, false).timeout | Good waits: ran out in frame 45, 15 frames after the pause | Poor has no stop(); its timeout still comes | Mixed one wait only: make a new one each time | Good follows it, as without false |
| Timer node Written as: $Cooldown.start(), then its timeout signal | Good stops with its node: ran out in frame 45 | Good stop() ends it, without a timeout | Good repeats unless one_shot; at most once a frame | Good follows it, unless ignore_time_scale is on |
| Counting frames Written as: for i in 15: await get_tree().process_frame | Poor process_frame comes while paused: done in frame 15 | Mixed only with a flag the loop checks | Mixed a loop you write yourself | Poor ignores it, and 15 frames last twice as long at 30 frames a second |
How the answers decide
- “Must the wait stop while the game is paused?” Yes, it is part of gameplay: rules out create_timer() and Counting frames. The default create_timer() and a loop of process_frame both kept going while the game was paused.
- “Must the wait stop while the game is paused?” No, it runs in a pause menu or a loading screen: rules out create_timer(…, false) and adds a caveat to Timer node. A Timer node stops with a paused tree unless its process_mode is set to always; create_timer(…, false) always stops.
- “Can the wait be called off before it ends, such as a cooldown reset?” Yes, it may be stopped early: rules out create_timer() and create_timer(…, false) and adds a caveat to Counting frames. An awaited SceneTreeTimer has no stop(), so the code after the await runs anyway; a Timer node has stop().
- “Does it repeat, such as a spawn every 2 seconds?” Yes, again and again: adds a caveat to create_timer(), create_timer(…, false) and Counting frames. A Timer node repeats by itself; the others need a loop around the await.
New in Godot 4.7
Tween.tween_await(signal) makes a tween wait until a signal is emitted before its next step, which suits animated
cutscenes. The Tween class reference suggests AwaitTweener.set_timeout() when the signal may never come.
Checkpoint
Check: Timers, frames, pause and time scale
The world can change while a function waits
Between an await and the line after it, a whole game goes on. cave.gd starts four functions that wait, then
changes the world under them: after a tenth of a second it frees the player that three bats aim at, and frees one of
the bats. Its cutscene emits gate_opened and only then waits for it:
extends Node
# A bat that waits half a second, then swoops at its target. The second version looks before it swoops.
func swoop_at(target: Node2D) -> void:
print(" ", name, ": waits 0.5 s, then swoops at ", target.name)
await get_tree().create_timer(0.5).timeout
print(" ", name, ": swoops at ", target.name)
func swoop_carefully_at(target: Node2D) -> void:
print(" ", name, ": waits 0.5 s, then swoops at ", target.name)
await get_tree().create_timer(0.5).timeout
if not is_instance_valid(target):
print(" ", name, ": its target is gone, so it flies off")
return
print(" ", name, ": swoops at ", target.name) extends Node
# The world changes while four functions wait: a target and a bat are freed, and a signal comes too early.
signal gate_opened
var frame := 0
func _enter_tree() -> void:
get_tree().process_frame.connect(func() -> void: frame += 1)
func _ready() -> void:
var player := Node2D.new()
player.name = "Player"
add_child(player)
$Bat1.swoop_at(player)
$Bat2.swoop_carefully_at(player)
$Bat3.swoop_at(player)
open_gate_and_wait()
await get_tree().create_timer(0.1).timeout
print("frame ", frame, ": the player and Bat3 are freed")
player.free()
$Bat3.free()
await get_tree().create_timer(1.0).timeout
print("frame ", frame, ": the cutscene is still waiting for the gate; the run ends")
get_tree().quit()
func open_gate_and_wait() -> void:
print(" cutscene: opens the gate, then waits for gate_opened")
gate_opened.emit()
await gate_opened
print(" cutscene: the gate is open") from session import files, run
files("project.godot", "cave.tscn", "cave.gd", "bat.gd")
# Bat1 uses a freed target after its wait, so the run reports a script error on purpose.
run("""
!godot --headless --no-header --fixed-fps 60 cave.tscn
""") Output
$ godot --headless --no-header --fixed-fps 60 cave.tscn
Bat1: waits 0.5 s, then swoops at Player
Bat2: waits 0.5 s, then swoops at Player
Bat3: waits 0.5 s, then swoops at Player
cutscene: opens the gate, then waits for gate_opened
frame 7: the player and Bat3 are freed
SCRIPT ERROR: Invalid access to property or key 'name' on a base object of type 'previously freed'.
at: swoop_at (res://bat.gd:9)
GDScript backtrace (most recent call first):
[0] swoop_at (res://bat.gd:9)
Bat2: its target is gone, so it flies off
frame 67: the cutscene is still waiting for the gate; the run ends
Recorded with Python 3.14.8 on macOS 26 arm64. To run it yourself: mise exec python@3.14.8 -- python3 cave_session.py
Each bat met its own outcome. Bat1 woke up after half a second and read target.name from a target that was gone:
previously freed, and its function stopped. Bat2 checked with is_instance_valid() first and flew off. Bat3 printed
nothing more, because it had been freed itself: a freed object’s coroutines never go on, and Godot drops them without a
word. The cutscene waited until the run ended, because an await hears only emissions that come after it starts.
Nothing reported that either.
| What happened during the wait | What the waiting function did | What to write |
|---|---|---|
| an object it uses was freed | went on, and failed on the freed object | is_instance_valid() after the await |
| its own object was freed | never went on, silently | nothing, if that is what you want |
| the signal came before the await | waited for good, silently | check the state before you wait |
await on the timer instead of its timeout
await get_tree().create_timer(0.5) awaits the timer object, which is not a signal, so it gives the timer back at
once and waits for nothing. The running game prints nothing; only the script editor’s list of warnings says the
await is unnecessary. Write await get_tree().create_timer(0.5).timeout. The practice below catches this slip by
the frames its steps start on.
Checkpoint
Check: Await mistakes in a changing world
Practise: a cutscene that waits its turn
cutscene.gd must play a list of steps in order, each starting when the one before it has finished. A step is a
Dictionary with one key: {"wait": 0.5} waits half a second, {"signal": door_opened} waits for that signal, and
{"call": callable} calls it. Each step appends the frame it starts on to started, and finished is emitted after
the last.
- Next to an empty
project.godot, save the startercutscene.gdandcheck_cutscene.gdbelow. - Write
play(); the starter already counts frames, as the examples above do. - Start the checker in a terminal in that folder:
godot --headless --no-header --fixed-fps 60 --script check_cutscene.gd.
extends Node
# Plays a cutscene: a list of steps, each one started when the step before it has finished.
# A step is a Dictionary with one key: {"wait": 0.5} waits that many seconds, {"signal": some_signal} waits until the
# signal is emitted, and {"call": some_callable} calls it. Each step appends the frame it starts on to started, and
# finished is emitted once the last step is done.
# Your task: write play().
signal finished
var frame := 0
var started: Array[int] = []
func _enter_tree() -> void:
get_tree().process_frame.connect(func() -> void: frame += 1)
func play(steps: Array[Dictionary]) -> void:
pass # Replace this line with your code. extends SceneTree
# Checks cutscene.gd. Run it in the project's folder:
# godot --headless --no-header --fixed-fps 60 --script check_cutscene.gd
# It plays six steps from frame 1 of the cutscene; the door opens in its frame 70. Each step must start on its frame.
signal door_opened
const WANT: Array[int] = [1, 1, 31, 31, 70, 85]
var cutscene: Node
var finished_at := -1
var said: Array[String] = []
var passed := 0
var total := 0
func _initialize() -> void:
cutscene = load("res://cutscene.gd").new()
cutscene.tree_entered.connect(_on_cutscene_entered)
root.add_child.call_deferred(cutscene)
func _on_cutscene_entered() -> void:
# Connected after the cutscene's own frame counter, so cutscene.frame is already this frame's number here.
process_frame.connect(_on_frame)
cutscene.finished.connect(func() -> void: finished_at = cutscene.frame)
func _on_frame() -> void:
if cutscene.frame == 1:
var steps: Array[Dictionary] = [
{"call": say.bind("Asha: Is anyone here?")},
{"wait": 0.5},
{"call": say.bind("The door creaks.")},
{"signal": door_opened},
{"wait": 0.25},
{"call": say.bind("Ravi: Come in.")},
]
cutscene.play(steps)
elif cutscene.frame == 70:
print("frame 70: the door opens")
door_opened.emit()
elif cutscene.frame == 100:
check(cutscene.started == WANT, "the steps start in frames %s (want %s)" % [cutscene.started, WANT])
check(said.size() == 3, "all three lines were said")
check(finished_at == 85, "finished is emitted in frame 85 (it was %d)" % finished_at)
print("%d of %d checks passed" % [passed, total])
quit(0 if passed == total else 1)
func say(line: String) -> void:
said.append(line)
print("frame %d: %s" % [cutscene.frame, line])
func check(ok: bool, what: String) -> void:
total += 1
passed += int(ok)
print("%s %s" % ["PASS" if ok else "FAIL", what]) The checker plays six steps from frame 1 and opens the door in frame 70, so the steps must start in frames 1, 1, 31,
31, 70 and 85. Its output for a finished play() and for one that awaits the timer object itself:
from session import files, run
# The checker on a cutscene.gd that solves the task, then on one that awaits the timer instead of its timeout.
files("practice/project.godot=right/project.godot", "practice/check_cutscene.gd=right/check_cutscene.gd",
"practice/solution/cutscene.gd=right/cutscene.gd",
"practice/project.godot=tempting/project.godot", "practice/check_cutscene.gd=tempting/check_cutscene.gd",
"practice/tempting/cutscene.gd=tempting/cutscene.gd")
run("""
cd right
godot --headless --no-header --fixed-fps 60 --script check_cutscene.gd
cd ../tempting
!godot --headless --no-header --fixed-fps 60 --script check_cutscene.gd
""") Output
$ cd right $ godot --headless --no-header --fixed-fps 60 --script check_cutscene.gd frame 1: Asha: Is anyone here? frame 31: The door creaks. frame 70: the door opens frame 85: Ravi: Come in. PASS the steps start in frames [1, 1, 31, 31, 70, 85] (want [1, 1, 31, 31, 70, 85]) PASS all three lines were said PASS finished is emitted in frame 85 (it was 85) 3 of 3 checks passed $ cd ../tempting $ godot --headless --no-header --fixed-fps 60 --script check_cutscene.gd frame 1: Asha: Is anyone here? frame 1: The door creaks. frame 70: the door opens frame 70: Ravi: Come in. FAIL the steps start in frames [1, 1, 1, 1, 70, 70] (want [1, 1, 31, 31, 70, 85]) PASS all three lines were said FAIL finished is emitted in frame 85 (it was 70) 1 of 3 checks passed [exit status 1]
Recorded with Python 3.14.8 on macOS 26 arm64. To run it yourself: mise exec python@3.14.8 -- python3 check_session.py
The tempting play() started the line after each wait in the same frame, 1 and 70, because each of its waits ended at
once. A finished one passes 3 of 3. With that done, practise the four ideas of the page below: the weakest comes
first, and each answer explains itself the moment you choose it:
Practice that adapts to you
Practise this lesson
12 questions about 4 ideas of this lesson. Each answer is explained at once, and what you get right is kept in this browser.
Interview questions
Warm-up: what does await do in GDScript?
await pauses the function it is written in until a signal is emitted or a coroutine returns, then goes on from the
same line. A function that contains await becomes a coroutine: at the first await it hands control back to its
caller, which goes on at once unless it awaits the call too. On Godot 4.7.2, a countdown that waited for
process_frame between numbers printed its first number before the first frame and the rest one frame apart, while
its caller had moved on. await gives back the coroutine’s return value, or the signal’s argument, null for no
arguments or an Array for several. Typical uses are await get_tree().create_timer(0.5).timeout for a delay and
await animation_player.animation_finished in a cutscene. Using a coroutine’s result without await is a parse
error.
Model answers open with any pass: ₹79 for 1 week ($2.99), one payment that never renews.
-
Fresher to mid
A cutscene calls gate.open() and then await gate.opened before the next line of dialogue. On most playthroughs it works; when the player has opened the gate earlier, the cutscene freezes for good, and nothing appears in the output. Find the cause and fix it.
-
Fresher to mid
A speed power-up lasts 10 seconds: activate(), then await get_tree().create_timer(10.0).timeout, then deactivate(). Players report that pausing the game eats into the power-up, and that it sometimes runs out while the pause menu is open. Explain and fix it, and say what else in the game probably has the same bug.
Key takeaways
awaitpauses only the function it is in, which becomes a coroutine; its caller goes on at once unless it awaits too, and the coroutine goes on from the same line when its signal is emitted.awaitgives a coroutine’s return value, a function’s result at once,nullfor a signal without arguments, the value of one argument, or an Array of several, and a coroutine’s result needsawait.create_timer(0.25)took 15 frames at 60 frames a second and 8 at time scale 2.0, kept counting through a pause unless made withfalse, and no timer runs out more than once a frame.- During a wait, objects can be freed and signals can come early: check references with
is_instance_valid()after anawait, check the state before waiting, and await a timer’stimeout, not the timer.
Terms in this lesson
- coroutine
- A function that contains await. At each await it pauses and hands control back to its caller, and it goes on from the same line when the signal it waits for is emitted; await on the call waits for its return value.
- deferred call
- A call made with call_deferred() or a CONNECT_DEFERRED connection. It runs at idle time, after the frame's _process() calls and before its timers and queued deletions, instead of at once.
- SceneTreeTimer
- The one-shot timer that get_tree().create_timer(seconds) makes, awaited by its timeout signal. It needs no node, keeps counting while the game is paused unless created with false as the second argument, and cannot be stopped.
- time scale
- Engine.time_scale, how fast the game's clock runs compared with real time. At 2.0, timers and delta run twice as fast; audio playback does not change. The docs advise keeping it above 0.0.
References
- GDScript reference: awaiting signals or coroutines (Godot Engine)
- SceneTree class reference: create_timer, process_frame and physics_frame (Godot Engine)
- SceneTreeTimer class reference (Godot Engine)
- Timer class reference (Godot Engine)
- Engine class reference: time_scale (Godot Engine)
- Object class reference: call_deferred (Godot Engine)
- GDScript warning system (Godot Engine)
- Tween class reference: tween_await (Godot Engine)
- Godot 4.7 release page (tweens that wait for signals) (Godot Engine)
Report a problem with this lesson
Kept only in this browser. Your Learn progress