Skip to main content

Save & Load

The StoryFlow Editor plugin uses Unreal Engine's native USaveGame infrastructure. Save global variables, character state, once-only options and Data Asset changes between play sessions with 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 and maps. Loading restores saved map text as written; it does not translate that text into a newly selected language.
  • 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.
  • Data Asset changes - Values written to Data Assets during the session. Authored defaults and parent relationships remain in the imported project.

Save Compatibility

Current saves include Data Asset changes in the same state as globals, characters and once-only options. Older saves without Data Asset changes can still load. When you load one, Data Assets use their imported values. Keep a backup of existing save slots when changing plugin versions or revising the exported project.

Character records in saves are matched by path. Stable IDs in a new project export do not migrate those saved paths after a character file is moved or renamed. Loading a save also replaces the character records with those stored in that slot, so characters added since the save are not included. Plan a save migration or require a new game when making these changes between releases.

The player's language preference is separate from story state. Save it in your game's settings and restore it with SetLanguage. See Localization.

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 globals, runtime characters, once-only options and Data Asset changes.
  • 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. Since v1.2.0, FStoryFlowVariant::MapValue is the second non-UPROPERTY field persisted via SerializedArrayData alongside ArrayValue, for the same reason (map entries hold variants recursively). If Unreal's native serialization were used, array and map variable data would be silently dropped on save. JSON serialization handles nested and recursive data structures correctly, ensuring arrays and maps 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 global variables, runtime characters, once-only options and Data Asset changes into a UStoryFlowSaveGame object, then writes it to the specified 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: bool - true if the save was written successfully, false on failure.

Loading Progress

LoadFromSlot reads a save file and restores global variables, runtime characters, once-only options and Data Asset changes.

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: bool - true 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.

Most games should use SaveToSlot and LoadFromSlot. If you maintain custom save integration in C++, update calls to StoryFlowSaveHelpers::SerializeSaveData and DeserializeSaveData to pass the Data Asset seed and session overlay as well as globals, characters and once-only options.

Reset Functions

The subsystem provides 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.
  • ResetDataAssetOverlay() - Clears Data Asset changes from this session and reveals imported values again.
  • ResetAllState() - Resets globals, runtime characters, once-only options and the Data Asset overlay. Use it for a new game.
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