Skip to main content

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.

GDScript
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:

GDScript
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:

  1. The importer reads characters.json from your exported project build directory.
  2. Each character entry becomes a StoryFlowCharacter instance stored in the project's characters dictionary.
  3. Character names are registered in the project's string table for localization support.
  4. Character paths are normalized and stored for consistent runtime lookups.
  5. Character portrait assets are resolved and stored in each character's own resolved_assets dictionary.

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 each StoryFlowCharacter's own resolved_assets dictionary. The runtime resolves the portrait from the character's assets at display time.
  • Script-specific assets (images referenced within individual .sfe files) are imported into each StoryFlowScript's own resolved_assets dictionary.

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:

GDScript
# 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.

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:

GDScript
@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 null if 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:

Code
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. 1. Resolve character - Look up the character, load the name from the string table, load the portrait texture.
  2. 2. Update the dialogue state character - Store the resolved character data on the dialogue state.
  3. 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:

GDScript
# 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()

Writing Character Variables

Use set_character_variable to update a character variable from your game code:

GDScript
@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.

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_characters dictionary. These copies are what get_character_variable and set_character_variable operate 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.
GDScript
# 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.

Need Help?

Join our Discord community to ask questions, share your projects, report bugs, and get support from the team and other users.

Join Discord