Pardon our dust! Docs for Yarn Spinner for Unity 3.2 and Yarn Spinner for Godot C# are on docs.yarnspinner.dev (the old docs site). Docs for Yarn Spinner for Unity 3.3 (pre-release, out ASAP), Yarn Spinner for Godot GDScript and Yarn Spinner for Unreal Engine (work in progress) are on /docs/ (the new docs site).

Building Your Own Presenter

The Line Presenter and Options Presenter cover most games. If you want dialogue to look or behave differently, like speech bubbles over characters’ heads, you can write your own Presenter.

A Presenter is a script that extends YarnDialoguePresenter. The Dialogue Runner gives it each line and each set of options, and it decides how to show them.

How the Dialogue Runner uses Presenters

For each line, the Dialogue Runner calls run_line on every Presenter in its Presenters list, all at the same time. It moves on to the next line once every one of them has returned.

For each set of options, it calls run_options on every Presenter. The first one to return an option’s index decides which option was picked. The Dialogue Runner then tells the others to stop, and they return -1.

sequenceDiagram
  participant R as Dialogue Runner
  participant L as Line Presenter
  participant O as Options Presenter
  R->>L: run_line(line)
  R->>O: run_line(line)
  O-->>R: returns immediately
  L-->>R: returns once the player continues
  Note over R: Both have returned,<br>so the options start
  R->>L: run_options(options)
  R->>O: run_options(options)
  L-->>R: returns -1, it doesn't show options
  O-->>R: returns 1, the option the player picked
  Note over R: The dialogue carries on<br>after option 1
A line, then a set of options, with the Line Presenter and Options Presenter. The Dialogue Runner waits for both Presenters each time.

A Presenter has two jobs:

  • in run_line, show the line, wait for as long as it needs to, then tidy up and return,
  • in run_options, show the options, wait for the player to pick one, then return its index.

A simple Presenter

This Presenter shows each line in a Label, and each set of options as buttons. The player presses ui_accept, which is Enter, Space or a gamepad’s bottom face button, to move on to the next line.

class_name SimplePresenter
extends YarnDialoguePresenter

signal _option_chosen(index: int)

@export var label: Label
@export var options_box: VBoxContainer


func _unhandled_input(event: InputEvent) -> void:
	if event.is_action_pressed("ui_accept"):
		dialogue_runner.request_next_content()


func run_line(
		line: YarnLine,
		token: YarnCancellationToken = null
) -> void:
	label.text = line.text
	await token.wait_for_next_content()
	label.text = ""


func run_options(
		options: Array[YarnOption],
		token: YarnCancellationToken = null
) -> int:
	if token.is_next_content_requested:
		return -1
	for i in options.size():
		if not options[i].is_available:
			continue
		var button := Button.new()
		button.text = options[i].text
		button.pressed.connect(_option_chosen.emit.bind(i))
		options_box.add_child(button)

	var give_up := _option_chosen.emit.bind(-1)
	token.next_content_requested.connect(give_up)
	var index: int = await _option_chosen
	token.next_content_requested.disconnect(give_up)

	for button in options_box.get_children():
		button.queue_free()
	return index
Lines 1–2 Extend YarnDialoguePresenter. The class_name gives the Presenter a type name. The steps below attach the script to a Control.
Line 4 Emitted when the player picks an option, with its index. run_options waits for it.
Lines 6–7 The Label for lines, and the container for option buttons. Set both in the Inspector.
Lines 10–12 When the player presses ui_accept, ask the Dialogue Runner to move on. dialogue_runner is set for you when the Presenter is in a Dialogue Runner’s Presenters list.
Lines 15–18 run_line is called for each line. token tells you when the line should finish.
Line 19 Show the line. line.text includes the speaker’s name, like Rosa: Oh, hello.
Line 20 Wait until something asks for the next line, here the ui_accept press on line 12.
Line 21 Clear the label. When run_line returns, this Presenter is done with the line.
Lines 24–27 run_options is called for each set of options, and returns the index of the one the player picked.
Lines 28–29 If the options were already dismissed before this Presenter got to them, return -1 straight away.
Lines 30–36 Make a button for each option the player can choose. Pressing a button emits _option_chosen with that option’s index in the array.
Lines 38–39 If the Dialogue Runner stops the options, for example because another Presenter’s option was picked first or time ran out, emit -1, meaning no choice.
Lines 40–41 Wait for a choice, then stop listening to the token.
Lines 43–45 Remove the buttons, and return the index.
A Presenter that shows lines in a Label and options as buttons.

