Pardon our dust while we move our docs here. The Godot (GDScript) docs are on this site, and the Yarn language, Unity, Godot (C#) and Unreal docs are still at docs.yarnspinner.dev.

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 uses await, the dialogue waits until it’s finished. See Commands.
  • Presenters: the dialogue waits until every Presenter has returned from run_line() or run_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:

RequestWhat it meansWhat a Presenter should do
Hurry upFinish showing the line now.Skip to the end of any typing or animation, but keep the line on screen.
Next contentMove on.Tidy up and return as soon as possible.
The two requests a token can carry.

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
Two presses of the continue button while a line is typing.

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 tokenWhat it does
is_hurry_up_requestedtrue once hurry up or next content has been requested.
is_next_content_requestedtrue 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_requestedA signal, emitted when hurry up is requested.
next_content_requestedA signal, emitted when next content is requested.
cancellation_requested(mode)A signal, emitted once for each request, with which one it was.
What you can use on a YarnCancellationToken.

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.0
Lines 10–12 Start the fade from invisible, over one second.
Lines 15–18 Wait a frame at a time until the fade is done, or until hurry up is requested.
Lines 19–20 Either way, stop the fade and show the portrait fully.
Line 23 Wait for next content. If it was requested already, this doesn’t wait.
Line 24 Hide the portrait, and return. The dialogue can move on.
A Presenter that fades in a portrait, and handles both requests.

Return 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.
The warning for a Presenter that doesn’t return, called PortraitPresenter here.

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)
A pause_for_effect Command that waits two seconds.

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()
Stopping the dialogue, and waiting until it has finished.

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)
A walk_home Command, added with add_command(), that stops part way if the dialogue is stopped.

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))
Lines 8–9 Start loading when the dialogue starts. The first line might not need the portrait for a while.
Line 16 wait() returns the texture, waiting only if it hasn’t loaded yet.
Lines 20–23 Make a promise, start loading into it, and return it before the load has finished. _load_into() isn’t awaited, so it carries on in the background.
Lines 26–31 Load the texture on another thread, and settle the promise with it when it’s done.
A Presenter that loads a portrait 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_settled and value, 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 demo scene running. The game is paused during the countdown, and only the create_timer() count keeps going.
GameNode2D
BackgroundColorRect
FloorColorRect
DoorColorRect
LampColorRect
GlowColorRect
RosaNode2D
BodyColorRect
NameLabel
YarnDialogueRunner
VariableStorageYarnInMemoryVariableStorage
UICanvasLayer
TokenPanelPanelContainer
TokenRichTextLabel
TimersPanelPanelContainer
TimersLabel
YarnLinePresenter
YarnOptionsPresenter
PauseLayerCanvasLayer
PausedPanelPanelContainer
PausedTextLabel
The demo scene. Game’s script has the Command and fills in both panels. PauseLayer pauses the game.

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.
===
Line 3 A long line that types slowly. The first press hurries it up, and the second moves on.
Line 5 The Command. The dialogue waits here until the lamp is lit, and pressing continue does nothing.
The Yarn Script.
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))
Lines 11–15 The two countdowns, the token being watched, and the log.
Lines 19–21 Add the Command, and start the dialogue. The Dialogue Runner’s Auto Start is off.
Lines 26–31 Only look at tokens while a line is on screen. get_cancellation_token() returns the current line’s token.
Lines 32–35 A new token means a new line. Log it, and listen for its requests.
Lines 38–43 Log each request. cancellation_requested is emitted once for each, with which one it was.
Lines 47–48 The Command. It doesn’t get a token, as the log shows.
Lines 50–52 Start the SceneTree timer countdown, without waiting for it.
Lines 54–57 Count down with YarnAsync.wait(). While the game is paused, this node can’t process, so the wait stops counting.
Lines 61–64 Light the lamp, and wait for the tween. The tween pauses with this node too.
Lines 67–71 The same countdown with create_timer(). It keeps counting while the game is paused.
Lines 82–84 Add a line to the log, and show the last six.
The script on the Game node.
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
Lines 2–3 PauseLayer’s Process Mode is set to Always in the Inspector.
Lines 8–13 Press P to pause or unpause the whole game, and show or hide the Paused panel.
The script on PauseLayer.
Next step Localisation and Assets Translating lines with Godot's translation system, and how voice clips and images are found for each line.