Skip to main content

Save & Load

The StoryFlow plugin provides a complete save/load system built on Unreal Engine's native USaveGame infrastructure. Persist global variables, character state, and once-only option tracking across play sessions with simple Blueprint-callable functions.

What Gets Saved

When you call SaveToSlot, the following data is serialized into a single JSON blob and written to disk:

  • Global Variables (TMap<FString, FStoryFlowVariable>) — All project-level variables with their current values, including arrays of any type (boolean, integer, float, string, image, character, audio). This captures the full state of your story's persistent data.
  • Runtime Characters (TMap<FString, FStoryFlowCharacterDef>) — Character definitions with any modified character variables. If your story changes a character's name, color, or custom variables during play, those modifications are preserved.
  • Once-Only Options (TSet<FString>) — Tracking data for which "once only" dialogue options have been used. Keys follow the format nodeId-optionId, so the system knows exactly which options to hide on subsequent visits.

What Does NOT Get Saved

Save files represent "between dialogue" checkpoints. The following data is intentionally excluded:

  • Local variables — These are per-script and reset every time a script is entered. They are transient by design and do not carry over between sessions.
  • Current dialogue position / execution state — The current node, call stack, and flow stack are not persisted. A save does not capture "where you are" in a conversation.
  • Mid-dialogue state — If a dialogue is currently being displayed to the player, that in-progress state is not saved. The subsystem actively guards against this (see Active Dialogue Guard below).

Checkpoint Model

Think of StoryFlow saves as story checkpoints, not mid-sentence bookmarks. They capture the cumulative result of all player choices (variable values, used options, character modifications) but not the current position in a dialogue tree. Your game is responsible for knowing which script to start when loading a save — typically by storing a "current scene" identifier in a global variable.

UStoryFlowSaveGame

The UStoryFlowSaveGame class extends Unreal's built-in USaveGame and serves as the container for serialized StoryFlow state. It has two properties:

  • SaveDataJson (FString) — The JSON blob containing all serialized global variables, runtime characters, and once-only options.
  • SaveVersion (FString, default "1") — A version string for future compatibility. If the save format changes in a future plugin update, this field allows the deserializer to handle older saves gracefully.

Why JSON Instead of Unreal Reflection?

StoryFlow uses JSON serialization rather than Unreal's built-in reflection-based serialization. The reason: FStoryFlowVariant::ArrayValue is not a UPROPERTY because Unreal Header Tool (UHT) does not support recursive struct arrays. If Unreal's native serialization were used, array variable data would be silently dropped on save. JSON serialization handles nested and recursive data structures correctly, ensuring arrays of any depth are preserved.

Save & Load Functions

All save/load functions live on UStoryFlowSubsystem and are BlueprintCallable, so you can use them from both C++ and Blueprints.

Saving Progress

SaveToSlot serializes the current global variables, runtime characters, and once-only options into a JSON string, wraps it in a UStoryFlowSaveGame object, and writes it to the specified save slot.

C++
// C++ - Save current StoryFlow state
UStoryFlowSubsystem* StoryFlow = GetGameInstance()->GetSubsystem<UStoryFlowSubsystem>();

bool bSuccess = StoryFlow->SaveToSlot("SaveSlot1", 0);
if (bSuccess)
{
    UE_LOG(LogTemp, Log, TEXT("StoryFlow state saved successfully."));
}
Code
Blueprint:
  Get StoryFlow Subsystem → Save to Slot
    Slot Name: "SaveSlot1"
    User Index: 0
    Return Value → Branch (success/failure)

Parameters:

  • SlotName (FString) — The name of the save slot. Can be any string.
  • UserIndex (int32, default 0) — The user index for the save, matching Unreal's multi-user save system.

Returns: booltrue if the save was written successfully, false on failure.

Loading Progress

LoadFromSlot reads a save file from disk, deserializes the JSON data, and replaces the current global variables, runtime characters, and once-only options with the saved values.

C++
// C++ - Load StoryFlow state
UStoryFlowSubsystem* StoryFlow = GetGameInstance()->GetSubsystem<UStoryFlowSubsystem>();

