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 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:
- In the Content Browser, click Add, and choose . Pick User Widget, and name it
WBP_SimpleDialogue. - Add a Vertical Box. Inside it, add a Text Block (Text in the Palette) named
LineText, and another Vertical Box namedOptionsBox.
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:
- 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. - 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.
- 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.
- 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.
- 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.
- Compile and save.
Then make the Presenter:
- 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 itBP_SimplePresenter, and open it. The top right of its editor should say Parent class: Yarn Dialogue Presenter. - 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.
- 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.
- Drag from its exec pin and add Create Widget. Set Class to
- 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.
- 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.
- 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. - On Event Run Options, make a button for each option the player can choose:
- Right-click the event’s Options pin and choose Split Struct Pin. It becomes Options Options, the array of options.
- 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.
- Right-click Array Element and choose Split Struct Pin. You now have Array Element Line, Array Element Is Available and Array Element Option ID.
- 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.
- 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. - 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.
- 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.
- 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.
- On Event On Dialogue Complete, get Dialogue Widget and add Remove from Parent.
- Compile and save.
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);
};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);
}GetDialogueRunner() is set for you when the Presenter is in a Dialogue Runner’s Dialogue Presenters list.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.RunOptions is called for each set of options.To use it:
- Build your project, and open the Actor that has your Dialogue Runner.
- In the Components panel, click Add, and add a Simple Presenter.
- Select the Simple Presenter, and set its Widget Class to
WBP_SimpleDialogue. - 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.
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:
| Pin | C++ | What it is |
|---|---|---|
| Text | Line.Text | The whole line, including the speaker’s name. |
| Character Name | Line.CharacterName | The speaker’s name, or empty if the line has none. |
| Text Without Character Name | Line.TextWithoutCharacterName | The line without the speaker’s name. |
| Text Markup | Line.TextMarkup | The line’s markup. See Markup. |
| Raw Line > Line ID | Line.RawLine.LineID | The line’s ID, like line:rosa-hello. |
| Metadata | Line.Metadata | The line’s hashtags, without the #. |
| Shadow Source Line ID | Line.ShadowSourceLineID | For a shadow line, the ID of the line it shadows. Otherwise empty. |
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:
| Event | When it’s called | What the inherited version does |
|---|---|---|
| On Hurry Up Requested | The player has asked for the line to hurry up. | Nothing. |
| On Next Line Requested | The dialogue is moving on. | Calls On Line Presentation Complete, if the Presenter is showing a line. |
| On Options Hurry Up Requested | The player has asked for the options to hurry up. | Nothing. |
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:
| Function | What it means |
|---|---|
| Is Hurry Up Requested | The line should finish showing now. This is also true once the dialogue is moving on. |
| Is Next Content Requested | The dialogue is moving on. |
| Is Option Hurry Up Requested | The options should finish appearing now. |
| Is Option Next Content Requested | The options have been stopped, because another Presenter’s option was picked or the dialogue is stopping. |
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:
| Function | What 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. |
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
| Event | When it’s called |
|---|---|
| On Dialogue Started | Dialogue has started. Set up your UI here. |
| On Dialogue Complete | Dialogue has ended. Hide or clear your UI here. |
| On Node Enter | A node has started running, with its Node Name. |
| On Node Exit | A node has finished running, with its Node Name. |
| On Prepare For Lines | Lines are about to run, with their Line IDs. Load anything they’ll need here, such as audio or images. |
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.