Saliency and Storylets
Sometimes a Yarn Script offers several pieces of content that could run at the same point, and only one of them should. There are two ways to write this:
- A node group is several nodes with the same title, each with
when:headers that say when it can run. Nodes like these are often called storylets. - A line group is a set of lines that start with
=>, each with an optional condition.
When the dialogue reaches a node group or a line group, the Dialogue Runner works out which pieces of content can run, and asks its saliency strategy to pick one. To write your own strategy, see Custom Saliency Strategies. For how to write node groups and line groups, see the Storylets and Saliency Primer.
title: Rosa
when: always
---
Rosa: Hello again.
===
title: Rosa
when: always
---
Rosa: Oh, it's you.
===
title: Rosa
when: not $lamp_lit
---
Rosa: Who's there? It's too dark.
===
title: Rosa
when: $lamp_lit
---
Rosa: Ah, I can see you now.
===
title: Rosa
when: once
when: $lamp_lit
---
Rosa: Thanks for lighting the lamp!
===To run a node group, <<jump>> or <<detour>> to its name, or start the dialogue there with Start Dialogue, the same as any other node.
A line group does the same job inside a node, for a single line. When the dialogue reaches it, one of its lines is shown, the others are skipped, and the dialogue carries on. It’s useful for small variations, like a greeting, where a whole node for each one would be too much:
title: RosaGreeting
---
=> Rosa: Evening.
=> Rosa: Mind the step, it's dark. <<if not $lamp_lit>>
=> Rosa: Nice and bright in here. <<if $lamp_lit>>
Rosa: What can I do for you?
===Node groups and line groups are picked the same way.
How one is picked
Each piece of content that could run is a candidate. The Dialogue Runner tracks three things for each one:
- Whether it passes: a candidate passes if all its conditions are true. Only candidates that pass can be picked.
- Its complexity: each condition adds 1, and so does each
and,ororxorin it.onceadds 1, andalwaysadds nothing. A candidate with a higher complexity has more specific conditions. - How many times it’s been seen: the default strategy counts how many times it has picked each candidate.
The saliency strategy uses these to pick one candidate that passes.
flowchart TD
reach["The dialogue reaches a<br>node group or line group"]
check["Check each candidate's conditions"]
pick{"Do any pass?"}
strategy["The saliency strategy<br>picks one"]
run["That candidate runs"]
none["Nothing from the group runs"]
reach --> check --> pick
pick -- yes --> strategy --> run
pick -- no --> noneIf nothing passes, nothing from the group runs. After a <<detour>> to a node group, the dialogue carries on after the <<detour>>. After a <<jump>> to one, the dialogue ends.
To check first, use the has_any_content() Function in Yarn. It returns true if something in the node group can run:
-> Talk to Rosa <<if has_any_content("Rosa")>>
<<detour Rosa>>From Blueprints and C++, use the Dialogue Runner’s Has Salient Content. See Looking at a node group from Blueprints and C++.
Choosing a strategy
Set the Dialogue Runner’s Saliency Strategy in the Details panel, in the Behaviour section under Yarn Spinner:
| Saliency Strategy | Picks |
|---|---|
| Random Best Least Recently Viewed | From the candidates that pass, the ones seen the fewest times. Of those, the ones with the highest complexity. Then one of those at random. This is the default. |
| Best Least Recently Viewed | The same as Random Best Least Recently Viewed, but takes the first one instead of a random one. |
| Best | The candidate with the highest complexity. If there’s a tie, the first one. |
| First | The first candidate that passes. |

