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.

Yarn Spinner for Unity does this with C# async and await. Yarn Spinner for Unreal Engine doesn’t use tasks. When the Dialogue Runner gives a Presenter a line, Run Line starts showing it and returns straight away. The Presenter calls a function when it has finished, and finds out about requests to hurry up or move on from events and a cancellation token. The behaviour is the same as in Unity.

How the dialogue waits

The dialogue waits for two kinds of things:

  • Commands: a blocking Command keeps the dialogue waiting until Complete Blocking Command is called on the Dialogue Runner. Any other Command is finished when its handler returns. See Commands.
  • Presenters: for a line, the dialogue waits until every Presenter has called On Line Presentation Complete. For options, it waits until a Presenter calls On Option Selected. 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 cancellation token. 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 call On Line Presentation Complete as soon as possible.
The two requests a token can carry.

When the player presses the advance key, the Line Advancer asks for hurry up the first time, and next content the second time, as long as its Combine Hurry Up And Advance is on. 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
  Note over Presenter: Typing the line
  Player->>Runner: Advance
  Runner->>Presenter: token: hurry up
  Note over Presenter: Shows the whole line
  Player->>Runner: Advance
  Runner->>Presenter: token: next content
  Presenter-->>Runner: On Line Presentation Complete
  Note over Runner: The dialogue moves on
Two presses of the advance key while a line is typing.

You can ask for them from your own Blueprints and C++ too, with the Dialogue Runner’s Request Hurry Up and Request Next Line. For options, Request Hurry Up Option asks for hurry up. Next content is requested for options when a Presenter picks one, so the other Presenters know to stop waiting. 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.

In a Presenter

Every Presenter, in Blueprints and C++, has these functions and events for the current line and options:

Function or eventWhat it does
Is Hurry Up RequestedTrue once hurry up or next content has been requested for the current line.
Is Next Content RequestedTrue once next content has been requested for the current line.
Is Option Hurry Up RequestedTrue once hurry up or next content has been requested for the current options.
Is Option Next Content RequestedTrue once next content has been requested for the current options.
On Hurry Up RequestedAn event you can override. Called when the Dialogue Runner’s Request Hurry Up is called.
On Next Line RequestedAn event you can override. Called when the Dialogue Runner’s Request Next Line is called. If you don’t override it, it calls On Line Presentation Complete for you.
On Options Hurry Up RequestedAn event you can override. Called when the Dialogue Runner’s Request Hurry Up Option is called.
What a Presenter can use to find out about requests.

The three events are only called by the Dialogue Runner’s Request functions. Next content is also requested when another Presenter ends the line, and when the dialogue stops, and the events aren’t called then. Check Is Next Content Requested as well, so your Presenter doesn’t miss it.

The Dialogue Runner’s Get Current Cancellation Token and Get Current Options Cancellation Token return the tokens themselves. In Blueprints, connect one to the Cancellation Token pin of the Yarn Fade Widget, Yarn Fade Widget In, Yarn Fade Widget Out, Yarn Punch Scale and Yarn Shake nodes, and the effect finishes at once when either request comes in.

The token in C++

In C++, the token is an FYarnLineCancellationToken, from YarnCancellationToken.h. While a Presenter is showing a line, it’s in the Presenter’s CurrentLineCancellationToken, and while it’s showing options, CurrentOptionsCancellationToken. The token has:

FunctionWhat it does
IsHurryUpRequested()true once hurry up or next content has been requested.
IsCancellationRequested()true once next content has been requested.
IsAnyCancellationRequested()true once either has been requested.
CanBeCancelled()false if the token isn’t connected to anything, such as after the line has finished.
What you can use on an FYarnLineCancellationToken.

A token only reports what’s been requested. The requests themselves are made on a UYarnCancellationTokenSource, which the Dialogue Runner owns. It has one source for the whole dialogue, and one for each line and set of options that’s linked to it, so stopping the dialogue cancels the line too. Hurry up is RequestHurryUp() on a source, and next content is Cancel().

To be told about a request instead of checking for it, make your own source that’s linked to the token, and register a function with it:

LineSource = UYarnCancellationTokenSource::CreateLinkedTokenSource(this, { CurrentLineCancellationToken });
LineSource->RegisterOnCancelled([WeakThis = TWeakObjectPtr<ThisClass>(this)]()
{
	if (WeakThis.IsValid())
	{
		WeakThis->FinishLine();
	}
});
Calling FinishLine() when next content is requested for the current line, in a Presenter with a LineSource property.