bool bSuccess = StoryFlow->LoadFromSlot("SaveSlot1", 0);
if (bSuccess)
{
    UE_LOG(LogTemp, Log, TEXT("StoryFlow state loaded successfully."));
    // Now start the appropriate script based on your game logic
}
Code
Blueprint:
  Get StoryFlow Subsystem → Load from Slot
    Slot Name: "SaveSlot1"
    User Index: 0
    Return Value → Branch
      True: Start the appropriate script
      False: Show "Load Failed" message

Parameters: Same as SaveToSlot.

Returns: booltrue if the load succeeded, false if the file does not exist, deserialization failed, or a dialogue is currently active.

Loading Refuses While Dialogue Is Active

LoadFromSlot will return false and do nothing if any StoryFlow component currently has an active dialogue. This is a safety guard — replacing variables and character data mid-execution would corrupt the runtime state. Always ensure all dialogues are stopped before attempting to load. See Active Dialogue Guard for details.

Checking & Deleting Saves

Two utility functions let you check for existing saves and delete them:

C++
// Check if a save exists
bool bExists = UStoryFlowSubsystem::DoesSaveExist("SaveSlot1", 0);

// Delete a save
bool bDeleted = UStoryFlowSubsystem::DeleteSave("SaveSlot1", 0);
Code
Blueprint:
  Does Save Exist
    Slot Name: "SaveSlot1"
    User Index: 0
    Return Value → Use to enable/disable "Continue" button

  Delete Save
    Slot Name: "SaveSlot1"
    User Index: 0
    Return Value → Branch (confirm deletion succeeded)

Both are static functions, so they can be called without a subsystem reference. They are useful for building save slot UIs — gray out empty slots, show confirmation dialogs before deleting, and so on.

JSON Serialization

The StoryFlowSaveHelpers namespace contains the low-level serialization functions. You typically do not need to call these directly — SaveToSlot and LoadFromSlot handle them internally — but they are available if you need custom save integration.

C++
// Serialize state to a JSON string
FString JsonString = StoryFlowSaveHelpers::SerializeSaveData(
    GlobalVariables,      // TMap<FString, FStoryFlowVariable>
    RuntimeCharacters,    // TMap<FString, FStoryFlowCharacterDef>
    UsedOnceOnlyOptions   // TSet<FString>
);

// Deserialize a JSON string back into state
TMap<FString, FStoryFlowVariable> OutGlobals;
TMap<FString, FStoryFlowCharacterDef> OutCharacters;
TSet<FString> OutUsedOptions;

bool bSuccess = StoryFlowSaveHelpers::DeserializeSaveData(
    JsonString,
    OutGlobals,
    OutCharacters,
    OutUsedOptions
);

SerializeSaveData takes the three data collections and produces a single FString containing valid JSON. DeserializeSaveData parses a JSON string and populates the output parameters. It returns false if the JSON is malformed or missing expected fields.

These helpers are useful if you want to embed StoryFlow state inside your own save format, transmit it over a network, or store it in a database rather than using Unreal's save slot system.

Reset Functions

The subsystem provides three reset functions for returning state to project defaults:

  • ResetGlobalVariables() — Resets all global variables to their default values as defined in the project. Does not affect characters or once-only options.
  • ResetRuntimeCharacters() — Resets all character definitions to their project defaults, reverting any in-game modifications to character variables, names, or colors.
  • ResetAllState() — Resets global variables, runtime characters, and clears the once-only options tracking. This is a full reset intended for a "New Game" flow.
C++
// New Game flow
UStoryFlowSubsystem* StoryFlow = GetGameInstance()->GetSubsystem<UStoryFlowSubsystem>();

// Reset everything to project defaults
StoryFlow->ResetAllState();

// Optionally delete the old save
UStoryFlowSubsystem::DeleteSave("SaveSlot1", 0);

