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>>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 --> needsUsing 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>>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.
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);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;
}
};UObject that implements the interface. It doesn’t need to be a component.$player_health. Every other variable is left to the Variable Storage.if (UYarnInMemoryVariableStorage* Storage = Cast<UYarnInMemoryVariableStorage>(DialogueRunner->VariableStorage.GetObject()))
{
UPlayerHealthEvaluator* Evaluator = NewObject<UPlayerHealthEvaluator>(this);
Storage->RegisterSmartVariableEvaluator(Evaluator);
}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>>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;
};#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));
}