RegisterOnHurryUp() does the same for hurry up. If next content has already been requested, the function is called straight away. Keep the linked source in a UPROPERTY so it isn’t garbage collected, and call UnlinkFromParents() on it when the line has finished.

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:

In a Blueprint Presenter, made from the Yarn Dialogue Presenter class:

  1. Override Run Line. Set the portrait’s render opacity to 0, then add a Yarn Fade Widget In node. Connect the portrait to Widget, set Duration to 1, and connect Get Dialogue Runner then Get Current Cancellation Token to Cancellation Token. The fade finishes at once if hurry up is requested.
  2. Override On Next Line Requested. Set the portrait’s render opacity to 0, then call On Line Presentation Complete.
PLACEHOLDER — Screenshot needed
Two Blueprint graphs in a Presenter Blueprint. In the first, Event Run Line sets Portrait’s Render Opacity to 0, then runs Yarn Fade Widget In with Portrait connected to Widget, Duration set to 1, and Get Dialogue Runner connected to Get Current Cancellation Token, which is connected to Cancellation Token. In the second, Event On Next Line Requested sets Portrait’s Render Opacity to 0, then calls On Line Presentation Complete.

This Blueprint version only finishes when Request Next Line is called. If another Presenter can end the line, such as a Voice Over Presenter, also check Is Next Content Requested, for example from a looping timer, and finish when it’s true.

#pragma once

#include "CoreMinimal.h"
#include "YarnDialoguePresenter.h"
#include "PortraitPresenter.generated.h"

class UWidget;

UCLASS(ClassGroup = (YarnSpinner), meta = (BlueprintSpawnableComponent))
class UPortraitPresenter : public UYarnDialoguePresenter
{
	GENERATED_BODY()

public:
	UPortraitPresenter();

	UPROPERTY(BlueprintReadWrite, Category = "Portrait")
	TObjectPtr<UWidget> Portrait;

	virtual void TickComponent(float DeltaTime, ELevelTick TickType, FActorComponentTickFunction* ThisTickFunction) override;
	virtual void RunLine_Implementation(const FYarnLocalizedLine& Line, bool bCanHurry) override;
	virtual void OnNextLineRequested_Implementation() override;
};
Lines 17–18 The portrait widget. Set it from the Blueprint that creates your widget.
PortraitPresenter.h.
#include "PortraitPresenter.h"
#include "Components/Widget.h"

UPortraitPresenter::UPortraitPresenter()
{
	PrimaryComponentTick.bCanEverTick = true;
	PrimaryComponentTick.bStartWithTickEnabled = false;
}

void UPortraitPresenter::RunLine_Implementation(const FYarnLocalizedLine& Line, bool bCanHurry)
{
	if (!Portrait)
	{
		OnLinePresentationComplete();
		return;
	}

	Portrait->SetRenderOpacity(0.0f);
	SetComponentTickEnabled(true);
}

void UPortraitPresenter::OnNextLineRequested_Implementation()
{
}

void UPortraitPresenter::TickComponent(float DeltaTime, ELevelTick TickType, FActorComponentTickFunction* ThisTickFunction)
{
	Super::TickComponent(DeltaTime, TickType, ThisTickFunction);

	if (IsNextContentRequested())
	{
		Portrait->SetRenderOpacity(0.0f);
		SetComponentTickEnabled(false);
		OnLinePresentationComplete();
		return;
	}

	if (IsHurryUpRequested())
	{
		Portrait->SetRenderOpacity(1.0f);
	}
	else
	{
		Portrait->SetRenderOpacity(FMath::Min(Portrait->GetRenderOpacity() + DeltaTime, 1.0f));
	}
}
Lines 4–8 Tick only while a line is showing.
Lines 12–16 With no portrait, there’s nothing to show, so finish at once.
Lines 18–19 Start the fade from invisible, and start ticking. RunLine returns straight away, and the dialogue waits until the Presenter calls OnLinePresentationComplete().
Lines 22–24 Replace the default, which would finish the line as soon as Request Next Line is called. The tick finishes it instead.
Lines 30–36 Once next content has been requested, for any reason, hide the portrait, stop ticking and finish. The dialogue can move on.
Lines 38–45 Fade in over one second, a frame at a time. If hurry up has been requested, show the portrait fully.
PortraitPresenter.cpp.

