Images & Media
StoryFlow dialogue nodes 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 Unity plugin.
How Images Work
Images in StoryFlow are referenced by dialogue nodes via asset keys. When a project is imported into Unity, the importer converts image files into Texture2D assets with proper sprite import settings, then stores references in ResolvedAssets dictionaries. At runtime, the execution context resolves these keys to Sprite objects that you can display in your UI.
There are two levels of asset storage:
- Character assets - Character portraits are stored directly on each
StoryFlowCharacterAsset.ResolvedImage. They are defined incharacters.jsonand shared across all scripts. - Script-level assets - Defined per script file. Images used in a specific script's dialogue nodes (illustrations, background images, etc.) are stored on
StoryFlowScriptAsset.ResolvedAssets.
Image persistence
Images can persist across dialogue nodes without being explicitly re-assigned. The persistence logic follows three rules:
- Dialogue has an image assigned - That image is used and 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.
- Dialogue has no image and image reset is true - The image is cleared. No image is shown.
Dialogue Images
The current dialogue image is available on StoryFlowDialogueState.Image as a Sprite. This field may be null if no image is assigned and the image was reset. Read it each time OnDialogueUpdated fires and apply it to a Unity UI Image component.
void OnDialogueUpdated(StoryFlowDialogueState state)
{
if (state.Image != null)
{
dialogueImage.sprite = state.Image;
dialogueImage.gameObject.SetActive(true);
}
else
{
dialogueImage.gameObject.SetActive(false);
}
}
Character portrait images are separate from the dialogue Image field. They are accessed through StoryFlowDialogueState.Character.Image, which is also a Sprite. Character images are resolved from each StoryFlowCharacterAsset.ResolvedImage since characters are defined in characters.json and shared across all scripts.
void OnDialogueUpdated(StoryFlowDialogueState state)
{
// Dialogue illustration
if (state.Image != null)
{
sceneImage.sprite = state.Image;
sceneImage.gameObject.SetActive(true);
}
else
{
sceneImage.gameObject.SetActive(false);
}
// Character portrait (separate from dialogue image)
if (!string.IsNullOrEmpty(state.Character.Name) && state.Character.Image != null)
{
characterPortrait.sprite = state.Character.Image;
characterPortrait.gameObject.SetActive(true);
}
else
{
characterPortrait.gameObject.SetActive(false);
}
} Handling null images gracefully
Always check for null before assigning a sprite. When state.Image is null, hide or disable the image GameObject rather than leaving a blank space. This keeps your dialogue UI clean when transitioning between nodes that have images and nodes that do not.
Background Images
Background images are set via dedicated SetBackgroundImage nodes rather than being attached to dialogue nodes. They are intended for full-screen visuals, scene transitions, or environment art that exist outside the dialogue box.
Background images are set via the SetBackgroundImage node in the StoryFlow Editor. When the runtime processes this node, it broadcasts the OnBackgroundImageChanged event with the resolved Sprite:
component.OnBackgroundImageChanged += (sprite) => {
backgroundImage.sprite = sprite;
};
The background image persists until explicitly changed by another SetBackgroundImage node or cleared by passing an empty value. It is stored internally on the execution context as PersistentBackgroundImage and survives across dialogue node transitions.
Dialogue image vs. background image
Internally, both dialogue node images and SetBackgroundImage nodes write to the same PersistentBackgroundImage field on the execution context. This means a SetBackgroundImage node will affect state.Image on subsequent dialogue nodes that have no image assigned. In your UI, you may want to use state.Image for inline illustrations and OnBackgroundImageChanged for full-screen backgrounds, but be aware they share the same underlying persistence.
Asset Resolution
The asset resolution system is the bridge between StoryFlow's string-based asset references and Unity's loaded asset objects. It operates at two levels:
-
StoryFlowScriptAsset.ResolvedAssets- Per-script assets. Images used only within a specific script's nodes are resolved from this dictionary. -
StoryFlowProjectAsset.ResolvedAssets- Project-wide assets shared across all scripts. It receives theassetssections ofglobal-variables.jsonandcharacters.jsonas well, so a global Image or Audio variable, a custom image or audio character variable and a character map value all resolve here. Note: a character's own portrait is also stored directly on itsStoryFlowCharacterAsset.ResolvedImage.
Both pools, the project's and each script's, are emptied and repopulated on every import. An asset you remove or rename in the StoryFlow Editor therefore stops resolving in Unity on the next sync instead of lingering as a stale entry pointing at whatever it used to be.
The StoryFlowComponent provides a generic ResolveAsset<T> method that checks both levels automatically, prioritizing script-level assets:
// Resolves an asset by key, checking script assets first, then project assets
T asset = component.ResolveAsset<Sprite>("assetKey");
// Example: resolve a specific image
Sprite illustration = component.ResolveAsset<Sprite>("asset_image_forest");
if (illustration != null)
{
myImage.sprite = illustration;
} This two-level lookup means script-specific assets can override project-level assets with the same key, giving you fine-grained control over which asset is used in a given context.
For character portraits, prefer GetCharacterPortrait(characterPath, assetKey) over ResolveAsset<Sprite>. It additionally checks the character asset's own resolved-assets pool before falling back to script and project pools, which is the correct lookup order for character-scoped portraits, and for the character's current portrait it falls back one last time to the sprite baked onto the character asset as ResolvedImage when no pool holds the key. Passing an empty assetKey resolves the character's current ImageAssetKey, which reflects any setCharacterVar mutations on the Image field.
StoryFlowComponent sf = GetComponent<StoryFlowComponent>();
// Current portrait for any character (not just the speaker)
Sprite portrait = sf.GetCharacterPortrait("characters/elder");
// Alternate pose stored on an image-typed character variable
Sprite cheering = sf.GetCharacterPortrait("characters/elder", "elder_cheer"); Import Process
When you import a StoryFlow project via the editor importer, the following happens for image assets:
- The importer reads asset references from each script's JSON data and from
characters.json, then imports the referenced image files from the exportedbuild/directory. - Each image is imported as a
Texture2Dasset with sprite import settings applied (texture type set to Sprite, appropriate filter mode, etc.). - Script-specific image references are stored as
ResolvedAssetEntryentries on the correspondingStoryFlowScriptAsset. Character portrait images are stored directly on eachStoryFlowCharacterAsset.ResolvedImage. - At runtime, the
ResolvedAssetEntriesserialized list is lazily rebuilt into aDictionary<string, Object>for fast lookup.
Character images from characters.json are stored on individual StoryFlowCharacterAsset ScriptableObjects. Script-specific images (dialogue illustrations, background images referenced only in that script) go into the corresponding script asset's ResolvedAssets.
What Gets Rewritten
Copying is skipped when nothing changed. Before a media file is written, the source and the destination are compared by length and then by content, and an identical destination is left alone. A routine sync therefore produces no rewrite, no ForceUpdate reimport, no texture recompression and no version control traffic for media you did not touch. A file referenced by several scripts is settled once per import rather than once per reference.
The skip check is safe against a deleted output folder. Resolved references are certified by GUID as well as by path, so deleting the imported media folder in the Project window and syncing again is not mistaken for "nothing changed": the assets come back with new GUIDs, the certification differs and the referencing assets are rewritten to match. A hash is recorded only after the save actually reaches disk, so a file that failed to write is retried on the next sync rather than skipped forever.
Re-importing updates assets in place
Re-importing does not recreate your assets. A changed image is written over the existing asset and keeps its GUID, so prefabs and scenes referencing that Sprite pick up the new image without being re-linked, and an unchanged image is not written at all. To deliberately rewrite everything, for instance after an imported asset was hand-edited or partially deleted, use Actions > Re-Import from Source on the project asset. See Importing Projects.