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 component whose class is a subclass of Yarn Dialogue Presenter. You can make one as a Blueprint, or as a C++ class that extends UYarnDialoguePresenter. 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 Dialogue Presenters list, all at the same time. Each Presenter calls On Line Presentation Complete when it’s finished with the line. The Dialogue Runner moves on to the next line once every one of them has.

For each set of options, it calls Run Options on every Presenter. The first one to call On Option Selected with an option’s index decides which option was picked. The Dialogue Runner then tells the others to stop, through their tokens.

sequenceDiagram
  participant R as Dialogue Runner
  participant L as Line Presenter
  participant O as Options Presenter
  R->>L: Run Line
  R->>O: Run Line
  O-->>R: On Line Presentation Complete, straight away
  L-->>R: On Line Presentation Complete, once the player continues
  Note over R: Both have finished,<br>so the options start
  R->>L: Run Options
  R->>O: Run Options
  Note over L: Doesn't show options
  O-->>R: On Option Selected (1)
  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 to finish each line.

A Presenter has two jobs:

  • in Run Line, show the line, wait for as long as it needs to, then tidy up and call On Line Presentation Complete,
  • in Run Options, show the options, wait for the player to pick one, then call On Option Selected with its index.

Run Line and Run Options start the work, and return straight away. Your Presenter finishes later, by calling On Line Presentation Complete or On Option Selected.

A simple Presenter

This Presenter shows each line in a Text Block, and each set of options as buttons. The player presses Space to move on to the next line, and clicks a button to pick an option.

First, make the Widget Blueprint it shows:

  1. In the Content Browser, click Add, and choose User InterfaceWidget Blueprint. Pick User Widget, and name it WBP_SimpleDialogue.
  2. Add a Vertical Box. Inside it, add a Text Block (Text in the Palette) named LineText, and another Vertical Box named OptionsBox.

The same Presenter can be made as a Blueprint. The option buttons need a widget of their own, so they can say which option they’re for:

  1. Make a Widget Blueprint called WBP_OptionButton. In the Designer, add a Button, and drag a Text Block (Text in the Palette) onto it. Select the Text Block and turn on Is Variable.
  2. Click Graph, at the top right. Make the two variables:
    • In the My Blueprint panel, click + next to Variables. Name it Option Index, and in the Details panel set its Variable Type to Integer.
    • Add another called Label, of type Text.
    • Select each one, and in the Details panel turn on Instance Editable and Expose on Spawn. Exposing them adds them as pins on Create Widget, so the Presenter can fill them in.
  3. From Event Construct, add Set Text for the Text Block: drag the Text Block in from Variables, then drag from it and choose Set Text. Drag Label in, choose Get Label, and connect it to In Text.
  4. In My Blueprint, click + next to Event Dispatchers and name it Picked. In the Details panel, under Inputs, click +, name the input Index, and set its type to Integer.
  5. Click Designer, select the Button, and in the Details panel click + next to On Clicked, in the Events section. This adds an On Clicked event to the graph. Drag Picked in from Event Dispatchers and choose Call. Connect On Clicked to Call Picked, and connect Get Option Index to its Index pin.
  6. Compile and save.

