Skip to main content

Images & Media

StoryFlow dialogues can include images for character portraits, inline illustrations, and full-screen backgrounds. This guide covers how image assets are imported, resolved, and displayed in the Godot plugin.

Image Assets in StoryFlow

Images in StoryFlow are referenced in dialogue nodes via setImage nodes or directly on dialogue node data. Rather than embedding image data inline, StoryFlow stores asset keys that map to entries in a resolved_assets dictionary. This means the node graph is lightweight and the actual textures are loaded separately.

There are three levels of asset resolution, checked in order:

  • Character-level assets - Each StoryFlowCharacter has its own resolved_assets dictionary. Character portraits and character-specific media are resolved here first.
  • Script-level assets - Defined per script file. Images used in a specific script's nodes (dialogue illustrations, background images, etc.) are resolved from the script's own asset dictionary.
  • Project-level assets - Defined in characters.json and shared across all scripts. Fallback for assets not found at the character or script level.

At runtime, the execution context looks up asset keys in these dictionaries (character → script → project) to find the corresponding Texture2D resources.

Importing Images

When you import a StoryFlow project, the StoryFlowImporter processes all media files from the exported build directory. Image files are copied into your Godot project's resource directory and their paths are stored in the appropriate resolved_assets dictionary.

GDScript
# The importer processes the build directory and registers all media
# Image files are stored as file paths in resolved_assets
# At runtime, they are lazily loaded as Texture2D via ResourceLoader.load()

# Internal flow:
# 1. Scan the build/media directory for asset files
# 2. Copy image files to the output directory in the Godot project
# 3. Store the resource path in the resolved_assets dictionary
# 4. On first access at runtime, load via ResourceLoader.load()