To use it:

  1. Build the nodes it needs: a Control with a VBoxContainer inside it, holding a Label and another VBoxContainer for the options.
GameNode2D
YarnDialogueRunner
CanvasLayer
SimplePresenterControl
VBoxContainer
Label
OptionsBoxVBoxContainer
A scene with the Presenter above as its only Presenter.
  1. Attach the script to the Control, and set its Label and Options Box in the Inspector.
The Inspector for SimplePresenter, with Label set to the Label node and Options Box set to OptionsBox
The Presenter’s two exported properties, set to the nodes inside it.
  1. Add the Control to the Dialogue Runner’s Presenters list.
The Presenter above, running the Lighthouse conversation. Each line shows all at once, with the speaker's name in front, because this Presenter has no typewriter.

Showing lines

run_line gets the line as a YarnLine. The parts you’ll use most:

Property or functionWhat it is
line.textThe whole line, including the speaker’s name.
line.character_nameThe speaker’s name, or empty if the line has none.
line.text_without_character_nameThe line without the speaker’s name.
line.get_bbcode_text()The line without the speaker’s name, with its markup turned into BBCode, for a RichTextLabel.
line.line_idThe line’s ID, like line:rosa-hello.
line.metadataThe line’s hashtags.
What a Presenter can read from a line.

Follow these rules in run_line:

  • Wait for whatever takes time before you return: a typewriter, an animation, audio, or token.wait_for_next_content().
  • Return as soon as you can once the token asks for the next line. The Dialogue Runner can’t move on until you do.
  • If your Presenter has nothing to show for a line, return immediately.
  • Don’t call dialogue_runner.signal_content_complete(). The Dialogue Runner moves on by itself once every Presenter has returned.

If your Presenter decides when a line ends, like a voice-over Presenter whose clip has finished, call _request_line_end(line) instead. It asks the Dialogue Runner to move on, which fires the token for every Presenter, so they all finish together.

If a Presenter is still running five seconds after the token asked for the next line, the Dialogue Runner logs a warning naming it, because the dialogue is stuck waiting for it. Check that the Presenter returns once the token fires.

The token

Each call to run_line and run_options gets a YarnCancellationToken. It tells your Presenter what the player or your game has requested:

Part of the tokenWhat it means
token.is_hurry_up_requestedThe player has asked for the line to finish showing now. Skip to the end of any typing or animation, but keep the line on screen.
token.is_next_content_requestedThe dialogue is moving on. Tidy up and return.
await token.wait_for_next_content()Waits until the dialogue moves on, or returns immediately if it already has.
token.hurry_up_requestedA signal, emitted when a hurry is requested.
token.next_content_requestedA signal, emitted when the dialogue moves on.
What a Presenter can check on the token.

These happen when:

  • the Line Presenter or a Line Advancer gets the player’s input,
  • your own code calls dialogue_runner.request_hurry_up() or dialogue_runner.request_next_content(),
  • another Presenter’s option is picked, or the Dialogue Runner’s Option Timeout runs out.

Showing options

run_options gets an array of YarnOption. Each has text, text_without_character_name and is_available, which is false when the option’s condition failed.

Return the index in the array of the option the player picked. Return -1 if your Presenter doesn’t handle options, or when the token says the dialogue is moving on.

Other functions you can use

If your Presenter has any of these functions, the Dialogue Runner calls them:

FunctionWhen it’s called
on_dialogue_started()Dialogue has started. Set up your UI here.
on_dialogue_completed()Dialogue has ended. Hide or clear your UI here.
on_node_started(node_name)A node has started running.
on_node_completed(node_name)A node has finished running.
prepare_for_lines(line_ids)Lines are about to run, with their IDs. Load anything they’ll need here, such as audio or images.
Functions the Dialogue Runner calls on a Presenter.

Using the Dialogue Runner from your Presenter

A Presenter’s dialogue_runner variable holds the Dialogue Runner that’s using it. If your Presenter is in the Dialogue Runner’s Presenters list in the Inspector, dialogue_runner is set by the time your Presenter’s _ready() runs, wherever the Presenter is in the scene tree. You can add Commands and Functions there:

extends YarnDialoguePresenter