Then make the Presenter:

  1. In the Content Browser, click Add and choose Blueprint Class. Don’t pick from the Common Classes buttons, such as Actor: a Presenter is a component, and a Blueprint can’t be changed from an Actor to a component later. Instead, expand All Classes at the bottom of the dialog, search for Yarn Dialogue Presenter, select YarnDialoguePresenter, and click Select. Name it BP_SimplePresenter, and open it. The top right of its editor should say Parent class: Yarn Dialogue Presenter.
  2. In the My Blueprint panel, hover over Functions and click Override. Add On Dialogue Started, On Dialogue Complete, Run Line, On Next Line Requested and Run Options, one at a time. They appear as red event nodes in the Event Graph.
  3. On Event On Dialogue Started:
    • Drag from its exec pin and add Create Widget. Set Class to WBP_SimpleDialogue.
    • Drag from its exec pin and add Add to Viewport, and connect Create Widget’s Return Value to its Target.
    • Right-click Create Widget’s Return Value pin and choose Promote to Variable. This adds a variable, named New Var at first, and a Set node that stores the widget in it. If the Set node isn’t already in the white exec chain, connect it after Add to Viewport. Rename the variable Dialogue Widget in My Blueprint. The other events read the widget from this variable.
    • After that, add Get Player Controller, drag from its Return Value and add Set Show Mouse Cursor, and turn on its Show Mouse Cursor box. Connect it to the end of the chain.
  4. On Event Run Line:
    • Right-click its Line pin and choose Split Struct Pin. Line holds several values in one pin; splitting it gives one pin for each value, named after the pin and the value, such as Line Text.
    • Drag Dialogue Widget in from Variables and choose Get Dialogue Widget. Drag from it and add Get Line Text, then drag from that and add Set Text.
    • Connect Line Text from the event to In Text, and the event’s exec pin to Set Text.
  5. Add Event Tick, by right-clicking the graph and searching for it. From it:
    • Add a Branch. Its condition is two things together, so drag from Condition and add an AND node.
    • Into one input of AND, connect Is Presenting Line, which you find by right-clicking the graph and searching for it.
    • Into the other, connect Was Input Key Just Pressed: add Get Player Controller, drag from its Return Value, add Was Input Key Just Pressed, and set its Key to Space Bar.
    • From the Branch’s True pin, add Get Dialogue Runner, then drag from it and add Request Next Line.
  6. On Event On Next Line Requested, add a Branch with Is Presenting Line as its condition. From True, set LineText’s text to nothing, the same way as in step 4 with In Text left empty, then add On Line Presentation Complete.
  7. On Event Run Options, make a button for each option the player can choose:
    1. Right-click the event’s Options pin and choose Split Struct Pin. It becomes Options Options, the array of options.
    2. Drag from Options Options and add For Each Loop. Connect the event’s exec pin to the loop. Loop Body runs once for each option, with the option in Array Element and its position in the array, starting at 0, in Array Index.
    3. Right-click Array Element and choose Split Struct Pin. You now have Array Element Line, Array Element Is Available and Array Element Option ID.
    4. Drag from Loop Body and add a Branch, with Array Element Is Available as its Condition. The rest of this step comes from the Branch’s True pin, so options the player can’t choose get no button.
    5. From True, add Create Widget with Class set to WBP_OptionButton. Because Option Index and Label are exposed on spawn, the node has pins for them. Connect Array Index to Option Index. Right-click Array Element Line, choose Split Struct Pin, and connect Array Element Line Text Without Character Name to Label.
    6. Get Dialogue Widget, drag from it and add Get Options Box, then drag from that and add Add Child. Connect Create Widget’s exec output to Add Child, and its Return Value, the new button, to Content.
    7. Drag from Create Widget’s Return Value again and add Bind Event to Picked, after Add Child. Drag from its red Event pin and choose Add Custom Event. The new event has an Index pin, the number the button sends when it’s clicked.
  8. From that custom event, get Options Box from Dialogue Widget and add Clear Children, to remove the buttons. Then add On Option Selected, and connect the event’s Index to it.
  9. On Event On Dialogue Complete, get Dialogue Widget and add Remove from Parent.
  10. Compile and save.
PLACEHOLDER — Screenshot needed
A Blueprint graph for BP_SimplePresenter: Event Run Line, with the Line pin broken to its Text, going into Set Text on LineText; below it, Event On Next Line Requested going through a Branch on Is Presenting Line into Set Text with an empty Text, then On Line Presentation Complete.

Add BP_SimplePresenter to your Actor, and to the Dialogue Runner’s Dialogue Presenters list, the same as the C++ version.

Add a C++ class to your project, and add "YarnSpinner" and "UMG" to the PublicDependencyModuleNames in your module’s .Build.cs file.

#pragma once

#include "CoreMinimal.h"
#include "Components/Button.h"
#include "YarnDialoguePresenter.h"
#include "SimplePresenter.generated.h"

class UTextBlock;
class UUserWidget;
class UVerticalBox;

UCLASS()
class USimpleOptionButton : public UButton
{
	GENERATED_BODY()

public:
	int32 OptionIndex = INDEX_NONE;

	TFunction<void(int32)> OnPicked;

	UFUNCTION()
	void HandleClicked();
};

UCLASS(ClassGroup = (Custom), meta = (BlueprintSpawnableComponent))
class USimplePresenter : public UYarnDialoguePresenter
{
	GENERATED_BODY()

public:
	USimplePresenter();

	UPROPERTY(EditAnywhere, Category = "Simple Presenter")
	TSubclassOf<UUserWidget> WidgetClass;

	virtual void TickComponent(float DeltaTime, ELevelTick TickType, FActorComponentTickFunction* ThisTickFunction) override;

