Some Commands and Presenters take time: a Command that walks a character across the room, or a Presenter that types a line out. Write them so they still behave correctly when the player hurries the dialogue along, when the game is paused, and when the dialogue is stopped.
How the dialogue waits
The dialogue waits for two kinds of things:
- Commands: if a Command returns a
Signal, or usesawait, the dialogue waits until it’s finished. See Commands. - Presenters: the dialogue waits until every Presenter has returned from
run_line()orrun_options(). See Building Your Own Presenter.
The player can ask a line to hurry up or move on, but not a Command.
Hurry up and next content
Every line and every set of options gets a new YarnCancellationToken, which is passed to each Presenter’s run_line() or run_options(). The token tells the Presenters when the line has been asked to finish early. There are two kinds of request:
| Request | What it means | What a Presenter should do |
|---|---|---|
| Hurry up | Finish showing the line now. | Skip to the end of any typing or animation, but keep the line on screen. |
| Next content | Move on. | Tidy up and return as soon as possible. |
When the player presses the continue button, the Line Presenter asks for hurry up if the line is still typing, and next content if it’s finished. The first press shows the whole line, and the second moves on:
sequenceDiagram participant Player participant Runner as Dialogue Runner participant Presenter as Your Presenter Runner->>Presenter: run_line(line, token) Note over Presenter: Typing the line Player->>Runner: Continue Runner->>Presenter: token: hurry up Note over Presenter: Shows the whole line Player->>Runner: Continue Runner->>Presenter: token: next content Presenter-->>Runner: returns from run_line() Note over Runner: The dialogue moves on
You can ask for them from your own code too, with dialogue_runner.request_hurry_up() and dialogue_runner.request_next_content(). stop_dialogue() asks for next content as well.
A request for next content also counts as hurry up, so is_hurry_up_requested is true after either. Check is_next_content_requested when you need to tell them apart.
The token has these properties, methods and signals:
| Part of the token | What it does |
|---|---|
is_hurry_up_requested | true once hurry up or next content has been requested. |
is_next_content_requested | true once next content has been requested. |
should_hurry() | true if hurry up has been requested, but next content hasn’t. |
await wait_for_next_content() | Waits until next content is requested. Returns immediately if it already has been. |
await wait_for_cancellation(timeout) | Waits until either request, and returns which one: YarnCancellationToken.CancellationMode.HURRY_UP or NEXT_CONTENT. With a timeout in seconds, it returns NONE if nothing is requested in time. The timeout keeps counting while the game is paused. |
hurry_up_requested | A signal, emitted when hurry up is requested. |
next_content_requested | A signal, emitted when next content is requested. |
cancellation_requested(mode) | A signal, emitted once for each request, with which one it was. |
An example
This Presenter fades in a character’s portrait for each line. If the player hurries up, the portrait appears in full at once. It stays up until the dialogue moves on:
extends YarnDialoguePresenter
@export var portrait: CanvasItem
func run_line(
line: YarnLine,
token: YarnCancellationToken = null
) -> void:
portrait.modulate.a = 0.0
var tween := create_tween()
tween.tween_property(portrait, "modulate:a", 1.0, 1.0)
# Fade in, unless the player asks to hurry up.
while portrait.modulate.a < 1.0:
if token.is_hurry_up_requested:
break
await get_tree().process_frame
tween.kill()
portrait.modulate.a = 1.0
# Keep the portrait up until the dialogue moves on.
await token.wait_for_next_content()
portrait.modulate.a = 0.0Return promptly
Once next content has been requested, the Dialogue Runner can’t move on until every Presenter has returned. If a Presenter hasn’t returned five seconds later, the Dialogue Runner logs a warning that names it:
dialogue runner: line presenter PortraitPresenter
(res://PortraitPresenter.gd) has not finished 5 seconds
after next content was requested. Once the cancellation
token fires, run_line/run_options must wrap up and
return — dialogue is waiting on it.This usually means a Presenter is waiting for something that will never happen, like a signal that’s already been emitted.
Waiting in Commands
Commands don’t get a token. While a Command is running, pressing continue, request_hurry_up() and request_next_content() do nothing, and the dialogue waits until the Command has finished. Keep a Command’s waits short, or show the player that something is happening.
To wait for a number of seconds in a Command, use YarnAsync.wait():
func _ready():
dialogue_runner.add_command("pause_for_effect", _pause_for_effect)
func _pause_for_effect() -> void:
await YarnAsync.wait(self, 2.0)YarnAsync.wait(node, seconds) counts time only while node can process. When the game is paused, and node pauses with it, the wait stops counting until the game is unpaused. If node leaves the scene tree, the wait ends.
Don’t use await get_tree().create_timer(seconds).timeout. A SceneTree timer keeps counting while the game is paused, so the dialogue can carry on under a pause menu. The built-in <<wait>> Command uses YarnAsync.wait(), so it pauses with the game.
Tweens made with a node’s create_tween() pause with that node, so a Command that returns a tween’s finished signal pauses properly too.
Pausing the game
When you pause the game with get_tree().paused = true, the Line Presenter stops typing, stops auto-advancing, and stops taking input. YarnAsync.wait() stops counting. Your pause menu needs its Process Mode set to Always, so it keeps working while everything else is paused.
In your own Presenters, wait with YarnAsync.wait(), or with a tween, rather than a SceneTree timer, so they pause too.
Stopping the dialogue
stop_dialogue() asks the current line or options for next content, and ends the dialogue. The Dialogue Runner emits dialogue_cancelled, then dialogue_completed. To wait until it’s finished, await it:
await dialogue_runner.stop_dialogue()If a Command is waiting when the dialogue stops, the dialogue ends without waiting for it. The Command’s own code carries on to the end, but the dialogue doesn’t continue after it. If the rest of a Command shouldn’t happen once the dialogue has stopped, check dialogue_runner.is_running() after each await:
func _walk_home() -> void:
await walk_to(door.position)
if not dialogue_runner.is_running():
return
await walk_to(home.position)Waiting for something that might already be done
If you await a signal after it has been emitted, you wait until the next time it’s emitted, which might be never. When something might finish before you get round to waiting for it, use a YarnPromise instead. A promise records that it has finished, and the value it finished with.
This Presenter starts loading a portrait when the dialogue starts, and waits for it in run_line(). If the portrait has already loaded by then, there’s no wait:
extends YarnDialoguePresenter
@export var portrait: TextureRect
var _portrait_loading: YarnPromise
func on_dialogue_started() -> void:
_portrait_loading = _load("res://portraits/rosa.png")
func run_line(
line: YarnLine,
token: YarnCancellationToken = null
) -> void:
portrait.texture = await _portrait_loading.wait()
await token.wait_for_next_content()
func _load(path: String) -> YarnPromise:
var promise := YarnPromise.new()
_load_into(path, promise)
return promise
func _load_into(path: String, promise: YarnPromise) -> void:
ResourceLoader.load_threaded_request(path)
while ResourceLoader.load_threaded_get_status(path) \
== ResourceLoader.THREAD_LOAD_IN_PROGRESS:
await get_tree().process_frame
promise.settle(ResourceLoader.load_threaded_get(path))wait() returns the texture, waiting only if it hasn’t loaded yet._load_into() isn’t awaited, so it carries on in the background.A YarnPromise has:
settle(value), which finishes the promise with a value. Settling it again does nothing.await wait(), which returns the value, waiting only if the promise hasn’t been settled yet.is_settledandvalue, to check it without waiting.completed(value), a signal emitted when it’s settled.
The demo scene
This scene shows the token for each line, and a Command that pauses with the game. The panel in the top left logs each new token and each request. In the video, the player presses continue once while the first line is typing, which hurries it up, and again to move on. Then Rosa counts down to lighting the lamp. The panel in the top right counts down twice, once with YarnAsync.wait() and once with a SceneTree timer. The player pauses the game part way through: the SceneTree timer carries on, and YarnAsync.wait() doesn’t.
The Line Presenter’s Characters per Second is set to 15, so the first line types slowly enough to hurry up.
title: AsyncDemo
---
Rosa: This line types out slowly. Press once to hurry it up, then again to move on.
Rosa: I'll light the lamp in five seconds. Press P to pause the game while I count.
<<light_lamp_in 5>>
Rosa: There we go. My count stopped while the game was paused.
===extends Node2D
const LAMP_ON := Color(1.0, 0.85, 0.35)
@export var dialogue_runner: YarnDialogueRunner
@export var token_label: RichTextLabel
@export var timers_label: Label
@export var lamp: ColorRect
@export var glow: ColorRect
var _yarn_wait_left := 0
var _scene_timer_left := 0
var _token: YarnCancellationToken
var _line_number := 0
var _log := PackedStringArray()
func _ready():
dialogue_runner.add_command(
"light_lamp_in", _light_lamp_in)
dialogue_runner.start_dialogue("AsyncDemo")
_show_timers()
func _process(_delta: float) -> void:
# Each line gets a new token. Listen to each one.
if not dialogue_runner.is_presenting_line():
return
var token := dialogue_runner.get_cancellation_token()
if token == _token:
return
_token = token
_line_number += 1
_add_to_log("Line %d started: new token" % _line_number)
token.cancellation_requested.connect(_on_request)
func _on_request(mode) -> void:
match mode:
YarnCancellationToken.CancellationMode.HURRY_UP:
_add_to_log("Line %d: hurry up" % _line_number)
YarnCancellationToken.CancellationMode.NEXT_CONTENT:
_add_to_log("Line %d: next content" % _line_number)
# <<light_lamp_in 5>>
func _light_lamp_in(seconds: int) -> void:
_add_to_log("<<light_lamp_in>> started: no token")
# Count down with a SceneTree timer too, to compare.
# It isn't awaited, so it runs alongside.
_count_with_scene_timer(seconds)
for i in range(seconds, 0, -1):
_yarn_wait_left = i
_show_timers()
await YarnAsync.wait(self, 1.0)
_yarn_wait_left = 0
_show_timers()
var tween := create_tween().set_parallel()
tween.tween_property(lamp, "color", LAMP_ON, 0.8)
tween.tween_property(glow, "modulate:a", 1.0, 0.8)
await tween.finished
func _count_with_scene_timer(seconds: int) -> void:
for i in range(seconds, 0, -1):
_scene_timer_left = i
_show_timers()
await get_tree().create_timer(1.0).timeout
_scene_timer_left = 0
_show_timers()
func _show_timers() -> void:
var text := "YarnAsync.wait(): %d\n" % _yarn_wait_left
text += "create_timer(): %d\n" % _scene_timer_left
timers_label.text = text + "Press P to pause"
func _add_to_log(text: String) -> void:
_log.append(text)
token_label.text = "\n".join(_log.slice(-6))get_cancellation_token() returns the current line’s token.cancellation_requested is emitted once for each, with which one it was.SceneTree timer countdown, without waiting for it.YarnAsync.wait(). While the game is paused, this node can’t process, so the wait stops counting.create_timer(). It keeps counting while the game is paused.extends CanvasLayer
## Pauses and unpauses the game with the P key. Its
## Process Mode is Always, so it still gets input when paused.
@export var paused_label: Control
func _unhandled_input(event: InputEvent) -> void:
var key := event as InputEventKey
if key and key.pressed and key.keycode == KEY_P:
var tree := get_tree()
tree.paused = not tree.paused
paused_label.visible = tree.paused