Async and Cancellation

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.

YarnTask

Yarn Spinner’s async methods return a YarnTask, or a YarnTask<T> when they return a value. You await a YarnTask the same way as any other task, and you can return one from your own async methods.

A YarnTask wraps one of three kinds of task, depending on your project. Yarn Spinner picks the first of these that’s available:

BackendUsed when
UniTaskThe UniTask package is installed.
AwaitableYou’re using Unity 2023.1 or later.
System.Threading.Tasks.TaskNeither of the others is available.
The three backends a YarnTask can use, in the order Yarn Spinner tries them.

Most of the time you don’t need to know which one you’re using. A YarnTask converts to and from the backend’s own type, and from the other kinds of task where they’re available, so you can return a Task or an Awaitable where a YarnTask is expected.

YarnTask has static methods for common waits:

MethodWhat it does
YarnTask.Yield()Waits until the next frame.
YarnTask.Delay(milliseconds, token)Waits for a number of milliseconds, or a TimeSpan.
YarnTask.WaitUntil(predicate, token)Waits until predicate returns true.
YarnTask.WaitUntilCanceled(token)Waits until token is cancelled.
YarnTask.WhenAll(tasks)Waits until every task in tasks has finished.
YarnTask.CompletedTaskA task that has already finished. Return it from a method that has nothing to wait for.
Some of YarnTask’s static methods.

With UniTask and Awaitable, awaiting something that’s been cancelled throws an OperationCanceledException. To wait without the exception, add .SuppressCancellationThrow(). It returns true if the task was cancelled.

var cancelled = await YarnTask.Delay(2000, token).SuppressCancellationThrow();
Waiting for two seconds, or until the token is cancelled, without throwing.

How the dialogue waits