	virtual void OnDialogueStarted_Implementation() override;
	virtual void OnDialogueComplete_Implementation() override;
	virtual void RunLine_Implementation(const FYarnLocalizedLine& Line, bool bCanHurry) override;
	virtual void OnNextLineRequested_Implementation() override;
	virtual void RunOptions_Implementation(const FYarnOptionSet& Options) override;

private:
	UPROPERTY(Transient)
	TObjectPtr<UUserWidget> Widget;

	UPROPERTY(Transient)
	TObjectPtr<UTextBlock> LineText;

	UPROPERTY(Transient)
	TObjectPtr<UVerticalBox> OptionsBox;

	void FinishLine();
	void PickOption(int32 Index);
};
SimplePresenter.h

USimpleOptionButton is a button that remembers which option it’s for. A button’s On Clicked event doesn’t say which button was clicked, so each one passes its own OptionIndex on to the Presenter.

#include "SimplePresenter.h"

#include "Blueprint/UserWidget.h"
#include "Blueprint/WidgetTree.h"
#include "Components/TextBlock.h"
#include "Components/VerticalBox.h"
#include "GameFramework/PlayerController.h"
#include "YarnDialogueRunner.h"

void USimpleOptionButton::HandleClicked()
{
	if (OnPicked)
	{
		OnPicked(OptionIndex);
	}
}

USimplePresenter::USimplePresenter()
{
	PrimaryComponentTick.bCanEverTick = true;
}

void USimplePresenter::OnDialogueStarted_Implementation()
{
	APlayerController* Player = GetWorld()->GetFirstPlayerController();
	Widget = CreateWidget<UUserWidget>(Player, WidgetClass);
	LineText = Cast<UTextBlock>(Widget->GetWidgetFromName(TEXT("LineText")));
	OptionsBox = Cast<UVerticalBox>(Widget->GetWidgetFromName(TEXT("OptionsBox")));
	Widget->AddToViewport();
	Player->SetShowMouseCursor(true);
	Player->SetInputMode(FInputModeGameAndUI());
}

void USimplePresenter::OnDialogueComplete_Implementation()
{
	APlayerController* Player = GetWorld()->GetFirstPlayerController();
	Widget->RemoveFromParent();
	Player->SetShowMouseCursor(false);
	Player->SetInputMode(FInputModeGameOnly());
}

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

	if (bIsPresentingOptions && IsOptionNextContentRequested())
	{
		OptionsBox->ClearChildren();
		bIsPresentingOptions = false;
	}

	if (!bIsPresentingLine)
	{
		return;
	}

	if (IsNextContentRequested())
	{
		FinishLine();
		return;
	}

	APlayerController* Player = GetWorld()->GetFirstPlayerController();
	if (Player && Player->WasInputKeyJustPressed(EKeys::SpaceBar))
	{
		GetDialogueRunner()->RequestNextLine();
	}
}

void USimplePresenter::RunLine_Implementation(const FYarnLocalizedLine& Line, bool bCanHurry)
{
	LineText->SetText(Line.Text);
}

void USimplePresenter::OnNextLineRequested_Implementation()
{
	if (bIsPresentingLine)
	{
		FinishLine();
	}
}

void USimplePresenter::FinishLine()
{
	LineText->SetText(FText::GetEmpty());
	OnLinePresentationComplete();
}

void USimplePresenter::RunOptions_Implementation(const FYarnOptionSet& Options)
{
	for (int32 i = 0; i < Options.Options.Num(); i++)
	{
		const FYarnOption& Option = Options.Options[i];
		if (!Option.bIsAvailable)
		{
			continue;
		}

		UTextBlock* Label = Widget->WidgetTree->ConstructWidget<UTextBlock>();
		Label->SetText(Option.Line.TextWithoutCharacterName);

		USimpleOptionButton* Button = Widget->WidgetTree->ConstructWidget<USimpleOptionButton>();
		Button->AddChild(Label);
		Button->OptionIndex = i;
		Button->OnPicked = [this](int32 Index) { PickOption(Index); };
		Button->OnClicked.AddDynamic(Button, &USimpleOptionButton::HandleClicked);
		OptionsBox->AddChild(Button);
	}
}

