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
StoryFlowCharacterhas its ownresolved_assetsdictionary. 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.jsonand 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.
# 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.
@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:
- First, it checks if execution is inside a
forEachloop - if so, the loop continues to the next iteration. - 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:
# 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.
# 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.
# 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.
# 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.