The dialogue waits for two kinds of things:

  • Commands: if a Command returns a YarnTask, a Task, an Awaitable, a UniTask, a Coroutine or an IEnumerator, the dialogue waits until it’s finished. See Commands.
  • Presenters: the dialogue waits until every Presenter has finished RunLineAsync() or RunOptionsAsync(). 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 LineCancellationToken, which is passed to each Presenter’s RunLineAsync() or RunOptionsAsync(). 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 Advancer asks for hurry up if the line is still appearing, 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: RunLineAsync(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 RunLineAsync()
  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 dialogueRunner.RequestHurryUpLine() and dialogueRunner.RequestNextLine(). For options, dialogueRunner.RequestHurryUpOption() asks for hurry up. Next content is requested for options when a Presenter picks one, so the other Presenters know to stop waiting. Stop() asks for next content as well.

A request for next content also counts as hurry up, so IsHurryUpRequested is true after either. Check IsNextContentRequested when you need to tell them apart.

The token has these properties:

Part of the tokenWhat it does
IsHurryUpRequestedtrue once hurry up or next content has been requested.
IsNextContentRequestedtrue once next content has been requested.
HurryUpTokenA CancellationToken that’s cancelled when hurry up or next content is requested. Pass it to anything that should finish early.
NextContentTokenA CancellationToken that’s cancelled when next content is requested.
What you can use on a LineCancellationToken.

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:

using UnityEngine;
using Yarn.Unity;

public class PortraitPresenter : DialoguePresenterBase
{
    [SerializeField] CanvasGroup portrait;

    public override YarnTask OnDialogueStartedAsync() => YarnTask.CompletedTask;

    public override YarnTask OnDialogueCompleteAsync() => YarnTask.CompletedTask;

    public override async YarnTask RunLineAsync(LocalizedLine line, LineCancellationToken token)
    {
        portrait.alpha = 0;

        while (portrait.alpha < 1 && !token.IsHurryUpRequested)
        {
            portrait.alpha += Time.deltaTime;
            await YarnTask.Yield();
        }
        portrait.alpha = 1;

        await YarnTask.WaitUntilCanceled(token.NextContentToken).SuppressCancellationThrow();
        portrait.alpha = 0;
    }
}
Lines 8–10 This Presenter has nothing to do when the dialogue starts or ends, so it returns a task that’s already finished.
Line 14 Start the fade from invisible.
Lines 16–20 Fade in over one second, a frame at a time, until the fade is done or hurry up is requested.
Line 21 Either way, 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. A Presenter that never returns holds up the dialogue for good. This usually means it’s waiting for something that will never happen, like a button press on a button that’s already been hidden.

When a request comes in, return from RunLineAsync() or RunOptionsAsync(). Don’t let an OperationCanceledException escape. If one does, the Dialogue Runner catches it and logs a warning that names the Presenter. Use SuppressCancellationThrow(), or catch the exception yourself, and then return.

Waiting in Commands

Commands don’t get a token. While a Command is running, pressing continue, RequestHurryUpLine() and RequestNextLine() 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, return a coroutine that uses WaitForSeconds, or a YarnTask from YarnTask.Delay():

void Awake()
{
    dialogueRunner.AddCommandHandler("pause_for_effect", PauseForEffect);
}

IEnumerator PauseForEffect()
{
    yield return new WaitForSeconds(2);
}
A pause_for_effect Command that waits two seconds.

The built-in <<wait>> Command uses WaitForSeconds too.

Pausing the game

When you pause the game by setting Time.timeScale to 0, anything that counts scaled time stops:

  • The Line Presenter stops typing, and its fades stop.
  • <<wait>>, and anything else that uses WaitForSeconds, stops counting.
  • YarnTask.Delay() stops counting with UniTask and Awaitable. With System.Threading.Tasks.Task, it counts real time and carries on.

The Line Advancer still reads input while Time.timeScale is 0, so stop it from advancing the dialogue while your pause menu is open.

In your own Presenters and Commands, count time with Time.deltaTime, WaitForSeconds or YarnTask.Delay() with UniTask or Awaitable, so they pause too.

Stopping the dialogue

Stop() asks the current line or options for next content, and ends the dialogue. Each Presenter’s OnDialogueCompleteAsync() is called, and then the Dialogue Runner invokes On Dialogue Complete. To wait until it’s finished, await it:

await dialogueRunner.Stop();
Stopping the dialogue, and waiting until it has finished.

The Dialogue Runner also stops the dialogue when it’s destroyed.

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 dialogueRunner.IsDialogueRunning after each await:

async YarnTask WalkHome()
{
    await WalkTo(door.position);
    if (!dialogueRunner.IsDialogueRunning)
    {
        return;
    }
    await WalkTo(home.position);
}
A walk_home Command, added with AddCommandHandler(), that stops part way if the dialogue is stopped.

Waiting for something that might already be done

Sometimes you want to wait for something that might have finished before you get round to waiting for it, like an event that’s already been invoked. Use a YarnTaskCompletionSource for this. It records that it has finished, and the value it finished with. Awaiting its Task returns at once if it’s already finished.

This Presenter starts loading a portrait when the dialogue starts, and waits for it in RunLineAsync(). If the portrait has already loaded by then, there’s no wait:

using UnityEngine;
using UnityEngine.UI;
using Yarn.Unity;

public class PortraitLoadingPresenter : DialoguePresenterBase
{
    [SerializeField] Image portrait;

    YarnTaskCompletionSource<Sprite> portraitLoading;

    public override YarnTask OnDialogueStartedAsync()
    {
        portraitLoading = new YarnTaskCompletionSource<Sprite>();
        var request = Resources.LoadAsync<Sprite>("Portraits/Rosa");
        request.completed += _ => portraitLoading.TrySetResult((Sprite)request.asset);
        return YarnTask.CompletedTask;
    }

    public override YarnTask OnDialogueCompleteAsync() => YarnTask.CompletedTask;

    public override async YarnTask RunLineAsync(LocalizedLine line, LineCancellationToken token)
    {
        if (portrait.sprite == null)
        {
            portrait.sprite = await portraitLoading.Task;
        }
        await YarnTask.WaitUntilCanceled(token.NextContentToken).SuppressCancellationThrow();
    }
}
Lines 13–15 Start loading when the dialogue starts, and finish the completion source with the sprite when it has loaded. The first line might not need the portrait for a while.
Line 16 Return straight away, so the dialogue doesn’t wait for the load to start.
Lines 23–26 Wait for the sprite, only if it hasn’t been shown yet. If it has already loaded, this doesn’t wait.
A Presenter that loads a portrait in the background.

Await a completion source’s Task once, and keep the result if you need it again. With Awaitable, a task can’t be awaited more than once.

A YarnTaskCompletionSource has:

  • TrySetResult(value), which finishes it with a value. It returns false, and does nothing, if it has already finished.
  • TrySetCanceled() and TrySetException(exception), which finish it with a cancellation or an error instead.
  • Task, which you await to get the value.

YarnTaskCompletionSource without a type finishes without a value.

Next step Localisation and Assets Translating lines, and how voice clips and images are found for each line.