void USimplePresenter::PickOption(int32 Index)
{
	OptionsBox->ClearChildren();
	OnOptionSelected(Index);
}
Lines 10–16 When a button is clicked, tell the Presenter which option it’s for.
Lines 18–21 Presenters don’t tick unless you turn it on. This one ticks to read the keyboard.
Lines 23–32 When dialogue starts, make the widget, find the Text Block and Vertical Box in it by name, and show it. Show the mouse cursor, so the player can click the option buttons.
Lines 34–40 When dialogue ends, remove the widget, and give the game its input back.
Lines 46–50 If the Dialogue Runner stops the options, for example because another Presenter’s option was picked first, remove the buttons.
Lines 57–61 If the line is ending for any other reason, such as a Voice Over Presenter’s clip finishing, finish with it.
Lines 63–67 When the player presses Space, ask the Dialogue Runner to move on. GetDialogueRunner() is set for you when the Presenter is in a Dialogue Runner’s Dialogue Presenters list.
Lines 70–73 RunLine is called for each line. Show the line. Line.Text includes the speaker’s name, like Rosa: Oh, hello. Then return, without finishing the line.
Lines 75–81 When something asks for the next line, here the Space press on line 66, finish the line.
Lines 83–87 Clear the text, and tell the Dialogue Runner this Presenter is done with the line.
Line 89 RunOptions is called for each set of options.
Lines 91–108 Make a button for each option the player can choose, with the option’s text on it. Each button remembers its option’s index in the array.
Lines 111–115 When the player clicks a button, remove the buttons, and tell the Dialogue Runner which option was picked.
SimplePresenter.cpp, a Presenter that shows lines in a Text Block and options as buttons.

To use it:

  1. Build your project, and open the Actor that has your Dialogue Runner.
  2. In the Components panel, click Add, and add a Simple Presenter.
  3. Select the Simple Presenter, and set its Widget Class to WBP_SimpleDialogue.
  4. Select the Dialogue Runner, and add the Simple Presenter to its Dialogue Presenters list. It handles both lines and options, so it can be the only Presenter.
BP_Dialogue (Self)
DefaultSceneRoot Scene Component
YarnDialogueRunner Yarn Dialogue Runner
SimplePresenter Simple Presenter
An example Actor with the Presenter above as its only Presenter.
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

Run Line gets the line as a Yarn Localized Line, and Can Hurry, which is always true for lines from the Dialogue Runner. The parts of the line you’ll use most:

PinC++What it is
TextLine.TextThe whole line, including the speaker’s name.
Character NameLine.CharacterNameThe speaker’s name, or empty if the line has none.
Text Without Character NameLine.TextWithoutCharacterNameThe line without the speaker’s name.
Text MarkupLine.TextMarkupThe line’s markup. See Markup.
Raw Line > Line IDLine.RawLine.LineIDThe line’s ID, like line:rosa-hello.
MetadataLine.MetadataThe line’s hashtags, without the #.
Shadow Source Line IDLine.ShadowSourceLineIDFor a shadow line, the ID of the line it shadows. Otherwise empty.
What a Presenter can read from a line.

The line is also in the Presenter’s Current Line variable until the Presenter finishes with it.

The text has already been through any marker processors, so if you’ve registered one that turns tags into Rich Text Block tags, show the text in a Rich Text Block to see the formatting. To find assets for a line, such as a portrait, ask the Dialogue Runner’s Asset Provider. See Line and Asset Providers.

Yarn Spinner adds a lastline tag to the Metadata of 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 Run Line:

  • Wait for whatever takes time before you call On Line Presentation Complete: typing, an animation, audio, or the player.
  • Call On Line Presentation Complete as soon as you can once the dialogue is moving on. The Dialogue Runner can’t move on until you do.
  • Call On Line Presentation Complete once for each line. A second call can count towards the next line, and end it early.
  • If your Presenter has nothing to show for a line, don’t override Run Line. The one it inherits calls On Line Presentation Complete straight away.

If your Presenter’s timing decides when the line ends, like a cutscene that ends the line when it finishes, call On Line Presentation Complete And End Line instead. It finishes the line for your Presenter, and asks for the line to end. The other Presenters see that through Is Next Content Requested. The Voice Over Presenter does this when a clip finishes.

Hurrying and moving on

While a line is showing, the player or your game can ask for two things:

  • Hurry up: the player wants the line to finish showing now. Skip to the end of any typing or animation, but keep the line on screen.
  • Next line: the dialogue is moving on. Tidy up and call On Line Presentation Complete.

Your Presenter finds out about these in two ways. These events are called when the request comes through the Dialogue Runner:

EventWhen it’s calledWhat the inherited version does
On Hurry Up RequestedThe player has asked for the line to hurry up.Nothing.
On Next Line RequestedThe dialogue is moving on.Calls On Line Presentation Complete, if the Presenter is showing a line.
On Options Hurry Up RequestedThe player has asked for the options to hurry up.Nothing.
Events the Dialogue Runner calls when the player wants to move on.

If you override On Next Line Requested, call On Line Presentation Complete yourself, or call the parent version. Don’t do both.

These functions tell you where things stand at any moment:

FunctionWhat it means
Is Hurry Up RequestedThe line should finish showing now. This is also true once the dialogue is moving on.
Is Next Content RequestedThe dialogue is moving on.
Is Option Hurry Up RequestedThe options should finish appearing now.
Is Option Next Content RequestedThe options have been stopped, because another Presenter’s option was picked or the dialogue is stopping.
What a Presenter can check about the current line and options.

The events are only called when the request comes through the Dialogue Runner, like this:

  • the Line Advancer gets the player’s input,
  • your own code calls Request Hurry Up, Request Next Line or Request Hurry Up Option on the Dialogue Runner, or Request Continue on a Presenter.

When the line ends for any other reason, the functions change but no event is called. That happens when:

  • a Voice Over Presenter’s clip finishes, with End Line When Voice Over Complete on,
  • another Presenter calls On Line Presentation Complete And End Line,
  • another Presenter’s option is picked.

To catch these, check Is Next Content Requested and Is Option Next Content Requested while your Presenter is waiting, as the simple Presenter above does in its tick.

The token in C++

In C++, the line and options each come with a FYarnLineCancellationToken, in CurrentLineCancellationToken and CurrentOptionsCancellationToken. The functions above read them. You can also use them directly:

FunctionWhat it means
IsHurryUpRequested()The line should finish showing now, or is ending.
IsCancellationRequested()The dialogue is moving on.
IsAnyCancellationRequested()Either of the above.
CanBeCancelled()The token belongs to a line or options that are still running.
What a Presenter can check on its token in C++.

Pass the token to anything that should finish early when the player hurries, such as the effects on Typewriter and Effects. In Blueprints, get the same token with Get Current Cancellation Token on the Dialogue Runner.

The Dialogue Runner makes a new token for each line and each set of options, and the Presenter clears its own when it calls On Line Presentation Complete or On Option Selected. Don’t keep one to use later.

A Presenter that passes lines on to other Presenters can override Internal_RunLine and Internal_RunOptions, which receive the token and the function to call when it’s done. To stop its own Presenters without ending the line for everyone, it can make a token source of its own with UYarnCancellationTokenSource::CreateLinkedTokenSource, linked to the token it was given.

Showing options

Run Options gets a Yarn Option Set. Its Options array holds a Yarn Option for each option. Each has Line, the option’s text as a Yarn Localized Line, and Is Available, which is false when the option’s condition failed.

Call On Option Selected with the index in the array of the option the player picked. To carry on without a choice, pass -1. The Options Presenter does this when every option is unavailable.

If your Presenter doesn’t show options, don’t override Run Options. The one it inherits does nothing. In C++, also override CanHandleOptions to return false. The Dialogue Runner uses it to tell whether any Presenter can show options. If none can, what happens depends on its Allow Option Fallthrough setting. See Dialogue Runner.

Other events you can use

EventWhen it’s called
On Dialogue StartedDialogue has started. Set up your UI here.
On Dialogue CompleteDialogue has ended. Hide or clear your UI here.
On Node EnterA node has started running, with its Node Name.
On Node ExitA node has finished running, with its Node Name.
On Prepare For LinesLines are about to run, with their Line IDs. Load anything they’ll need here, such as audio or images.
Events the Dialogue Runner calls on a Presenter.

Every Presenter also has the Auto Advance settings, for lines that move on by themselves after enough time to read them. Turn on Auto Advance Enabled, and call Start Auto Advance Timer when your line has finished appearing. When the timer runs out, the Presenter calls On Line Presentation Complete And End Line. The time is Auto Advance Min Delay, 2 seconds by default, plus Auto Advance Time Per Character, 0.05 by default, for each character in the line, up to Auto Advance Max Delay, 10 seconds by default. Cancel Auto Advance Timer stops it.

To fade your Presenter’s widgets in or out, use the Yarn Fade Widget In and Yarn Fade Widget Out nodes. See Typewriter and Effects.

Next step A Different Kind of Presenter A second custom Presenter, to show how differently a Presenter can work.