// Start the opening script
StoryFlowComponent->StartDialogueWithScript(TEXT("scripts/intro.json"));
Code
Blueprint - New Game:
  Get StoryFlow Subsystem → Reset All State
  Delete Save (Slot Name: "SaveSlot1")
  StoryFlow Component → Start Dialogue With Script ("scripts/intro.json")

Active Dialogue Guard

The subsystem tracks how many StoryFlow components currently have active dialogues via internal NotifyDialogueStarted and NotifyDialogueEnded calls. The IsDialogueActive() function returns true if any component has an active dialogue.

Never Load During Active Dialogue

LoadFromSlot checks IsDialogueActive() before proceeding. If any dialogue is active, the load is rejected and the function returns false. This prevents a class of bugs where:

  • Variable values change underneath a running script, causing incorrect branching
  • Character data is swapped mid-dialogue, producing mismatched names or portraits
  • Once-only option state is cleared while the player is viewing those options

How to handle this in practice:

C++
UStoryFlowSubsystem* StoryFlow = GetGameInstance()->GetSubsystem<UStoryFlowSubsystem>();

// Check before loading
if (StoryFlow->IsDialogueActive())
{
    // Stop all active dialogues first, or show a message to the player
    UE_LOG(LogTemp, Warning, TEXT("Cannot load while dialogue is active."));
    return;
}

bool bSuccess = StoryFlow->LoadFromSlot("SaveSlot1", 0);
Code
Blueprint - Safe Load:
  Get StoryFlow Subsystem → Is Dialogue Active
    → Branch
      True: Show "Please close the dialogue first" notification
      False: Load from Slot → Start appropriate script

If your game has a pause menu where the player can load saves, ensure that opening the pause menu either stops all active dialogues or disables the load option while dialogue is running.

Integration with Game Saves

StoryFlow's save system uses Unreal's standard USaveGame / save slot infrastructure. This gives you flexibility in how you integrate it with your game's own save system.

Paired Slot Strategy

Use a consistent naming convention to pair your game save and StoryFlow save slots. Add a prefix like "SF_" to the StoryFlow slot to avoid collision with your game's save data, since both use Unreal's USaveGame system under the hood.

Code
Blueprint - Save Game:
  // Save your game state
  Save Game to Slot (MyGameSave, "Slot1", 0)

  // Save StoryFlow state with a "SF_" prefix to avoid collision
  Get StoryFlow Subsystem → Save to Slot ("SF_Slot1", 0)

Blueprint - Load Game:
  // Load StoryFlow state first (so variables are ready)
  Get StoryFlow Subsystem → Load from Slot ("SF_Slot1", 0)

  // Load your game state
  Load Game from Slot ("Slot1", 0)

  // Resume from the appropriate script
  StoryFlow Component → Start Dialogue With Script (loaded scene name)

Separate Slot Strategy

Use a different slot name for StoryFlow, such as appending _story to your game's slot name. This is useful if you want to manage StoryFlow saves independently.

Code
Game save slot:     "Slot1"
StoryFlow slot:     "Slot1_story"

Game save slot:     "AutoSave"
StoryFlow slot:     "AutoSave_story"

New Game Flow

When the player starts a new game, reset all StoryFlow state and optionally clear the save:

Code
Blueprint - New Game:
  1. Get StoryFlow Subsystem → Reset All State
  2. Delete Save ("Slot1")         // optional: clear old save
  3. Initialize your game state
  4. StoryFlow Component → Start Dialogue With Script ("scripts/intro.json")

Continue / Load Game Flow

When the player loads an existing save:

Code
Blueprint - Continue Game:
  1. Does Save Exist ("Slot1") → Branch
       False: Show "No save found" → Return to main menu
       True:  Continue to step 2
  2. Get StoryFlow Subsystem → Load from Slot ("Slot1")
  3. Load your game state from slot
  4. Read "currentScene" global variable from StoryFlow
  5. StoryFlow Component → Start Dialogue With Script (currentScene value)

Tracking the Current Scene

Since StoryFlow saves do not include execution position, you need a way to know which script to start after loading. A common pattern is to create a global string variable called something like currentScene and update it each time the player enters a new scene. On load, read this variable and use it to start the correct script.

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