Ending the line from a Presenter

Most Presenters call On Line Presentation Complete when they’ve finished, and the line carries on until the others have finished too. A Presenter whose timing should end the line, such as one that plays a voice clip, can call On Line Presentation Complete And End Line instead. The Dialogue Runner then requests next content for the line, so the other Presenters wrap up.

Only call it when your Presenter finished by itself. If it finished because next content was requested, call On Line Presentation Complete, because the line is already ending.

Finish promptly

Once next content has been requested, the Dialogue Runner can’t move on until every Presenter has called On Line Presentation Complete. A Presenter that never calls it holds up the dialogue for good. This usually means it’s waiting for something that will never happen, like an event that has already fired.

Waiting in Commands

Commands don’t get a token. While a Command is running, the Line Advancer, Request Hurry Up and Request Next Line don’t move the dialogue on, 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, use the built-in <<wait>> Command in Yarn. To wait in your own Command, make it a blocking Command, and call Complete Blocking Command on the Dialogue Runner when the wait is over:

In Blueprints, use Register Blocking Command Handler, and bind Handler to a custom event that runs a Delay node, then Complete Blocking Command. A function on one of the Dialogue Runner’s Command Handler Objects blocks too, if it returns a Boolean and returns true.

PLACEHOLDER — Screenshot needed
A Blueprint graph where Event BeginPlay calls Register Blocking Command Handler with the Yarn Dialogue Runner component connected to Dialogue Runner, Command Name set to pause_for_effect, and Handler bound to a custom event. The custom event runs Delay with Duration 2, then Complete Blocking Command on the Yarn Dialogue Runner.
DialogueRunner->AddBlockingCommandHandler(TEXT("pause_for_effect"), [this](const TArray<FString>& Parameters)
{
	GetWorldTimerManager().SetTimer(PauseTimer, FTimerDelegate::CreateWeakLambda(this, [this]()
	{
		DialogueRunner->CompleteBlockingCommand();
	}), 2.0f, false);
});
A pause_for_effect Command that waits two seconds. PauseTimer is an FTimerHandle on the Actor.

<<wait>> uses a timer from the world’s timer manager, so it counts game time, the same as timers you set yourself and the Delay node.

Pausing the game

When you pause the game with Set Game Paused, timers, Delay nodes and component ticks stop:

  • The Line Presenter stops typing, and stops auto-advancing.
  • <<wait>>, and timers and Delay nodes in your own Commands, stop counting.
  • The Line Advancer stops reading its keys, so a key press doesn’t advance the dialogue under your pause menu.

Yarn Fade Widget, Yarn Punch Scale and Yarn Shake count real time, so they carry on while the game is paused.

In your own Presenters and Commands, count time with timers, Delay or a component’s tick, so they pause too.

Stopping the dialogue

Stop Dialogue asks the current line and options for next content, and ends the dialogue. It also stops <<wait>>, and stops waiting for a blocking Command. Each Presenter’s On Dialogue Complete is called, and then the Dialogue Runner’s On Dialogue Complete event. All of this happens before Stop Dialogue returns, so there’s nothing to wait for.

The <<stop>> Command does the same from Yarn. The Dialogue Runner also stops the dialogue in its EndPlay, such as when its Actor is destroyed or the level ends.

A Presenter that calls On Line Presentation Complete after the dialogue has stopped is ignored.

If a Command is waiting when the dialogue stops, the dialogue ends without waiting for it. The Command’s own timers and callbacks carry on, but the dialogue doesn’t continue after it, and calling Complete Blocking Command only logs a warning. If the rest of a Command shouldn’t happen once the dialogue has stopped, check Is Dialogue Running after each wait:

DialogueRunner->AddBlockingCommandHandler(TEXT("walk_home"), [this](const TArray<FString>& Parameters)
{
	WalkTo(Door, FSimpleDelegate::CreateWeakLambda(this, [this]()
	{
		if (!DialogueRunner->IsDialogueRunning())
		{
			return;
		}
		WalkTo(Home, FSimpleDelegate::CreateWeakLambda(this, [this]()
		{
			DialogueRunner->CompleteBlockingCommand();
		}));
	}));
});
A blocking walk_home Command that stops part way if the dialogue is stopped. WalkTo is your own function, which calls a delegate when the character arrives.
Next step Localisation and Assets Translating lines, and how voice clips and images are found for each line.