Characters
Characters bring your dialogue to life. This guide covers how character data flows from the StoryFlow Editor into Godot, 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 object for the full character definition and a runtime data object for in-game dialogue display.
StoryFlowCharacter
StoryFlowCharacter is a RefCounted class that stores the imported character definition. Each character in your project becomes a StoryFlowCharacter instance managed by the StoryFlowRuntime autoload.
class_name StoryFlowCharacter
extends RefCounted
## String table key for display name
var character_name: String = ""
## Asset key for default portrait image
var image_key: String = ""
## Normalized character path (for lookup)
var character_path: String = ""
## Character-specific variables: var_name -> { "name", "type", "value" }
var variables: Dictionary = {}
## Resolved assets: asset_key -> Resource (Texture2D)
var resolved_assets: Dictionary = {}
## CRITICAL: normalize character paths consistently
static func normalize_path(path: String) -> String:
return path.to_lower().replace("/", "\\") Data Object vs Runtime Data
The StoryFlowCharacter holds the raw imported data with string keys and asset references. At runtime, the plugin resolves these into StoryFlowCharacterData with actual loaded textures, localized strings, and typed variable values. You typically interact with the resolved runtime data during dialogue, not the character object directly.
Runtime Character Data
When a dialogue node references a character, the plugin resolves the character into a StoryFlowCharacterData object that contains everything your UI needs:
class_name StoryFlowCharacterData
extends RefCounted
## Resolved display name
var name: String = ""
## Resolved portrait image, or null
var image: Texture2D = null
## Character variables for interpolation: var_name -> display string
var variables: Dictionary = {}
The name field contains the final resolved string (not a key). The image field is a loaded Texture2D ready to display in a TextureRect node. The variables dictionary holds the current runtime values of all per-character variables as display strings.
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 build directory. - Each character entry becomes a
StoryFlowCharacterinstance stored in the project's characters dictionary. - Character names are registered in the project's string table for localization support.
- Character paths are normalized and stored for consistent runtime lookups.
- Character portrait assets are resolved and stored in each character's own
resolved_assetsdictionary.
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 eachStoryFlowCharacter's ownresolved_assetsdictionary. The runtime resolves the portrait from the character's assets at display time. - Script-specific assets (images referenced within individual
.sfefiles) are imported into eachStoryFlowScript's ownresolved_assetsdictionary.
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.
Re-importing Updates Characters
When you re-import your project, character data is 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 data 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
var normalized_path := path.to_lower().replace("/", "\\") This normalization is applied in two places:
- During import: The character path is normalized before being stored in the characters dictionary.
- During runtime lookup (
StoryFlowManager.get_runtime_character): The lookup path is normalized before searching the dictionary.
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 GDScript, you must normalize paths yourself before performing lookups.
Character Arrays
A script or global variable can store an ordered list of characters by setting its element type to character. This is the canonical pattern for representing a party, a roster, or any multi-character cast that should be iterated rather than addressed by hardcoded path.
Read the array from GDScript with get_character_array_variable. The function returns an Array[String] of character paths, each of which can be passed straight back into the other character APIs.
@onready var storyflow: StoryFlowComponent = %StoryFlowComponent
@onready var roster: HBoxContainer = %Roster
func display_party() -> void:
var paths: Array[String] = storyflow.get_character_array_variable("characters")
for path in paths:
var portrait := TextureRect.new()
portrait.texture = storyflow.get_character_portrait(path)
roster.add_child(portrait)
var label := Label.new()
label.text = storyflow.get_character_variable(path, "Name").get_string()
roster.add_child(label) Displaying Characters
Accessing Character Data
When the StoryFlowComponent emits a dialogue_updated signal, the StoryFlowDialogueState includes the resolved character data for the current dialogue node. Access it through the character field:
@onready var storyflow: StoryFlowComponent = %StoryFlowComponent
@onready var speaker_label: Label = %SpeakerLabel
@onready var portrait_rect: TextureRect = %PortraitRect
func _ready() -> void:
storyflow.dialogue_updated.connect(_on_dialogue_updated)
func _on_dialogue_updated(state: StoryFlowDialogueState) -> void:
# Character data (resolved from the character definition)
if state.character:
# Character name (resolved string, not a key)
speaker_label.text = state.character.name
speaker_label.visible = not state.character.name.is_empty()
# Character portrait texture (may be null)
if state.character.image:
portrait_rect.texture = state.character.image
portrait_rect.visible = true
else:
portrait_rect.visible = false
else:
speaker_label.visible = false
portrait_rect.visible = false Character Portraits
The character.image field is a Texture2D that can be used directly with TextureRect nodes. 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.
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, load the name from the string table, load the portrait texture.
- 2. Update the dialogue state 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 Godot, these become entries in the character's variables dictionary.
Reading Character Variables
Use get_character_variable on the StoryFlowComponent to read a character variable by path and name:
# Read a character variable from the StoryFlow component
@onready var storyflow: StoryFlowComponent = %StoryFlowComponent
var mood_value: StoryFlowVariant = storyflow.get_character_variable(
"characters/elena", # Character path
"mood" # Variable name
)
# Use the value (StoryFlowVariant can hold string, int, float, bool, etc.)
var mood: String = mood_value.get_string() Built-in Fields (Name and Image)
Two field names on every character are reserved and always available through get_character_variable, even if the character has no custom variables declared. get_character_variable(path, "Name") returns the localized display name with the string table resolved, so callers receive ready-to-render text. get_character_variable(path, "Image") returns the current portrait asset key, which reflects any setCharacterVar("Image", ...) mutations performed during dialogue, so a UI that polls this field after a costume change sees the new key immediately.
Writing Character Variables
Use set_character_variable to update a character variable from your game code:
@onready var storyflow: StoryFlowComponent = %StoryFlowComponent
# Set a character variable
var new_mood := StoryFlowVariant.from_string("angry")
storyflow.set_character_variable(
"characters/elena", # Character path
"mood", # Variable name
new_mood # New value
)
Changes made with set_character_variable take effect immediately. The next time dialogue text is interpolated or a getCharacterVar node runs, it will see the updated value.
Enumerating Character Variables and Direct Access
When a UI needs to inspect every variable on a character without knowing the names ahead of time, two helpers cover that case. get_character_variables(path) returns an Array[String] of the custom variable names declared on the character. The built-in "Name" and "Image" fields are intentionally excluded from this list because they are always available through get_character_variable regardless of declaration.
For the cases where you want to read several fields in one place without going through the variable accessors, get_character(path) returns the live runtime StoryFlowCharacter object. Mutating the returned object directly is not the supported path. Treat it as read-only and route writes through set_character_variable so the character_variable_changed signal fires for any UI listeners.
# Iterate every custom variable declared on a character
for var_name in storyflow.get_character_variables("characters/elena"):
var value := storyflow.get_character_variable("characters/elena", var_name)
print(var_name, " = ", value.to_display_string())
# Direct read access (do not mutate)
var elena: StoryFlowCharacter = storyflow.get_character("characters/elena")
print("Current portrait key: ", elena.image_key) Resolving Portraits at Runtime
get_character_portrait(path, asset_key="") resolves a character's portrait to a Texture2D at the moment of the call. With no asset_key argument, the function returns the texture for the character's current image_key, which already reflects any setCharacterVar("Image", ...) mutations from dialogue. Pass an explicit asset_key to fetch an alternate pose, which is the typical pattern when alternate portraits are stored in custom image-typed character variables (for example, "happy", "angry", "surprised").
Resolution walks the standard asset pools in priority order: character first, then script, then project. The first pool that contains the asset key wins, and the function returns null only when none of the three has a match.
# Current portrait (reflects setCharacterVar("Image", ...) mutations)
portrait_rect.texture = storyflow.get_character_portrait("characters/elena")
# Alternate pose stored in a custom image-typed character variable
var pose_key := storyflow.get_character_variable("characters/elena", "happyPose").get_string()
portrait_rect.texture = storyflow.get_character_portrait("characters/elena", pose_key) Listening for Character Variable Changes
The character_variable_changed signal fires every time a setCharacterVar node updates a character field. It covers the built-in "Name" and "Image" fields along with any custom variable. The general variable_changed signal continues to fire only for script and global variable mutations, so the two signals never overlap.
func _ready() -> void:
storyflow.character_variable_changed.connect(_on_character_variable_changed)
func _on_character_variable_changed(path: String, var_name: String, value: StoryFlowVariant) -> void:
if var_name == "Image":
portrait_rect.texture = storyflow.get_character_portrait(path) Runtime Persistence
Character variable state at runtime is managed by the StoryFlowRuntime autoload:
- Mutable runtime copies: When the manager initializes from a project, it creates mutable copies of all characters in its
_runtime_charactersdictionary. These copies are whatget_character_variableandset_character_variableoperate 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
reset_runtime_characters()on the manager 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
var manager = get_node("/root/StoryFlowRuntime")
manager.reset_runtime_characters() 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.