To use your own strategy, give it to the Dialogue Runner with Set Saliency Strategy. See Custom Saliency Strategies.
Only the two Least Recently Viewed strategies count how many times each candidate has been seen. They keep the counts in the Variable Storage, in variables named $Yarn.Internal.Content.ViewCount. followed by the candidate’s ID, so the counts are saved and loaded along with your other variables.
Saliency Strategy can be changed from Blueprints and C++ too. The Dialogue Runner makes a new strategy from it each time the dialogue starts, so a change is used from the next time Start Dialogue is called:
In Blueprints, use the setter for the Saliency Strategy property, which has a Saliency Strategy pin. The Set Saliency Strategy function, with an In Strategy pin, is for your own strategies.
DialogueRunner->SaliencyStrategy = EYarnSaliencyStrategy::Best;Once you’ve given the Dialogue Runner your own strategy with the Set Saliency Strategy function, the Saliency Strategy property isn’t used until you call Set Saliency Strategy again with nothing connected to In Strategy.
Looking at a node group from Blueprints and C++
The Dialogue Runner’s Get Saliency Options For Node Group gives you the candidates in a node group, as they are right now. Use it to show which storylets are available, or to debug why one isn’t running. It doesn’t change any variables. In C++, it’s GetSaliencyOptionsForNodeGroup().
#include "YarnDialogueRunner.h"
TArray<FYarnSaliencyCandidate> Candidates = DialogueRunner->GetSaliencyOptionsForNodeGroup(TEXT("Rosa"));
for (const FYarnSaliencyCandidate& Candidate : Candidates)
{
UE_LOG(LogTemp, Log, TEXT("%s passes: %d"), *Candidate.ContentID, Candidate.IsViable());
}It returns no candidates, and logs a warning, if there’s no node with that name. If you give it the name of an ordinary node, it returns one candidate for that node, which always passes.
The when: conditions are checked with the same Functions the dialogue uses, including any you’ve added. It needs the Dialogue Runner to have begun play. Before then, it returns no candidates and logs a warning.
Each candidate is an FYarnSaliencyCandidate, from YarnSaliency.h:
| Member | What it is |
|---|---|
ContentID | The candidate’s ID. In a node group, it’s a name that Yarn Spinner makes for the node, such as Rosa.3d371ffd. In a line group, it’s the line’s ID. |
ComplexityScore | The candidate’s complexity. |
PassingConditionCount | How many of its conditions are true. |
FailingConditionCount | How many of its conditions are false. The candidate can only run if this is 0. |
ContentType | EYarnSaliencyContentType::Node for a node, or EYarnSaliencyContentType::Line for a line. |
IsViable() | true if the candidate passes, which is when FailingConditionCount is 0. |
GetViewCountKey() | The name of the variable the Least Recently Viewed strategies use to count how many times the candidate has been seen. |
In Blueprints, a Yarn Saliency Candidate has Content ID, Passing Condition Count, Failing Condition Count, Complexity Score and Content Type.
To find out whether the saliency strategy would pick something, the way has_any_content() does, use the Dialogue Runner’s Has Salient Content, with the node group’s name in Node Group. It gives the candidates to the Dialogue Runner’s current strategy, and returns true if the strategy would pick one. In C++, it’s HasSalientContent():
bool bHasContent = DialogueRunner->HasSalientContent(TEXT("Rosa"));To find out how many times a candidate has been seen, read its view count from the Variable Storage:
FYarnValue Seen;
IYarnVariableStorage::Execute_TryGetValue(DialogueRunner->VariableStorage.GetObject(), Candidate.GetViewCountKey(), Seen);An example
This Actor shows an icon while a character has something to say. It checks the node group every frame, so the icon appears and disappears as variables change.
#pragma once
#include "CoreMinimal.h"
#include "GameFramework/Actor.h"
#include "TalkIndicator.generated.h"
class UStaticMeshComponent;
UCLASS()
class ATalkIndicator : public AActor
{
GENERATED_BODY()
public:
ATalkIndicator();
virtual void Tick(float DeltaSeconds) override;
UPROPERTY(EditAnywhere, Category = "Talk Indicator")
TObjectPtr<AActor> DialogueActor;
UPROPERTY(EditAnywhere, Category = "Talk Indicator")
FString NodeGroup = TEXT("Rosa");
UPROPERTY(VisibleAnywhere, Category = "Talk Indicator")
TObjectPtr<UStaticMeshComponent> Icon;
private:
bool HasSomethingToSay() const;
};#include "TalkIndicator.h"
#include "Components/StaticMeshComponent.h"
#include "YarnBlueprintLibrary.h"
#include "YarnDialogueRunner.h"
ATalkIndicator::ATalkIndicator()
{
PrimaryActorTick.bCanEverTick = true;
Icon = CreateDefaultSubobject<UStaticMeshComponent>(TEXT("Icon"));
RootComponent = Icon;
}
void ATalkIndicator::Tick(float DeltaSeconds)
{
Super::Tick(DeltaSeconds);
Icon->SetVisibility(HasSomethingToSay());
}
bool ATalkIndicator::HasSomethingToSay() const
{
UYarnDialogueRunner* Runner = UYarnBlueprintLibrary::GetDialogueRunner(DialogueActor);
if (!Runner || !Runner->YarnProject || Runner->IsDialogueRunning())
{
return false;
}
return Runner->HasSalientContent(NodeGroup);
}Writing your own strategy
If none of the built-in strategies pick content the way you want, you can write your own, in C++ or in Blueprints. See Custom Saliency Strategies.