Custom Saliency Strategies
When the dialogue reaches a node group or a line group, the Dialogue Runner asks its saliency strategy to pick one candidate. Saliency and Storylets covers the strategies that come with Yarn Spinner. If none of them pick the way you want, you can write your own, in C++ or in Blueprints.
What a strategy does
A saliency strategy is an object that implements the Yarn Saliency Strategy interface (IYarnSaliencyStrategy, from YarnSaliency.h). It has two functions:
- Query Best Content is given every candidate in Candidates, including the ones that don’t pass. It sets Out Selected Candidate to the one to run, and returns true. It returns false if nothing should run.
- Content Was Selected is called with Selected Candidate after a candidate has been picked, before it runs.
sequenceDiagram participant Runner as Dialogue Runner participant Strategy as Your strategy Runner->>Strategy: Query Best Content(Candidates) Strategy-->>Runner: the candidate to run, or false Runner->>Strategy: Content Was Selected(Selected Candidate) Note over Runner: The picked candidate runs
Each candidate is an FYarnSaliencyCandidate, with the members described in Looking at a node group from Blueprints and C++. Query Best Content must give back one of the candidates it was given, or return false. If it gives back a candidate with any other Content ID, the Dialogue Runner logs an error and the dialogue stops.
Give back a copy of the candidate from the array, not a new one. A candidate also holds where the dialogue goes when it’s picked, which Blueprints can’t see, so a candidate you make yourself sends the dialogue to the wrong place.
The strategy isn’t given anything else. If it needs more, such as the Variable Storage or the Yarn Project, give it to the strategy when you create it. In C++, YarnProject->Program.GetNode(ContentID) returns a node, and the node’s GetHeaderValue(HeaderName) reads one of its headers. YarnProject->LineMetadata holds each line’s tags, by line ID, separated by spaces.
An example
This strategy picks at random, but never picks the same candidate twice in a row:
- In the Content Browser, create a Blueprint Class with Object as its parent class. Call it
BP_NoRepeatSaliencyStrategy. - Open it, and in Class Settings, add Yarn Saliency Strategy under Interfaces.
- In the My Blueprint panel, under Interfaces, open Query Best Content. From Candidates, keep the candidates whose Failing Condition Count is 0. If there are none, return false. Otherwise pick one with Random Integer in Range, set Out Selected Candidate to that element of the array, and return true.
- Add the Event Content Was Selected event, and store the Content ID of Selected Candidate in a variable, so Query Best Content can leave it out next time.
#pragma once
#include "CoreMinimal.h"
#include "UObject/Object.h"
#include "YarnSaliency.h"
#include "NoRepeatSaliencyStrategy.generated.h"
UCLASS()
class UNoRepeatSaliencyStrategy : public UObject, public IYarnSaliencyStrategy
{
GENERATED_BODY()
public:
virtual bool QueryBestContent_Implementation(const TArray<FYarnSaliencyCandidate>& Candidates, FYarnSaliencyCandidate& OutSelectedCandidate) override;
virtual void ContentWasSelected_Implementation(const FYarnSaliencyCandidate& SelectedCandidate) override;
private:
FString LastPicked;
};UObject that implements the interface. It doesn’t need to be a component.#include "NoRepeatSaliencyStrategy.h"
bool UNoRepeatSaliencyStrategy::QueryBestContent_Implementation(const TArray<FYarnSaliencyCandidate>& Candidates, FYarnSaliencyCandidate& OutSelectedCandidate)
{
TArray<FYarnSaliencyCandidate> Valid = Candidates.FilterByPredicate([](const FYarnSaliencyCandidate& Candidate)
{
return Candidate.IsViable();
});
if (Valid.Num() == 0)
{
return false;
}
TArray<FYarnSaliencyCandidate> Choices = Valid.FilterByPredicate([this](const FYarnSaliencyCandidate& Candidate)
{
return Candidate.ContentID != LastPicked;
});
if (Choices.Num() == 0)
{
Choices = Valid;
}
OutSelectedCandidate = Choices[FMath::RandRange(0, Choices.Num() - 1)];
return true;
}
void UNoRepeatSaliencyStrategy::ContentWasSelected_Implementation(const FYarnSaliencyCandidate& SelectedCandidate)
{
LastPicked = SelectedCandidate.ContentID;
}Code that only checks whether anything can run, such as has_any_content(), calls Query Best Content too, and nothing runs afterwards. Don’t change any state in Query Best Content. Make changes in Content Was Selected, which is only called when a candidate is about to run.
If your strategy uses view counts, like the built-in Least Recently Viewed strategies, keep them in the Variable Storage so they’re saved with your other variables. In C++, each candidate’s GetViewCountKey() returns a variable name you can use for its count. In Blueprints, build the same name by adding the candidate’s Content ID to the end of $Yarn.Internal.Content.ViewCount.. Read the count with Try Get Value in Query Best Content, and add 1 to it with Set Number in Content Was Selected.
The same strategy is used for node groups and line groups. Use Content Type if it needs to treat them differently.
Using your strategy
Give the Dialogue Runner your strategy with its Set Saliency Strategy function. It replaces the strategy chosen in Saliency Strategy in the Details panel, and the Dialogue Runner keeps using it every time the dialogue starts.
In Blueprints, from Event BeginPlay:
- Add a Construct Object from Class node. Set Class to
BP_NoRepeatSaliencyStrategy, and connect Self to Outer. - Drag from your Yarn Dialogue Runner component and add Set Saliency Strategy. Connect the new object to In Strategy.
In C++, call it from your Actor’s BeginPlay, after Super::BeginPlay():
void AStoryActor::BeginPlay()
{
Super::BeginPlay();
DialogueRunner->SetSaliencyStrategy(NewObject<UNoRepeatSaliencyStrategy>(this));
}The Dialogue Runner keeps the strategy alive, so you don’t need to keep your own reference to it.
BeginPlay has run. Before then, the Dialogue Runner has nothing to give the strategy to, and the call does nothing. An Actor’s Event BeginPlay runs after its components’ BeginPlay, so it’s a safe place to call it from.To go back to the strategy chosen in Saliency Strategy, call Set Saliency Strategy with nothing connected to In Strategy, or SetSaliencyStrategy(nullptr) in C++.