Characters
Characters bring your dialogue to life. This guide covers how character data flows from the StoryFlow Editor into Unreal Engine, how to display character information in your UI, and how to work with per-character variables at runtime.
Character Data Structure
Characters are defined in the StoryFlow Editor and exported as part of the JSON project in a characters.json file. The plugin represents characters with two complementary types: a data asset for editor-time configuration and a runtime struct for in-game use.
UStoryFlowCharacterAsset
UStoryFlowCharacterAsset is a UDataAsset that stores the imported character definition. Each character in your project becomes a separate asset in the content directory.
UCLASS(BlueprintType)
class UStoryFlowCharacterAsset : public UDataAsset
{
GENERATED_BODY()
public:
// Display name (string table key for localization)
UPROPERTY(VisibleAnywhere, BlueprintReadOnly)
FString Name;
// Default portrait image (asset key resolved during import)
UPROPERTY(VisibleAnywhere, BlueprintReadOnly)
FString Image;
// Per-character variables (e.g., mood, clothing, relationship)
UPROPERTY(VisibleAnywhere, BlueprintReadOnly)
TMap<FString, FStoryFlowVariable> Variables;
// Normalized path used for lookups
UPROPERTY(VisibleAnywhere, BlueprintReadOnly)
FString CharacterPath;
// Resolved asset references (character images, etc.)
UPROPERTY(VisibleAnywhere, BlueprintReadOnly)
TMap<FString, TSoftObjectPtr<UObject>> ResolvedAssets;
}; Asset vs Runtime Data
The UStoryFlowCharacterAsset holds the raw imported data with string keys and references. At runtime, the plugin resolves these into FStoryFlowCharacterData with actual loaded textures, localized strings, and typed variable values. You typically interact with the resolved runtime data, not the asset directly.
Runtime Character Data
When a dialogue node references a character, the plugin resolves the asset into an FStoryFlowCharacterData struct that contains everything your UI needs:
USTRUCT(BlueprintType)
struct FStoryFlowCharacterData
{
GENERATED_BODY()
// Resolved character name (loaded from string table)
UPROPERTY(BlueprintReadOnly)
FString Name;
// Loaded portrait texture (can be null if no image assigned)
UPROPERTY(BlueprintReadOnly)
UTexture2D* Image = nullptr;
// Mutable character variables with typed values
UPROPERTY(BlueprintReadOnly)
TMap<FString, FStoryFlowVariant> Variables;
};
The Name field contains the final resolved string from the string table (not a key). The Image field is a loaded UTexture2D* pointer ready to display in a UMG Image widget. The Variables map holds the current runtime values of all per-character variables as FStoryFlowVariant values.
Importing Characters
Import Workflow
Characters are imported automatically when you import your StoryFlow project. You do not need to import characters separately. The import process works as follows:
- The importer reads
characters.jsonfrom your exported project. - Each character entry becomes a separate
UStoryFlowCharacterAssetsaved in your content directory under the StoryFlow project folder. - Character names are registered in the project's string table for localization support.
- Character paths are normalized and stored for consistent runtime lookups.
Asset Resolution
Character images follow a specific resolution pattern that differs from script-level assets:
- Character images (portraits defined in
characters.json) are imported into the project-levelResolvedAssetson theUStoryFlowProjectAsset. These are shared across all scripts. - Script-specific assets (images referenced within individual
.sfefiles) are imported into eachUStoryFlowScriptAsset's ownResolvedAssetsmap.
This separation ensures that character portraits are available globally regardless of which script is currently executing, while script-specific assets remain scoped to their respective scripts.
From Blueprint or C++ you do not need to walk these maps manually. Call GetCharacterPortrait(CharacterPath) and it traverses character, script, project ResolvedAssets in priority order, falling back to the character's CachedImage.
Re-importing Updates Characters
When you re-import your project, character assets are updated in place. New characters are created, existing characters are updated with any changes, and their resolved assets are refreshed. You do not need to delete old character assets manually.
Character Path Normalization
Critical: Path Normalization
Character paths must be normalized consistently. The plugin uses the following normalization rule:
// Normalization rule: lowercase + forward slashes replaced with backslashes
FString NormalizedPath = CharacterPath.ToLower().Replace(TEXT("/"), TEXT("\\")); This normalization is applied in two places:
- During import (
ParseCharacters): The character path is normalized before being stored in the asset map. - During runtime lookup (
FindCharacter): The lookup path is normalized before searching the map.
If these two normalizations do not match, the character will not be found at runtime, resulting in empty name and image values in your dialogue. The plugin handles this automatically for all built-in operations, but if you are accessing characters directly from C++ code, you must normalize paths yourself before performing lookups.
Character Lists
Some stories need to act on a group of characters at once, for example a party, a cast roster or an event's attendees. StoryFlow supports this with array variables whose element type is character. From Unreal you read these arrays with GetCharacterArrayVariable(VariableName) on the UStoryFlowComponent.
The function returns a TArray<FString> of character paths. Each path is normalized and ready to pass into GetCharacter, GetCharacterVariable or GetCharacterPortrait. The lookup searches local script variables first, then global variables.
// Read a "partyMembers" array variable defined as a character array in the StoryFlow Editor
UStoryFlowComponent* StoryFlow = FindComponentByClass<UStoryFlowComponent>();
TArray<FString> Party = StoryFlow->GetCharacterArrayVariable(TEXT("partyMembers"));
for (const FString& CharacterPath : Party)
{
// Pull a portrait for each cast member
UTexture2D* Portrait = StoryFlow->GetCharacterPortrait(CharacterPath);
// Or read a custom variable on each character
FStoryFlowVariant Health = StoryFlow->GetCharacterVariable(CharacterPath, TEXT("health"));
// Spawn a roster widget entry, update HUD, etc.
} Variable on a Character vs Variable of Characters
GetCharacterVariable reads a variable that lives on a single character (their mood, outfit, health). GetCharacterArrayVariable reads a script or global variable whose element type is character, which is a list of character paths. They sound similar but solve different problems.
Displaying Characters
Accessing Character Data
When the UStoryFlowComponent broadcasts a dialogue update, the FStoryFlowDialogueState struct includes the resolved character data for the current dialogue node. Access it through the Character field:
// In your dialogue widget or handler
void UMyDialogueWidget::OnDialogueUpdated(const FStoryFlowDialogueState& State)
{
// Character name (resolved from string table)
FString SpeakerName = State.Character.Name;
// Character portrait texture (may be null)
UTexture2D* Portrait = State.Character.Image;
// Character-specific variables
TMap<FString, FStoryFlowVariant> CharVars = State.Character.Variables;
// Update your UI
SpeakerNameText->SetText(FText::FromString(SpeakerName));
if (Portrait)
{
PortraitImage->SetBrushFromTexture(Portrait);
PortraitImage->SetVisibility(ESlateVisibility::Visible);
}
else
{
PortraitImage->SetVisibility(ESlateVisibility::Collapsed);
}
}
In Blueprints, break the DialogueState struct to access the Character field, then break that to reach Name, Image, and Variables.
Character Portraits
The Character.Image field is a UTexture2D* that can be used directly with UMG Image widgets. Keep in mind:
- The image can be
nullif no portrait was assigned to the character in the StoryFlow Editor. Always check before using it. - Portrait textures are loaded during character resolution, so they are ready to display immediately when the dialogue state is broadcast.
- If the same character appears across multiple dialogues, the same texture reference is reused - there is no redundant loading.
Reading Character Data Outside Dialogue
Dialogue state is only populated while a dialogue is active. For UI that displays character information outside of a conversation, for example a roster screen, a bestiary entry or a quest log, the UStoryFlowComponent exposes two helpers that read character state directly.
GetCharacter(CharacterPath)returns the fullFStoryFlowCharacterDefstruct for a character, includingName,Imageand the live custom variables map. Returns a default-constructed struct if no character is registered at that path.GetCharacterPortrait(CharacterPath, AssetKey)resolves a character's portrait to aUTexture2D*. WhenAssetKeyis empty it uses the character's currentImagefield, which reflects any runtime mutations fromsetCharacterVar("Image", ...). Pass a non-empty key to resolve an alternate pose stored in an image-typed character variable.
UStoryFlowComponent* StoryFlow = FindComponentByClass<UStoryFlowComponent>();
// Pull a character record for a roster widget
FStoryFlowCharacterDef Elena = StoryFlow->GetCharacter(TEXT("characters/elena"));
RosterEntry->SetName(Elena.Name);
// Pull the current default portrait
UTexture2D* Portrait = StoryFlow->GetCharacterPortrait(TEXT("characters/elena"));
// Or resolve an alternate pose stored in a custom image-typed character variable named "battlePose"
UTexture2D* BattlePose = StoryFlow->GetCharacterPortrait(
TEXT("characters/elena"),
TEXT("battlePose")
); GetCharacter Returns by Value
GetCharacter returns the struct by value. Stashed copies do not reflect later runtime mutations, even if the original character data is updated by setCharacterVar nodes or by SetCharacterVariable. For live tracking, bind to the OnCharacterVariableChanged delegate and refresh your UI in the callback. See Reacting to Character Variable Changes.
Character Name Interpolation
StoryFlow supports inserting character data directly into dialogue text using curly brace syntax. This allows you to write dialogue that dynamically includes the speaker's name or custom variable values.
{Character.Name}- Inserts the current character's resolved name.{Character.VarName}- Inserts the value of a character variable (e.g.,{Character.mood}or{Character.title}).
For example, a dialogue node with the text:
My name is {Character.Name} and I'm feeling {Character.mood} today. Would render as something like: "My name is Elena and I'm feeling cheerful today."
Resolution Order Matters
Character interpolation depends on a specific internal resolution order. The plugin processes each dialogue node in this exact sequence:
- 1. Resolve character - Look up the character asset, load the name from the string table, load the portrait texture.
- 2. Update CurrentDialogueState.Character - Store the resolved character data on the dialogue state.
- 3. Interpolate text - Process
{Character.Name}and other interpolation tokens, reading from the now-populated character data.
This order is critical. If text interpolation happened before character resolution, {Character.Name} would resolve to an empty string. The plugin handles this order internally, so you do not need to manage it yourself. This note is for understanding why the order matters if you encounter debugging scenarios.
Character Variables
Characters can have custom variables attached to them, defined in the StoryFlow Editor. These are ideal for per-character state like mood, relationship level, outfit, or any other property that should travel with the character rather than live as a global variable.
In the StoryFlow Editor, character variables are defined on the character file and manipulated with getCharacterVar and setCharacterVar nodes. At runtime in Unreal, these become entries in the character's Variables map.
Reading Character Variables
Use GetCharacterVariable on the UStoryFlowComponent to read a character variable by path and name:
// Read a character variable from the StoryFlow component
UStoryFlowComponent* StoryFlow = FindComponentByClass<UStoryFlowComponent>();
FStoryFlowVariant MoodValue = StoryFlow->GetCharacterVariable(
TEXT("characters/elena"), // Character path
TEXT("mood") // Variable name
);
// Use the value (FStoryFlowVariant can hold string, int, float, bool, etc.)
FString Mood = MoodValue.GetString();
In Blueprints, search for Get Character Variable on the StoryFlow Component node. It returns an FStoryFlowVariant that you can convert to the expected type.
The same getter also accepts the built-in "Name" and "Image" keys, treating them symmetrically with custom variables. This is convenient when iterating a character's full record without case-splitting on which field is built-in:
// Read built-in fields through the same API as custom variables
FStoryFlowVariant DisplayName = StoryFlow->GetCharacterVariable(TEXT("characters/elena"), TEXT("Name"));
FStoryFlowVariant CurrentImage = StoryFlow->GetCharacterVariable(TEXT("characters/elena"), TEXT("Image"));
To enumerate just the custom variables on a character without hardcoding their names, use GetCharacterVariableNames(CharacterPath). The returned list excludes the built-in Name and Image fields, even though those remain accessible via GetCharacterVariable.
// Walk every custom variable defined on a character
TArray<FString> Names = StoryFlow->GetCharacterVariableNames(TEXT("characters/elena"));
for (const FString& VarName : Names)
{
FStoryFlowVariant Value = StoryFlow->GetCharacterVariable(TEXT("characters/elena"), VarName);
UE_LOG(LogTemp, Log, TEXT("Elena.%s = %s"), *VarName, *Value.ToString());
} Writing Character Variables
Use SetCharacterVariable to update a character variable from your game code:
UStoryFlowComponent* StoryFlow = FindComponentByClass<UStoryFlowComponent>();
// Set a character variable
FStoryFlowVariant NewMood;
NewMood.SetString(TEXT("angry"));
StoryFlow->SetCharacterVariable(
TEXT("characters/elena"), // Character path
TEXT("mood"), // Variable name
NewMood // New value
);
Changes made with SetCharacterVariable take effect immediately. The next time dialogue text is interpolated or a getCharacterVar node runs, it will see the updated value.
Runtime Persistence
Character variable state at runtime is managed by the UStoryFlowSubsystem:
- Mutable runtime copies: When the subsystem initializes, it creates mutable copies of all character variables in
UStoryFlowSubsystem::RuntimeCharacters. These copies are whatGetCharacterVariableandSetCharacterVariableoperate on. - Cross-dialogue persistence: Changes to character variables persist across different dialogue sessions and script transitions within the same game session. If Elena's mood is set to "angry" in one dialogue, it remains "angry" when another dialogue references her.
- Resetting state: Call
ResetRuntimeCharacters()on the subsystem to restore all character variables to their original imported values. This is useful when starting a new game or resetting story state. - Save/Load integration: Runtime character variable state is included when using the plugin's Save & Load system. Saved character state is fully restored when loading a save slot.
// Reset all character variables to their imported defaults
UStoryFlowSubsystem* Subsystem = GetGameInstance()->GetSubsystem<UStoryFlowSubsystem>();
Subsystem->ResetRuntimeCharacters(); Character Variables vs Global Variables
Use character variables for state that conceptually belongs to a character (mood, relationship, outfit, health). Use global variables for state that belongs to the story or world (quest progress, time of day, world flags). This separation keeps your data model clean and makes it easy to reason about where state lives.
Reacting to Character Variable Changes
The UStoryFlowComponent broadcasts OnCharacterVariableChanged every time a character variable mutates. This covers the built-in Name and Image fields as well as any custom variable defined on the character. Bind to this delegate when you need a UI outside the active dialogue, for example a party roster, a relationship tracker or a portrait pinned to the HUD, to stay in sync with setCharacterVar mutations from the graph.
The delegate signature includes the character path, the variable name and the new FStoryFlowVariant value:
DECLARE_DYNAMIC_MULTICAST_DELEGATE_ThreeParams(
FOnCharacterVariableChanged,
const FString&, CharacterPath,
const FString&, VariableName,
const FStoryFlowVariant&, Value
); Binding in Blueprint: select the StoryFlow Component, scroll to StoryFlow | Events in the Details panel and click + next to On Character Variable Changed. The new event node provides CharacterPath, VariableName and Value as output pins.
Binding in C++:
// In your actor's BeginPlay
UStoryFlowComponent* StoryFlow = FindComponentByClass<UStoryFlowComponent>();
StoryFlow->OnCharacterVariableChanged.AddDynamic(
this, &AMyActor::HandleCharacterVariableChanged
);
void AMyActor::HandleCharacterVariableChanged(
const FString& CharacterPath,
const FString& VariableName,
const FStoryFlowVariant& Value)
{
if (CharacterPath == TEXT("characters/elena") && VariableName == TEXT("Image"))
{
// Elena's portrait changed mid-scene, refresh the HUD pin
UTexture2D* NewPortrait = StoryFlowComponent->GetCharacterPortrait(CharacterPath);
HUDPortrait->SetBrushFromTexture(NewPortrait);
}
else if (VariableName == TEXT("mood"))
{
// Update a mood widget when any character's mood changes
UpdateMoodIndicator(CharacterPath, Value.GetString());
}
} Choosing the Right Event
OnDialogueUpdated hands you the full speaking character's data on every dialogue update. OnCharacterVariableChanged fires for any character at any time, regardless of who is currently speaking. Use the dialogue event for the active speaker UI and the character event for everything else that needs to stay reactive.