Smart Variables

A smart variable is declared with an expression instead of a value. Its value is worked out from that expression every time something reads it, so it’s always up to date with the variables it’s made from.

<<declare $lamp_oil = 3>>
<<declare $lamp_hours = $lamp_oil * 2>>
<<declare $lamp_needs_oil = $lamp_oil < 5>>
Line 1 An ordinary variable. It’s declared with a value, and it’s stored in the Variable Storage.
Lines 2–3 Smart variables. They’re declared with an expression that uses another variable, and they aren’t stored.
One ordinary variable, and two smart variables worked out from it.

Only $lamp_oil is kept in the Variable Storage. When the oil goes up, $lamp_hours and $lamp_needs_oil change with it, without anything setting them.

flowchart LR
  oil[("Ordinary variable<br><br>$lamp_oil = 3")]
  hours["Smart variable<br><br>$lamp_hours<br>= $lamp_oil * 2<br>= 6"]
  needs["Smart variable<br><br>$lamp_needs_oil<br>= $lamp_oil #60; 5<br>= true"]
  oil --> hours
  oil --> needs
Setting $lamp_oil is enough. The two smart variables are worked out from it whenever they’re read.

Using smart variables in Yarn

Use a smart variable anywhere you’d use any other variable: in a line, in an <<if>>, or in another expression.

<<declare $lamp_oil = 3>>
<<declare $lamp_hours = $lamp_oil * 2>>
<<declare $lamp_needs_oil = $lamp_oil < 5>>
Rosa: There's only enough oil for {$lamp_hours} more hours.
<<set $lamp_oil to $lamp_oil + 4>>
Rosa: Thanks. Now it'll last {$lamp_hours} hours.
<<if $lamp_needs_oil>>
    Rosa: It'll need topping up soon.
<<endif>>
Smart variables in lines and a condition.

You can’t <<set>> a smart variable. The Yarn Project won’t compile, and the error says what the variable is always equal to. Change the variables it’s made from instead.

For more on writing smart variables, see Smart Variables in the Yarn docs.

Using smart variables from Blueprints and C++

There’s nothing to set up in Unreal. Read a smart variable the same way as any other variable, and you get its value as it is right now.

In Blueprints, use the Get Variable As Number, Get Variable As Bool or Get Variable As String node. Connect your Yarn Dialogue Runner to Dialogue Runner, and type the variable’s name, with its $, into Variable Name. The value comes out of Out Value, and Return Value is true if the variable could be read.

PLACEHOLDER — Screenshot needed
A Blueprint graph with a Yarn Dialogue Runner component reference connected to the Dialogue Runner pins of a Get Variable As Number node and a Get Variable As Bool node. Their Variable Name pins are set to $lamp_hours and $lamp_needs_oil.

In C++, the same functions are in UYarnBlueprintLibrary:

#include "YarnBlueprintLibrary.h"

float Hours = 0.0f;
bool bNeedsOil = false;
UYarnBlueprintLibrary::GetVariableAsNumber(DialogueRunner, TEXT("$lamp_hours"), Hours);
UYarnBlueprintLibrary::GetVariableAsBool(DialogueRunner, TEXT("$lamp_needs_oil"), bNeedsOil);
Reading two smart variables from C++.

These read from the Dialogue Runner’s Variable Storage. When the storage doesn’t have a stored value for the name, it asks the Dialogue Runner to work out the smart variable. The Dialogue Runner sets this up in its BeginPlay, so smart variables can be read from then on, whether or not a dialogue is running.

The Dialogue Runner gives its Variable Storage what it needs to work out smart variables by calling SetSmartVariableEvaluator() on it. The In Memory Variable Storage uses it. If you write your own Variable Storage in C++, override SetSmartVariableEvaluator() and GetSmartVariableEvaluator() to keep the evaluator, and call its TryGetSmartVariable() from your TryGetValue() when there’s no stored value. These two functions are C++ only, so a Variable Storage written in Blueprints can’t work out smart variables.

A few things work differently from ordinary variables:

  • Don’t set them from Blueprints or C++. Set Number Variable and the Variable Storage’s Set Number don’t stop you, but they store a value under the smart variable’s name, and from then on that stored value is used in place of the expression. Change the variables it’s made from.
  • They don’t tell you when they change. The In Memory Variable Storage’s On Variable Changed event, and the listeners added with Add Number Change Listener, Add Bool Change Listener and Add String Change Listener, only fire when a variable is set, and smart variables never are. To keep something up to date, read the smart variable again when you need it, for example every frame, or when one of the variables it’s made from changes.
  • They aren’t saved. Get All Variables and Save Variables To Slot only save stored variables. After loading, smart variables are worked out again from the loaded values, so they come back correct.
  • Get Debug String, Get All Variable Names and the Yarn Debug HUD Component don’t list them, because they only show stored variables.

Variables worked out by your game

Unreal has a second kind of variable that’s worked out when it’s read: one whose value comes from your game instead of from a Yarn expression. For example, $player_health could always return the player character’s current health, so you never have to copy it into Yarn.

To do this, make an object that implements the Yarn Game Smart Variable Evaluator interface (IYarnGameSmartVariableEvaluator). It has two functions:

  • Can Provide is given a variable name, and returns true if this object works out that variable.
  • Get Value is given a variable name, sets Out Value to its value, and returns true if it could.

Register it with the In Memory Variable Storage’s Register Smart Variable Evaluator node, and remove it with Unregister Smart Variable Evaluator. The Dialogue Runner creates its In Memory Variable Storage in BeginPlay if you haven’t given it one, so register your object after that, for example from your Actor’s Event BeginPlay. Get the storage from the Dialogue Runner’s Variable Storage, and cast it to Yarn In Memory Variable Storage.

