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 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 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;
}
}DialoguePresenterBase. The class name must match the file name, SimplePresenter.cs.YarnTask.CompletedTask.RunLineAsync is called for each line. token tells you when the line should finish.line.Text.Text includes the speaker’s name, like Rosa: Oh, hello.RunLineAsync returns, this Presenter is done with the line.RunOptionsAsync is called for each set of options, and returns the one the player picked.Task waits until then.null, meaning no choice. Then wait for a choice.To use it:
- 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.
- Make a button prefab: choose , drag the button into the Project window, and delete it from the scene.
- Add the script to the Presenter’s GameObject, and set its Dialogue Runner, Line Text, Options Container and Option Button Prefab in the Inspector.
- Add the Presenter’s GameObject to the Dialogue Runner’s Dialogue Presenters list.
Showing lines
RunLineAsync gets the line as a LocalizedLine. The parts you’ll use most:
| Property | What it is |
|---|---|
line.Text.Text | The whole line, including the speaker’s name. |
line.CharacterName | The speaker’s name, or null if the line has none. |
line.TextWithoutCharacterName.Text | The line without the speaker’s name. |
line.Text.Attributes | The line’s markup. See Markup. |
line.TextID | The line’s ID, like line:rosa-hello. |
line.Metadata | The line’s hashtags. |
line.Asset | The asset the Line Provider found for the line, such as a voice-over clip, or null. See Voice-Over. |
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 token | What it means |
|---|---|
token.IsHurryUpRequested | The 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.IsNextContentRequested | The dialogue is moving on. Tidy up and return. |
token.HurryUpToken | A CancellationToken that’s cancelled when a hurry is requested. Pass it to anything you await that should finish early. |
token.NextContentToken | A 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. |
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()orRequestNextLine()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
| Method | When 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. |
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.