Imported assets are placed in the output directory you configured during import (typically under res://storyflow/). The importer preserves the directory structure from the StoryFlow build.

Image Persistence Between Dialogues

One of the most useful image behaviors in StoryFlow is image persistence. Images can carry over from one dialogue node to the next without being explicitly re-assigned. This lets you set an image once and have it remain visible across a sequence of dialogue nodes.

How image persistence works

The persistence logic follows three rules:

  • Dialogue has an image assigned - That image is used, and it becomes the new persistent image for subsequent nodes.
  • Dialogue has NO image and image reset is false - The persistent image from the previous dialogue carries over. The player continues to see the same image.
  • Dialogue has NO image and image reset is true - The image is cleared (set to null). No image is shown.

The current image is stored on StoryFlowDialogueState.image as a Texture2D. Your UI reads this field each time dialogue_updated fires and displays the texture accordingly.

GDScript
@onready var dialogue_image: TextureRect = %DialogueImage

func _on_dialogue_updated(state: StoryFlowDialogueState) -> void:
    # state.image is either:
    # - A valid Texture2D (this node's image, or persisted from a previous node)
    # - null (no image, or image was reset)

    if state.image:
        dialogue_image.texture = state.image
        dialogue_image.visible = true
    else:
        dialogue_image.visible = false

SetImage Node

The setImage node sets an image variable value during script execution. The image can then be referenced by dialogue nodes or passed through connections to other nodes in the graph.

Like other Set* nodes in StoryFlow, the setImage node has a special behavior when it has no outgoing edge:

  1. First, it checks if execution is inside a forEach loop - if so, the loop continues to the next iteration.
  2. Otherwise, it checks if the incoming edge came from a Dialogue node - if so, it returns to that dialogue to re-render with the updated image.

This return-to-dialogue behavior is what enables live updates. For example, a player clicks an option on a dialogue node, the graph flows through a setImage node to change the displayed image, and because there is no outgoing edge, execution returns to the dialogue which re-renders with the new image immediately.

SetBackgroundImage Node

The setBackgroundImage node is distinct from the dialogue image field. While the dialogue image is typically an inline illustration or scene image shown alongside text, the background image is intended for full-screen backgrounds, scene transitions, or environment changes.

When the runtime processes a setBackgroundImage node, it emits the background_image_changed signal:

GDScript
# Signal signature
signal background_image_changed(image_path: String)

# Connect to the signal on your StoryFlow component
@onready var storyflow: StoryFlowComponent = %StoryFlowComponent
@onready var background_rect: TextureRect = %BackgroundRect

func _ready() -> void:
    storyflow.background_image_changed.connect(_on_background_image_changed)

func _on_background_image_changed(image_path: String) -> void:
    # Load the texture from the resolved path
    var texture := load(image_path) as Texture2D

    if texture and background_rect:
        background_rect.texture = texture
        background_rect.visible = true

Dialogue image vs. background image

The dialogue image field on StoryFlowDialogueState is designed for inline illustrations that appear within the dialogue UI - think character CGs or scene images next to the text. The setBackgroundImage node is for full-screen or large-scale visuals that exist outside the dialogue box, such as a visual novel background or environment art. Handle them separately in your UI.

Resolved Assets System

The Resolved Assets system is the bridge between StoryFlow's string-based asset references and Godot's loaded resource objects. It operates at three levels using Dictionary maps:

  • StoryFlowCharacter.resolved_assets - Per-character assets. Character portrait images and character-specific media are checked first.
  • StoryFlowScript.resolved_assets - Per-script assets. Images that are used only within a specific script's nodes are resolved from this dictionary.
  • StoryFlowProject.resolved_assets - Project-wide assets shared across all scripts. Fallback for assets not found at the character or script level.

Each dictionary entry maps an asset key (String) to either a loaded Resource or a String file path. The plugin uses lazy loading: on first access, if the value is a string path, it is loaded via ResourceLoader.load() and the dictionary entry is updated with the loaded resource for subsequent accesses.

GDScript
# Simplified internal asset resolution logic
# (this happens inside the plugin automatically)

func resolve_asset(asset_key: String, character: StoryFlowCharacter,
        script: StoryFlowScript, project: StoryFlowProject) -> Resource:
    # 1. Check character resolved assets first
    if character and character.resolved_assets.has(asset_key):
        return _load_or_return(character.resolved_assets, asset_key)

    # 2. Check script resolved assets
    if script and script.resolved_assets.has(asset_key):
        return _load_or_return(script.resolved_assets, asset_key)

    # 3. Fall back to project resolved assets
    if project and project.resolved_assets.has(asset_key):
        return _load_or_return(project.resolved_assets, asset_key)

    return null

At runtime, when the execution context encounters an asset reference (for example, a dialogue node's image key), it looks up the key in the character's resolved_assets first, then the script's, then falls back to the project's. This three-tier lookup ensures that character-specific assets take priority, followed by script-specific assets, while shared assets remain accessible from any script.

Displaying Images in Your UI

To display images in your dialogue UI, read the image fields from StoryFlowDialogueState and apply them to TextureRect nodes.

Dialogue Image

The main dialogue image is available at StoryFlowDialogueState.image. This is a Texture2D that may be null if no image is assigned and the image was reset.

GDScript
# In your dialogue UI's update function
@onready var scene_image: TextureRect = %SceneImage
@onready var character_portrait: TextureRect = %CharacterPortrait

func _on_dialogue_updated(state: StoryFlowDialogueState) -> void:
    # Dialogue illustration / scene image
    if state.image:
        scene_image.texture = state.image
        scene_image.visible = true
    else:
        scene_image.visible = false

    # Character portrait (separate from dialogue image)
    if state.character and state.character.image:
        character_portrait.texture = state.character.image
        character_portrait.visible = true
    else:
        character_portrait.visible = false

Character Portraits

Character portrait images are separate from the dialogue image field. They are accessed through StoryFlowDialogueState.character.image, which is also a Texture2D. Character images are resolved from the project-level resolved_assets since characters are defined in characters.json and shared across all scripts.

TextureRect Setup

In your dialogue scene, add TextureRect nodes for both the dialogue image and the character portrait. Set the texture property at runtime when the dialogue state updates. Toggle visibility based on whether the texture is valid to avoid displaying blank image boxes.

GDScript
# Typical TextureRect configuration for dialogue images
# Set these in the Inspector or via code:

# For scene/dialogue images (stretch to fit a content area)
@onready var scene_image: TextureRect = %SceneImage
# scene_image.stretch_mode = TextureRect.STRETCH_KEEP_ASPECT_COVERED
# scene_image.expand_mode = TextureRect.EXPAND_IGNORE_SIZE

# For character portraits (maintain aspect ratio)
@onready var portrait: TextureRect = %Portrait
# portrait.stretch_mode = TextureRect.STRETCH_KEEP_ASPECT_CENTERED
# portrait.expand_mode = TextureRect.EXPAND_FIT_WIDTH_PROPORTIONAL

Handling null images gracefully

Always check for null before setting the texture property. When state.image is null, hide the TextureRect rather than leaving a blank space. This keeps your dialogue UI clean when transitioning between nodes that have images and nodes that do not.

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