#pragma once

#include "CoreMinimal.h"
#include "UObject/Object.h"
#include "YarnVariableStorage.h"
#include "PlayerHealthEvaluator.generated.h"

UCLASS()
class UPlayerHealthEvaluator : public UObject, public IYarnGameSmartVariableEvaluator
{
	GENERATED_BODY()

public:
	float Health = 100.0f;

	virtual bool CanProvide_Implementation(const FString& VariableName) override
	{
		return VariableName == TEXT("$player_health");
	}

	virtual bool GetValue_Implementation(const FString& VariableName, FYarnValue& OutValue) override
	{
		OutValue = FYarnValue(Health);
		return true;
	}
};
Lines 8–9 A plain UObject that implements the interface. It doesn’t need to be a component.
Line 14 Where the value comes from. In a real game, read it from your character or game state instead.
Lines 16–19 Only answer for $player_health. Every other variable is left to the Variable Storage.
Lines 21–25 Return the current value each time Yarn reads the variable.
An evaluator that gives Yarn the player’s health.
if (UYarnInMemoryVariableStorage* Storage = Cast<UYarnInMemoryVariableStorage>(DialogueRunner->VariableStorage.GetObject()))
{
	UPlayerHealthEvaluator* Evaluator = NewObject<UPlayerHealthEvaluator>(this);
	Storage->RegisterSmartVariableEvaluator(Evaluator);
}
Registering the evaluator after the Dialogue Runner has begun play.

The In Memory Variable Storage keeps the evaluator while it’s registered. Keep your own reference to it as well if you need to unregister it later.

Declare the variable in Yarn as usual, with a value of the right type, so the Yarn Project compiles:

<<declare $player_health = 100>>
<<if $player_health < 50>>
    Rosa: You're not looking so good.
<<endif>>
Declaring a variable that the game works out.

When a variable is read, the Variable Storage looks for a stored value first, then asks each registered evaluator, then works out Yarn’s own smart variables, and then uses the value from the <<declare>>. So don’t <<set>> a variable that an evaluator provides: once it has a stored value, the evaluator isn’t asked any more.

An example

This Actor shows the oil level and the hours of oil left in a Text Render Component, and makes a Point Light brighter as $lamp_hours goes up. It reads all three variables every frame, because the smart variables don’t say when they change.

In Blueprints, the same Actor uses Event Tick, a Get Dialogue Runner node, and the Get Variable As Number and Get Variable As Bool nodes shown above, then sets the text and the light’s intensity.

#pragma once

#include "CoreMinimal.h"
#include "GameFramework/Actor.h"
#include "LampDisplay.generated.h"

class UPointLightComponent;
class UTextRenderComponent;

UCLASS()
class ALampDisplay : public AActor
{
	GENERATED_BODY()

public:
	ALampDisplay();

	virtual void Tick(float DeltaSeconds) override;

	UPROPERTY(EditAnywhere, Category = "Lamp")
	TObjectPtr<AActor> DialogueActor;

	UPROPERTY(VisibleAnywhere, Category = "Lamp")
	TObjectPtr<UPointLightComponent> Lamp;

	UPROPERTY(VisibleAnywhere, Category = "Lamp")
	TObjectPtr<UTextRenderComponent> Status;
};
Lines 20–21 The Actor that has the Yarn Dialogue Runner on it. Pick it in the Details panel.
Lines 23–27 The light and the text, made in the constructor.
LampDisplay.h.
#include "LampDisplay.h"
#include "Components/PointLightComponent.h"
#include "Components/TextRenderComponent.h"
#include "YarnBlueprintLibrary.h"
#include "YarnDialogueRunner.h"

ALampDisplay::ALampDisplay()
{
	PrimaryActorTick.bCanEverTick = true;

	Lamp = CreateDefaultSubobject<UPointLightComponent>(TEXT("Lamp"));
	RootComponent = Lamp;

	Status = CreateDefaultSubobject<UTextRenderComponent>(TEXT("Status"));
	Status->SetupAttachment(Lamp);
}

void ALampDisplay::Tick(float DeltaSeconds)
{
	Super::Tick(DeltaSeconds);

	UYarnDialogueRunner* Runner = UYarnBlueprintLibrary::GetDialogueRunner(DialogueActor);

	float Oil = 0.0f;
	float Hours = 0.0f;
	bool bNeedsOil = false;
	UYarnBlueprintLibrary::GetVariableAsNumber(Runner, TEXT("$lamp_oil"), Oil);
	UYarnBlueprintLibrary::GetVariableAsNumber(Runner, TEXT("$lamp_hours"), Hours);
	UYarnBlueprintLibrary::GetVariableAsBool(Runner, TEXT("$lamp_needs_oil"), bNeedsOil);

	Status->SetText(FText::FromString(FString::Printf(
		TEXT("$lamp_oil = %.0f\n$lamp_hours = %.0f (smart)\n$lamp_needs_oil = %s (smart)"),
		Oil, Hours, bNeedsOil ? TEXT("true") : TEXT("false"))));

	Lamp->SetIntensity(5000.0f * FMath::Clamp(Hours / 14.0f, 0.0f, 1.0f));
}
Line 9 Smart variables don’t tell you when they change, so read them every frame.
Line 22 Find the Yarn Dialogue Runner on the Actor picked in the Details panel.
Lines 24–29 Read the stored variable and the two smart variables, the same way.
Lines 31–33 Show the values.
Line 35 Make the lamp brighter the more hours of oil it has, up to full at 14 hours.
LampDisplay.cpp. It reads one stored variable and two smart variables every frame.
Next step Saliency and Storylets How the Dialogue Runner picks one line or node when several could run.