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, a chat log, or a phone screen, you can write your own Presenter.

A Presenter is a script that extends DialoguePresenterBase. 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 RunLineAsync on every Presenter in its Dialogue 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 RunOptionsAsync on every Presenter. The first one to return an option decides which option was picked. The Dialogue Runner then tells the others to stop, and they return null.

sequenceDiagram
  participant R as Dialogue Runner
  participant L as Line Presenter
  participant O as Options Presenter
  R->>L: RunLineAsync(line)
  R->>O: RunLineAsync(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: RunOptionsAsync(options)
  R->>O: RunOptionsAsync(options)
  L-->>R: returns null, it doesn't show options
  O-->>R: returns the option the player picked
  Note over R: The dialogue carries on<br>after that option
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 RunLineAsync, show the line, wait for as long as it needs to, then tidy up and return,
  • in RunOptionsAsync, show the options, wait for the player to pick one, then return it.

Starting a new Presenter

To make a new Presenter script, choose AssetsCreateYarn SpinnerDialogue Presenter Script in the Project window. The script it makes has every method you need, each with comments explaining what it’s for. You can also start from an empty MonoBehaviour and change it to extend DialoguePresenterBase, as in the example below.

DialoguePresenterBase has three methods every Presenter must have, even if they do nothing: RunLineAsync, OnDialogueStartedAsync and OnDialogueCompleteAsync. Write RunOptionsAsync too if your Presenter shows options.

These methods return a YarnTask, Yarn Spinner’s version of a C# Task. Write them as async methods and use await inside them, as you would with a Task. A method with nothing to wait for can return YarnTask.CompletedTask instead.

A simple Presenter

This Presenter shows each line in a TextMesh Pro text, and each set of options as buttons. The player presses Space to move on to the next line. It reads the keyboard with Unity’s Input System package.

using TMPro;
using UnityEngine;
using UnityEngine.InputSystem;
using UnityEngine.UI;
using Yarn.Unity;

public class SimplePresenter : DialoguePresenterBase
{
    [SerializeField] DialogueRunner dialogueRunner;
    [SerializeField] TMP_Text lineText;
    [SerializeField] Transform optionsContainer;
    [SerializeField] Button optionButtonPrefab;

    void Update()
    {
        var keyboard = Keyboard.current;
        if (keyboard != null && keyboard.spaceKey.wasPressedThisFrame)
        {
            dialogueRunner.RequestNextLine();
        }
    }

    public override YarnTask OnDialogueStartedAsync()
    {
        lineText.text = "";
        return YarnTask.CompletedTask;
    }

    public override YarnTask OnDialogueCompleteAsync()
    {
        lineText.text = "";
        return YarnTask.CompletedTask;
    }

    public override async YarnTask RunLineAsync(
        LocalizedLine line,
        LineCancellationToken token)
    {
        lineText.text = line.Text.Text;
        await YarnTask.WaitUntilCanceled(token.NextContentToken);
        lineText.text = "";
    }

    public override async YarnTask<DialogueOption> RunOptionsAsync(
        DialogueOption[] dialogueOptions,
        LineCancellationToken token)
    {
        var chosen = new YarnTaskCompletionSource<DialogueOption>();

        foreach (var option in dialogueOptions)
        {
            if (!option.IsAvailable)
            {
                continue;
            }
            var button = Instantiate(optionButtonPrefab, optionsContainer);
            var label = button.GetComponentInChildren<TMP_Text>();
            label.text = option.Line.TextWithoutCharacterName.Text;
            button.onClick.AddListener(() => chosen.TrySetResult(option));
        }

        DialogueOption result;
        using (token.NextContentToken.Register(() => chosen.TrySetResult(null)))
        {
            result = await chosen.Task;
        }

        foreach (Transform child in optionsContainer)
        {
            Destroy(child.gameObject);
        }
        return result;
    }
}
Line 7 Extend DialoguePresenterBase. The class name must match the file name, SimplePresenter.cs.
Lines 9–12 The Dialogue Runner, the text for lines, the container for option buttons, and a button prefab to copy for each option. Set all four in the Inspector.
Lines 14–21 When the player presses Space, ask the Dialogue Runner to move on.
Lines 23–33 Clear the text when dialogue starts and ends. Every Presenter needs these two methods. With nothing to wait for, they return YarnTask.CompletedTask.
Lines 35–37 RunLineAsync is called for each line. token tells you when the line should finish.
Line 39 Show the line. line.Text.Text includes the speaker’s name, like Rosa: Oh, hello.
Line 40 Wait until something asks for the next line, here the Space press on line 19.
Line 41 Clear the text. When RunLineAsync returns, this Presenter is done with the line.
Lines 44–46 RunOptionsAsync is called for each set of options, and returns the one the player picked.
Line 48 A completion source holds the choice once there is one. Awaiting its Task waits until then.
Lines 50–60 Make a button for each option the player can choose. Clicking a button sets the choice to that option.
Lines 62–66 If the Dialogue Runner stops the options, for example because another Presenter’s option was picked first, set the choice to null, meaning no choice. Then wait for a choice.
Lines 68–72 Remove the buttons, and return the option.
A Presenter that shows lines in a text and options as buttons.

To use it:

  1. Build the UI it needs: a Canvas with a GameObject inside it for the Presenter, holding a Text - TextMeshPro for lines and another GameObject with a Vertical Layout Group for the options.
Dialogue Runner Dialogue Runner
Canvas CanvasCanvas ScalerGraphic Raycaster
Simple Presenter Simple PresenterVertical Layout Group
Line Text TextMeshPro - Text (UI)
Options Vertical Layout Group
EventSystem Event SystemInput System UI Input Module
A scene with the Presenter above as its only Presenter.
  1. Make a button prefab: choose GameObjectUIButton - TextMeshPro, drag the button into the Project window, and delete it from the scene.
  2. Add the script to the Presenter’s GameObject, and set its Dialogue Runner, Line Text, Options Container and Option Button Prefab in the Inspector.
PLACEHOLDER — Screenshot needed
The Inspector for Simple Presenter, with Dialogue Runner, Line Text, Options Container and Option Button Prefab each set.
  1. Add the Presenter’s GameObject to the Dialogue Runner’s Dialogue Presenters list.
PLACEHOLDER — Video needed
The Presenter above running a short conversation: Rosa’s lines appearing as plain text, each all at once with the speaker’s name in front, then the two options as buttons, then her reply.

Showing lines

RunLineAsync gets the line as a LocalizedLine. The parts you’ll use most:

PropertyWhat it is
line.Text.TextThe whole line, including the speaker’s name.
line.CharacterNameThe speaker’s name, or null if the line has none.
line.TextWithoutCharacterName.TextThe line without the speaker’s name.
line.Text.AttributesThe line’s markup. See Markup.
line.TextIDThe line’s ID, like line:rosa-hello.
line.MetadataThe line’s hashtags.
line.AssetThe asset the Line Provider found for the line, such as a voice-over clip, or null. See Voice-Over.
What a Presenter can read from a line.

The text has already been through any markup processors, so formatting tags like [b] have been turned into TextMesh Pro rich text. Show it in a TextMesh Pro text to see the formatting.

Yarn Spinner adds a lastline tag to line.Metadata on any line that comes right before a set of options. Check for it if your Presenter needs to behave differently when options are about to appear.

Follow these rules in RunLineAsync:

  • Wait for whatever takes time before you return: typing, an animation, audio, or the token.
  • 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 throw an exception when the token is cancelled. Tidy up and return instead.

The token

Each call to RunLineAsync and RunOptionsAsync gets a LineCancellationToken. It tells your Presenter what the player or your game has requested:

Part of the tokenWhat it means
token.IsHurryUpRequestedThe 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.IsNextContentRequestedThe dialogue is moving on. Tidy up and return.
token.HurryUpTokenA CancellationToken that’s cancelled when a hurry is requested. Pass it to anything you await that should finish early.
token.NextContentTokenA CancellationToken that’s cancelled when the dialogue moves on.
await YarnTask.WaitUntilCanceled(token.NextContentToken)Waits until the dialogue moves on, or returns immediately if it already has.
What a Presenter can check on the token.

When the dialogue moves on, HurryUpToken is cancelled too, so anything waiting on it finishes as well.

These happen when:

  • the Line Advancer gets the player’s input, or the player clicks the Line Presenter’s continue button,
  • your own code calls RequestHurryUpLine() or RequestNextLine() on the Dialogue Runner,
  • a Voice Over Presenter’s clip finishes, with End Line When Voiceover Complete on,
  • another Presenter’s option is picked.

The Dialogue Runner makes a new token for each line and each set of options. Don’t keep one to use later.

Showing options

RunOptionsAsync gets an array of DialogueOption. Each has Line, the option’s text as a LocalizedLine, and IsAvailable, which is false when the option’s condition failed.

Return the DialogueOption the player picked. Return null when the token says the dialogue is moving on. If your Presenter doesn’t show options, leave RunOptionsAsync out. DialoguePresenterBase returns null for you.

If every Presenter returns null, what happens depends on the Dialogue Runner’s Allow Option Fallthrough setting. See Dialogue Runner.

Other methods you can use

MethodWhen it’s called
OnDialogueStartedAsync()Dialogue has started. Set up your UI here. The Dialogue Runner waits for every Presenter to return before it sends any lines.
OnDialogueCompleteAsync()Dialogue has ended. Hide or clear your UI here.
OnNodeEnter(nodeName)A node has started running. Override it if you need it.
OnNodeExit(nodeName)A node has finished running. Override it if you need it.
Methods the Dialogue Runner calls on a Presenter.

To fade your Presenter in or out, put a Canvas Group on it, and await Effects.FadeAlphaAsync(canvasGroup, from, to, duration, token.HurryUpToken). It fades the Canvas Group’s alpha from one value to another, from 0 to 1, over duration seconds. The Line Presenter uses it to fade the dialogue box.

Samples

These samples each have their own Presenter. To add them to your project, see Samples.

  • 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.
Next step A Different Kind of Presenter A second custom Presenter, to show how differently a Presenter can work.