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 Unreal Engine 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 ResolvedAssets map. This means the node graph is lightweight and the actual textures are loaded separately.

There are two levels of asset resolution:

  • Project-level assets — Defined in characters.json and shared across all scripts. Character portraits are the most common example.
  • 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 map.

At runtime, the execution context looks up asset keys in these maps to find the corresponding UTexture2D references.

Importing Images

When you import a StoryFlow project, the UStoryFlowImporter::ImportMediaAssets() function processes all media files from the exported build directory. Image files are imported as UTexture2D assets via the ImportImageAsset() function.

C++
// ImportMediaAssets processes the build directory and imports all media
// Image files are imported as UTexture2D
// Audio files are imported as USoundWave (WAV) or FileMediaSource (MP3)

// Internal flow:
// 1. Scan the build/media directory for asset files
// 2. Call ImportImageAsset(SourcePath, ContentPath, AssetName) for each image
// 3. Normalize the asset path via NormalizeAssetPath() for valid UE naming
// 4. Store the result in the ResolvedAssets map as TSoftObjectPtr<UObject>

Imported assets are placed in type-specific subdirectories under your content path:

  • Textures/ — All image assets (UTexture2D)
  • Audio/ — All sound assets (USoundWave)

Asset paths are normalized via NormalizeAssetPath() to ensure they conform to Unreal Engine's naming requirements. The normalized paths are then used as keys in the ResolvedAssets maps so the runtime can find them later.

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 bImageReset is false — The persistent image from the previous dialogue carries over. The player continues to see the same image.
  • Dialogue has NO image and bImageReset is true — The image is cleared (set to nullptr). No image is shown.

The current image is stored on FStoryFlowDialogueState.Image as a UTexture2D*. Your UI widget reads this field each time OnDialogueUpdated fires and displays the texture accordingly.

C++
void UMyDialogueWidget::UpdateDialogue(const FStoryFlowDialogueState& State)
{
    // State.Image is either:
    // - A valid UTexture2D* (this node's image, or persisted from a previous node)
    // - nullptr (no image, or image was reset)

    if (DialogueImage)
    {
        if (State.Image)
        {
            DialogueImage->SetBrushFromTexture(State.Image);
            DialogueImage->SetVisibility(ESlateVisibility::Visible);
        }
        else
        {
            DialogueImage->SetVisibility(ESlateVisibility::Collapsed);
        }
    }
}

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 broadcasts the OnBackgroundImageChanged delegate:

C++
// Delegate signature
DECLARE_DYNAMIC_MULTICAST_DELEGATE_OneParam(
    FOnBackgroundImageChanged, const FString&, ImagePath);

// Bind to the delegate on your StoryFlow component
StoryFlowComponent->OnBackgroundImageChanged.AddDynamic(
    this, &AMyActor::HandleBackgroundImageChanged);

// Handle the background image change
void AMyActor::HandleBackgroundImageChanged(const FString& ImagePath)
{
    // Look up the texture from the project or script resolved assets
    UTexture2D* BackgroundTexture = ResolveImageFromPath(ImagePath);

    if (BackgroundTexture && BackgroundWidget)
    {
        BackgroundWidget->SetBrushFromTexture(BackgroundTexture);
    }
}

Dialogue image vs. background image

The dialogue Image field on FStoryFlowDialogueState 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 Unreal Engine's loaded asset objects. It operates at two levels using TSoftObjectPtr<UObject> maps:

  • UStoryFlowProjectAsset::ResolvedAssets — Project-wide assets shared across all scripts. Character images imported from characters.json live here.
  • UStoryFlowScriptAsset::ResolvedAssets — Per-script assets. Images that are used only within a specific script's nodes are resolved from this map.

Each asset entry is represented by the FStoryFlowAsset struct:

C++
USTRUCT(BlueprintType)
struct FStoryFlowAsset
{
    GENERATED_BODY()

    // Unique identifier (e.g., "asset_image_1")
    UPROPERTY(BlueprintReadOnly)
    FString Id;

    // Asset type (Image, Audio, etc.)
    UPROPERTY(BlueprintReadOnly)
    EStoryFlowAssetType Type;

    // Normalized relative path from the build directory
    UPROPERTY(BlueprintReadOnly)
    FString Path;

    // Soft reference to the imported Unreal asset
    UPROPERTY(BlueprintReadOnly)
    TSoftObjectPtr<UObject> LoadedAsset;
};

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 script's ResolvedAssets first, then falls back to the project's ResolvedAssets. The resulting TSoftObjectPtr is resolved to load the actual UTexture2D.

Displaying Images in Your UI

To display images in your dialogue UI, read the image fields from FStoryFlowDialogueState and apply them to UMG Image widgets.

Dialogue Image

The main dialogue image is available at FStoryFlowDialogueState.Image. This is a UTexture2D* that may be null if no image is assigned and the image was reset.

C++
// In your dialogue widget's update function
void UMyDialogueWidget::UpdateDialogue(const FStoryFlowDialogueState& State)
{
    // Dialogue illustration / scene image
    if (SceneImage)
    {
        if (State.Image)
        {
            SceneImage->SetBrushFromTexture(State.Image);
            SceneImage->SetVisibility(ESlateVisibility::Visible);
        }
        else
        {
            SceneImage->SetVisibility(ESlateVisibility::Collapsed);
        }
    }

    // Character portrait (separate from dialogue image)
    if (CharacterPortrait)
    {
        if (State.Character.Image)
        {
            CharacterPortrait->SetBrushFromTexture(State.Character.Image);
            CharacterPortrait->SetVisibility(ESlateVisibility::Visible);
        }
        else
        {
            CharacterPortrait->SetVisibility(ESlateVisibility::Collapsed);
        }
    }
}

Character Portraits

Character portrait images are separate from the dialogue Image field. They are accessed through FStoryFlowDialogueState.Character.Image, which is also a UTexture2D*. Character images are resolved from the project-level ResolvedAssets since characters are defined in characters.json and shared across all scripts.

UMG Image Widget Setup

In your Widget Blueprint, add Image widgets for both the dialogue image and the character portrait. Use Set Brush from Texture to apply the UTexture2D* at runtime. Toggle visibility based on whether the texture pointer is valid to avoid displaying blank image boxes.

Handling null images gracefully

Always check for nullptr before calling SetBrushFromTexture. When State.Image is null, collapse or hide the image widget 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