func _ready() -> void:
	dialogue_runner.add_command("flash_portrait", _flash_portrait)


func _flash_portrait() -> void:
	$Portrait.modulate = Color.WHITE
A Presenter that adds its own Command.

If you add your Presenter from code, dialogue_runner is null until you’ve called add_presenter(). Don’t use it in _ready() in that case. Use on_dialogue_started() instead.

If several Dialogue Runners are in the scene, each Presenter gets the one that lists it.

YarnDialoguePresenter provides two functions for showing and fading your Presenter:

  • _set_presenter_visible(visible) shows or hides the Presenter. If the Presenter is a Control or other CanvasItem, it shows or hides itself. Otherwise, it shows or hides the nodes directly under it.
  • await _fade_presenter_alpha(from, to, duration) fades the Presenter’s transparency from one value to another, from 0 to 1, over duration seconds.

Without writing a Presenter

If you prefer connecting signals to writing a script, add a YarnSignalPresenter to your scene and add it to the Presenters list. It emits a signal for each line and each set of options, and waits until you tell it to continue.

SignalWhen it’s emitted
line_received(line_data)A line has arrived. line_data is a Dictionary with text, character_name, line_id, metadata and bbcode_text.
options_received(options_data)Options have arrived. Each item has text, option_index, is_available, line_id and metadata.
presenter_dialogue_startedDialogue has started.
presenter_dialogue_completedDialogue has ended.
presenter_node_started(node_name)A node has started running.
presenter_node_completed(node_name)A node has finished running.
The Signal Presenter’s signals.

After a line, call proceed() on the Signal Presenter to move on. After options, call choose_option(index) with the option the player picked.

line_data.text doesn’t include the speaker’s name. The name is in line_data.character_name, which is empty for a line with no speaker.

This script does the same job as the simple Presenter above, using a Signal Presenter. It goes on any node in the scene, here the root:

extends Node2D

@export var signal_presenter: YarnSignalPresenter
@export var label: Label
@export var options_box: VBoxContainer


func _ready():
	signal_presenter.line_received.connect(_on_line_received)
	signal_presenter.options_received.connect(_on_options)


func _unhandled_input(event: InputEvent) -> void:
	if event.is_action_pressed("ui_accept"):
		signal_presenter.proceed()


func _on_line_received(line_data: Dictionary) -> void:
	var speaker: String = line_data.character_name
	if speaker.is_empty():
		label.text = line_data.text
	else:
		label.text = speaker + ": " + line_data.text


func _on_options(options_data: Array) -> void:
	label.text = ""
	for option in options_data:
		if not option.is_available:
			continue
		var button := Button.new()
		button.text = option.text
		var index: int = option.option_index
		button.pressed.connect(_choose.bind(index))
		options_box.add_child(button)


func _choose(index: int) -> void:
	for button in options_box.get_children():
		button.queue_free()
	signal_presenter.choose_option(index)
Lines 3–5 The Signal Presenter, the Label for lines and the container for option buttons. Set all three in the Inspector.
Lines 8–10 Connect to the Signal Presenter’s signals when the scene starts.
Lines 13–15 When the player presses ui_accept, tell the Signal Presenter to move on to the next line.
Lines 18–23 Show each line, with the speaker’s name in front if it has one.
Lines 26–27 When options arrive, clear the line.
Lines 28–35 Make a button for each option the player can choose. Each button remembers its option’s option_index.
Lines 38–41 When a button is pressed, remove the buttons and tell the Signal Presenter which option was picked.
Showing lines and options by connecting to a Signal Presenter.
GameNode2D
YarnDialogueRunner
YarnSignalPresenter
CanvasLayer
VBoxContainer
Label
OptionsBoxVBoxContainer
The scene for the script above. The Signal Presenter is the only Presenter in the Dialogue Runner’s list.

Samples

These samples each have their own Presenter. Open them from the samples browser in the Yarn Spinner tab:

  • Welcome builds slides on a projector screen from lines of dialogue.
  • Background Chatter shows lines above the characters speaking them, and moves on without the player pressing anything.
  • Phone Chat shows lines and options as chat bubbles in a messaging app.
  • Options That Timeout shows a countdown bar under the options, and picks one when it runs out.
  • Themed Line Presenter extends the Line Presenter, with its own styles and fonts.
Next step A Different Kind of Presenter A second custom Presenter, to show how differently a Presenter can work.