Variables and Storage
Yarn variables, like $trust or $visitor_name, hold values that your dialogue can check and change. They’re kept in a Variable Storage, which your Yarn Scripts and your game share. Yarn Scripts set variables with <<set>>, check them in <<if>>, and show them in lines with {$visitor_name}. Your game’s Blueprints and C++ read and change the same variables, and can be told when they change.
A variable set by one side is seen by the other. Your game can set $visitor_name before a conversation, and the dialogue uses it. The dialogue can set $lamp_fixed, and your game lights the lamp.
flowchart LR
yarn["Your Yarn Scripts<br>#60;#60;set#62;#62;, #60;#60;if#62;#62;, {$variable}"]
storage[("Variable Storage<br>$trust = 2<br>$visitor_name = Sam")]
game["Your Blueprints and C++<br>Set Number Variable, Get Variable As Number,<br>Add Number Change Listener"]
save["Save slot in<br>Saved/SaveGames"]
yarn <-->|read and change| storage
game <-->|read and change| storage
storage <-.->|save and load| save<<declare $trust = 0>>
<<declare $lamp_fixed = false>>
<<declare $visitor_name = "stranger">>
Rosa: Hello, {$visitor_name}.
<<set $trust to $trust + 2>>
<<if $trust >= 2>>
Rosa: Thanks for the help.
<<endif>>Where variables are kept
Each Dialogue Runner has a Variable Storage. If you leave that setting empty, the Dialogue Runner makes a Yarn In Memory Variable Storage when it begins play. It keeps variables in memory while the game runs, and they’re lost when the game closes, unless you save them.
When something asks for a variable’s value, the Yarn In Memory Variable Storage looks in three places, in this order:
- the value it’s storing, if the variable has been set,
- the variable’s expression, if it’s a smart variable,
- the default value from the variable’s
<<declare>>in Yarn.
Reading a variable before it’s been set gives you its declared default.
To use a different Variable Storage, like your own, add it as a component on the Dialogue Runner’s Actor and set the Dialogue Runner’s Variable Storage to it before the Dialogue Runner begins play. In Blueprints, do this in the Actor’s Construction Script: drag in the Yarn Dialogue Runner, and call Set Variable Storage with your Variable Storage component. In C++, do it in your Actor’s PostInitializeComponents():
void AGameDialogueActor::PostInitializeComponents()
{
Super::PostInitializeComponents();
UYarnDialogueRunner* DialogueRunner = FindComponentByClass<UYarnDialogueRunner>();
DialogueRunner->VariableStorage = FindComponentByClass<USaveDataVariableStorage>();
}Reading and changing variables from Blueprints and C++
The Yarn Spinner nodes for variables take the Dialogue Runner and the variable’s name, including the $. They’re under Yarn Spinner > Variables when you right-click in a Blueprint graph:
| Node | What it does |
|---|---|
| Set Number Variable, Set String Variable, Set Bool Variable | Sets a variable to the Value you give it. |
| Get Variable As Number, Get Variable As Bool | Puts the variable’s value in Out Value. Return Value is true if the variable was found and has that type. |
| Get Variable As String | Puts the variable’s value in Out Value as text, whatever its type. Return Value is true if the variable was found. |
| Has Variable | True if the variable has a value, including a declared default. |
| Get All Variable Names, Get All Variables | The names, or names and values, of every variable that’s been set. |
The same functions are in UYarnBlueprintLibrary, so you can call them from C++:
#include "YarnBlueprintLibrary.h"
UYarnBlueprintLibrary::SetStringVariable(DialogueRunner, TEXT("$visitor_name"), TEXT("Sam"));
float Trust = 0.0f;
bool LampFixed = false;
FString Visitor;
UYarnBlueprintLibrary::GetVariableAsNumber(DialogueRunner, TEXT("$trust"), Trust);
UYarnBlueprintLibrary::GetVariableAsBool(DialogueRunner, TEXT("$lamp_fixed"), LampFixed);
UYarnBlueprintLibrary::GetVariableAsString(DialogueRunner, TEXT("$visitor_name"), Visitor);$.float, bool or FString. A variable that hasn’t been set gives its default from <<declare>>. Each returns true if it found the variable.You can also use the Variable Storage directly, through the IYarnVariableStorage interface. Its functions are Blueprint events, so in C++ call them through the interface’s Execute_ functions, not on the interface itself:
UObject* Storage = DialogueRunner->VariableStorage.GetObject();
IYarnVariableStorage::Execute_SetNumber(Storage, TEXT("$trust"), 2.0f);
FYarnValue Trust;
if (IYarnVariableStorage::Execute_TryGetValue(Storage, TEXT("$trust"), Trust))
{
float TrustValue = Trust.ConvertToNumber();
}Yarn variables can only hold numbers, text, and true or false. Yarn numbers are kept as float, so a whole number you set comes back as a float. A variable’s name has to start with $. If it doesn’t, the Yarn In Memory Variable Storage logs a warning and doesn’t store it.
The Dialogue Runner makes its default Variable Storage when it begins play, so variables can’t be set before then. To give a variable a value the dialogue can use from its first line, turn off the Dialogue Runner’s Auto Start, set the variable in Event BeginPlay or BeginPlay(), and then call Start Dialogue.
Reacting when a variable changes
The Yarn In Memory Variable Storage can tell you when a variable changes. That includes changes made by <<set>> in Yarn and changes made by your own Blueprints or C++.
To keep something on screen up to date, like a counter, add a change listener with Add Number Change Listener, Add String Change Listener or Add Bool Change Listener. Your event gets the variable’s name and its new value, as that type:
- After Event BeginPlay, get the Yarn Dialogue Runner’s Variable Storage, and cast it to Yarn In Memory Variable Storage.
- Call Add Number Change Listener on it, with Variable Name set to
$trust. - Drag from the Callback pin and add a custom event. It has two inputs: Variable Name and New Value, a Float. Update your counter from New Value.
- Save the node’s Return Value, a Yarn Variable Listener Handle, in a variable. In Event End Play, pass it to Remove Change Listener.
In C++, bind the callback to a UFUNCTION:
UYarnInMemoryVariableStorage* Storage = Cast<UYarnInMemoryVariableStorage>(DialogueRunner->VariableStorage.GetObject());
FOnYarnNumberVariableChanged Callback;
Callback.BindDynamic(this, &AVariablesDemo::OnTrustChanged);
TrustListener = Storage->AddNumberChangeListener(TEXT("$trust"), Callback);
float Trust = 0.0f;
UYarnBlueprintLibrary::GetVariableAsNumber(DialogueRunner, TEXT("$trust"), Trust);
OnTrustChanged(TEXT("$trust"), Trust);OnTrustChanged with the new value whenever $trust changes. Keep the handle, so you can stop listening with Storage->RemoveChangeListener(TrustListener).A Number listener only hears about number values, a String listener about text, and a Bool listener about true or false.
To hear about every variable, bind to the Yarn In Memory Variable Storage’s On Variable Changed event. It passes Variable Name and New Value, a Yarn Value. In C++, that’s Storage->OnVariableChanged.AddDynamic().
Smart variables don’t change by being set, so change listeners and On Variable Changed never hear about them.
Saving and loading
Yarn Spinner can save every variable in a Dialogue Runner’s Variable Storage to a save slot, using Unreal’s save game system, and load them back. The nodes are under Yarn Spinner > Persistence:
| Node | What it does |
|---|---|
| Save Variables To Slot | Saves every variable that’s been set to the slot. |
| Load Variables From Slot | Clears the Variable Storage, and loads the variables from the slot. |
| Does Variable Slot Exist | True if the slot has been saved to. |
| Delete Variable Slot | Deletes the slot. |
Each takes a Slot Name, YarnVariables unless you change it, and a User Index, 0 unless you change it. Use a different slot name for each save slot, like YarnSave_Slot1 and YarnSave_Slot2. The slots are saved in your project’s Saved/SaveGames folder while you work in the editor.
UYarnBlueprintLibrary::SaveVariablesToSlot(DialogueRunner, TEXT("YarnSave_Slot1"), 0);
UYarnBlueprintLibrary::LoadVariablesFromSlot(DialogueRunner, TEXT("YarnSave_Slot1"), 0);Both return true if they worked. Loading sets each variable, so change listeners hear about the loaded values. The save includes the values Yarn Spinner uses to track which nodes have been run, so visited() gives the same answers after loading.
Saving and loading only work with a Yarn In Memory Variable Storage, or a class based on it. With any other Variable Storage, both return false.
Only the variables are saved, not where the dialogue is up to. To pick a conversation up again, save the name of the node you want to start from, and pass it to Start Dialogue after loading.
To save Yarn variables as part of your game’s own save file, call Get All Variables on the Yarn In Memory Variable Storage. It gives you three maps of every variable that’s been set: Out Floats for numbers, Out Strings for text and Out Bools for true or false. Put them back with Set All Variables, which clears the Variable Storage first unless you untick Clear First. To remove every variable, call Clear.
Seeing variables while you test
The Yarn In Memory Variable Storage’s Get Debug String node returns every variable, one per line, like $trust = 2 (Number). Pass it to Print String to see it on screen and in the Output Log.
It only lists variables that have been set, not ones still at their declared default.
Writing your own Variable Storage
To keep Yarn variables somewhere else, write a C++ class that implements the IYarnVariableStorage interface. This one is a component that keeps them in a map, marked SaveGame so that your game’s own save system can write it along with everything else:
#pragma once
#include "CoreMinimal.h"
#include "Components/ActorComponent.h"
#include "YarnVariableStorage.h"
#include "SaveDataVariableStorage.generated.h"
UCLASS(ClassGroup = (Custom), meta = (BlueprintSpawnableComponent))
class USaveDataVariableStorage : public UActorComponent, public IYarnVariableStorage
{
GENERATED_BODY()
public:
UPROPERTY(SaveGame)
TMap<FString, FYarnValue> YarnVariables;
virtual void SetString_Implementation(const FString& VariableName, const FString& Value) override;
virtual void SetNumber_Implementation(const FString& VariableName, float Value) override;
virtual void SetBool_Implementation(const FString& VariableName, bool Value) override;
virtual void SetValue_Implementation(const FString& VariableName, const FYarnValue& Value) override;
virtual bool TryGetValue_Implementation(const FString& VariableName, FYarnValue& OutValue) override;
virtual bool Contains_Implementation(const FString& VariableName) override;
virtual void Clear_Implementation() override;
virtual void SetYarnProject(UYarnProject* Project) override;
virtual IYarnSmartVariableEvaluator* GetSmartVariableEvaluator() const override;
virtual void SetSmartVariableEvaluator(IYarnSmartVariableEvaluator* Evaluator, UObject* EvaluatorOwner) override;
private:
UPROPERTY()
TObjectPtr<UYarnProject> YarnProject;
IYarnSmartVariableEvaluator* SmartVariableEvaluator = nullptr;
};IYarnVariableStorage, so you can add it to an Actor.SaveGame marks the map for Unreal’s save game serialisation.#include "SaveDataVariableStorage.h"
#include "YarnSmartVariables.h"
void USaveDataVariableStorage::SetString_Implementation(const FString& VariableName, const FString& Value)
{
SetValue_Implementation(VariableName, FYarnValue(Value));
}
void USaveDataVariableStorage::SetNumber_Implementation(const FString& VariableName, float Value)
{
SetValue_Implementation(VariableName, FYarnValue(Value));
}
void USaveDataVariableStorage::SetBool_Implementation(const FString& VariableName, bool Value)
{
SetValue_Implementation(VariableName, FYarnValue(Value));
}
void USaveDataVariableStorage::SetValue_Implementation(const FString& VariableName, const FYarnValue& Value)
{
YarnVariables.Add(VariableName, Value);
}
bool USaveDataVariableStorage::TryGetValue_Implementation(const FString& VariableName, FYarnValue& OutValue)
{
if (const FYarnValue* Stored = YarnVariables.Find(VariableName))
{
OutValue = *Stored;
return true;
}
if (SmartVariableEvaluator && SmartVariableEvaluator->TryGetSmartVariable(VariableName, OutValue))
{
return true;
}
return YarnProject && YarnProject->TryGetInitialValue(VariableName, OutValue);
}
bool USaveDataVariableStorage::Contains_Implementation(const FString& VariableName)
{
return YarnVariables.Contains(VariableName);
}
void USaveDataVariableStorage::Clear_Implementation()
{
YarnVariables.Empty();
}
void USaveDataVariableStorage::SetYarnProject(UYarnProject* Project)
{
YarnProject = Project;
}
IYarnSmartVariableEvaluator* USaveDataVariableStorage::GetSmartVariableEvaluator() const
{
return SmartVariableEvaluator;
}
void USaveDataVariableStorage::SetSmartVariableEvaluator(IYarnSmartVariableEvaluator* Evaluator, UObject* EvaluatorOwner)
{
SmartVariableEvaluator = Evaluator;
}SetValue is called whenever the Yarn Script sets a variable with <<set>>, and by the Set … Variable nodes.TryGetValue returns a variable’s value: the stored value if it’s been set, then a smart variable’s value, then the declared default from the Yarn Project.Contains says whether a variable has been set, and Clear removes every variable.To use it, add a Save Data Variable Storage component to the Dialogue Runner’s Actor, and set it as the Dialogue Runner’s Variable Storage, as in Where variables are kept.
A Variable Storage needs those ten functions. Change listeners, Get All Variables and the Save Variables To Slot and Load Variables From Slot nodes only work with the Yarn In Memory Variable Storage. If you need them, base your class on UYarnInMemoryVariableStorage instead, and override the functions you want to change.
An example
Before the dialogue starts, this Actor sets $visitor_name to “Sam”. Rosa greets the player by name, and the player’s choice raises $trust, which is shown on screen. Setting $lamp_fixed lights the lamp, and the variables are saved when the dialogue ends.
title: VariablesDemo
---
<<declare $trust = 0>>
<<declare $lamp_fixed = false>>
<<declare $visitor_name = "stranger">>
Rosa: Hello, {$visitor_name}. What brings you here?
-> I can help with the lamp.
<<set $trust to $trust + 2>>
Rosa: That's kind of you.
-> Just looking around.
<<set $trust to $trust + 1>>
Rosa: Mind the stairs, then.
Rosa: Let me get the lamp going.
<<set $lamp_fixed to true>>
<<if $trust >= 2>>
Rosa: Thanks for the help, {$visitor_name}.
<<else>>
Rosa: See you around, {$visitor_name}.
<<endif>>
===#pragma once
#include "CoreMinimal.h"
#include "GameFramework/Actor.h"
#include "YarnVariableStorage.h"
#include "VariablesDemo.generated.h"
class UYarnDialogueRunner;
class UPointLightComponent;
UCLASS()
class AVariablesDemo : public AActor
{
GENERATED_BODY()
protected:
virtual void BeginPlay() override;
virtual void EndPlay(const EEndPlayReason::Type EndPlayReason) override;
private:
UPROPERTY()
TObjectPtr<UYarnDialogueRunner> DialogueRunner;
UPROPERTY()
TObjectPtr<UYarnInMemoryVariableStorage> Storage;
UPROPERTY()
TObjectPtr<UPointLightComponent> LampLight;
FYarnVariableListenerHandle TrustListener;
FYarnVariableListenerHandle LampListener;
UFUNCTION()
void OnTrustChanged(const FString& VariableName, float NewValue);
UFUNCTION()
void OnLampFixedChanged(const FString& VariableName, bool NewValue);
UFUNCTION()
void OnDialogueComplete();
};UFUNCTIONs, because change listeners and Dialogue Runner events are dynamic delegates.#include "VariablesDemo.h"
#include "Components/PointLightComponent.h"
#include "Engine/Engine.h"
#include "YarnBlueprintLibrary.h"
#include "YarnDialogueRunner.h"
void AVariablesDemo::BeginPlay()
{
Super::BeginPlay();
DialogueRunner = FindComponentByClass<UYarnDialogueRunner>();
LampLight = FindComponentByClass<UPointLightComponent>();
Storage = Cast<UYarnInMemoryVariableStorage>(DialogueRunner->VariableStorage.GetObject());
UYarnBlueprintLibrary::SetStringVariable(DialogueRunner, TEXT("$visitor_name"), TEXT("Sam"));
FOnYarnNumberVariableChanged TrustCallback;
TrustCallback.BindDynamic(this, &AVariablesDemo::OnTrustChanged);
TrustListener = Storage->AddNumberChangeListener(TEXT("$trust"), TrustCallback);
float Trust = 0.0f;
UYarnBlueprintLibrary::GetVariableAsNumber(DialogueRunner, TEXT("$trust"), Trust);
OnTrustChanged(TEXT("$trust"), Trust);
FOnYarnBoolVariableChanged LampCallback;
LampCallback.BindDynamic(this, &AVariablesDemo::OnLampFixedChanged);
LampListener = Storage->AddBoolChangeListener(TEXT("$lamp_fixed"), LampCallback);
DialogueRunner->OnDialogueComplete.AddDynamic(this, &AVariablesDemo::OnDialogueComplete);
DialogueRunner->StartDialogue(TEXT("VariablesDemo"));
}
void AVariablesDemo::EndPlay(const EEndPlayReason::Type EndPlayReason)
{
if (Storage)
{
Storage->RemoveChangeListener(TrustListener);
Storage->RemoveChangeListener(LampListener);
}
Super::EndPlay(EndPlayReason);
}
void AVariablesDemo::OnTrustChanged(const FString& VariableName, float NewValue)
{
if (GEngine)
{
GEngine->AddOnScreenDebugMessage(1, 30.0f, FColor::White, FString::Printf(TEXT("Trust: %.0f"), NewValue));
}
}
void AVariablesDemo::OnLampFixedChanged(const FString& VariableName, bool NewValue)
{
LampLight->SetIntensity(NewValue ? 5000.0f : 0.0f);
}
void AVariablesDemo::OnDialogueComplete()
{
UYarnBlueprintLibrary::SaveVariablesToSlot(DialogueRunner, TEXT("VariablesDemo"), 0);
}$visitor_name before the dialogue starts, so Rosa’s first line uses it.$lamp_fixed changes.$visitor_name is set.$trust, and shows it on screen.$lamp_fixed, and turns the lamp’s light on when it’s true.VariablesDemo.