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!
===
Lines 1–11 Two nodes that can always run.
Lines 13–17 Can only run while the lamp is out.
Lines 19–23 Can only run while the lamp is lit.
Lines 25–30 Can only run while the lamp is lit, and only once.
A node group called Rosa. Talking to Rosa runs one of these five nodes.

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?
===
Line 3 A line with no condition. It can always be picked.
Lines 4–5 Lines with a condition. Each can only be picked while its condition is true.
Line 6 An ordinary line. It’s shown after whichever line was picked.
A line group at the start of a node. Rosa says one of the three lines, then carries on.

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, or or xor in it. once adds 1, and always adds 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 --> none
What happens when the dialogue reaches a node group or a line group.

If 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>>
Only offer the option if Rosa has something to say.

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 StrategyPicks
Random Best Least Recently ViewedFrom 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 ViewedThe same as Random Best Least Recently Viewed, but takes the first one instead of a random one.
BestThe candidate with the highest complexity. If there’s a tie, the first one.
FirstThe first candidate that passes.
The Dialogue Runner’s Saliency Strategy options.
The Behaviour section of a Dialogue Runner with the Saliency Strategy dropdown open, listing First, Best, Best Least Recently Viewed and Random Best Least Recently Viewed.
The Dialogue Runner’s saliency strategies.

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;
Changing to the Best strategy from C++.

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());
}
Logging each candidate in the Rosa node group.

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:

MemberWhat it is
ContentIDThe 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.
ComplexityScoreThe candidate’s complexity.
PassingConditionCountHow many of its conditions are true.
FailingConditionCountHow many of its conditions are false. The candidate can only run if this is 0.
ContentTypeEYarnSaliencyContentType::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.
The members of FYarnSaliencyCandidate.

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"));
Asking the current strategy whether it would pick a candidate.

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);
Reading how many times a candidate has been 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;
};
Lines 19–23 The Actor that has the Yarn Dialogue Runner on it, and the name of the node group. Set them in the Details panel.
Lines 25–26 The icon to show. Give it a mesh in the Details panel.
TalkIndicator.h.
#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);
}
Line 18 Show the icon if there’s something to say, and hide it if not.
Lines 23–27 Hide the icon while a conversation is running, or if there’s no Dialogue Runner or Yarn Project.
Line 29 Show the icon if the saliency strategy would pick something from the node group.
TalkIndicator.cpp.

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.

Next step Custom Saliency Strategies Writing your own way to pick content from node groups and line groups.