Skip to main content

Save & Load

Persist dialogue state across sessions: variables, visited nodes, dialogue progress, actor data, relationships, and mid-dialogue snapshots.

What Gets Saved

DialogueCraftSaveData (version 2) stores all persistent dialogue state in a single serializable object:

DataFieldDescription
Global variablesvariablesAll global variable values (int, float, bool, string)
Visited nodesvisitedNodesPer-dialogue visit tracking with count and timestamps
Dialogue statedialogueStatesPlay count, completion status, timestamps per dialogue
Actor variablesactorDataPer-character variable values (e.g., Friendship, Mood)
RelationshipsrelationshipsDirectional relationships between actors (from/to/type/value)
Mid-dialogue statemidDialogueStateSnapshot for resuming a dialogue in progress
LanguagecurrentLanguageActive localization language code
Play timetotalPlayTimeTotal play time in seconds (tracked automatically)
Metadatatimestamp, saveNameUnix timestamp and display name for save slot UI

Quick Start

Add the DialogueCraftPersistence component to a GameObject in your scene, or let it auto-create as a singleton:

// Save to default slot
DialogueCraftPersistence.QuickSave();

// Load from default slot
DialogueCraftPersistence.QuickLoad();

The persistence manager automatically subscribes to all DialogueRunner instances in the scene and tracks visited nodes, play counts, and dialogue completion.

Save Slots

DialogueCraft supports up to 100 save slots (configurable via maxSaveSlots). Slots are stored in PlayerPrefs with a configurable key prefix.

Inspector Settings

FieldDefaultDescription
saveSlotPrefix"DialogueCraft_Save_"PlayerPrefs key prefix
defaultSlotName"default"Slot used by QuickSave()/QuickLoad()
maxSaveSlots10Maximum number of named save slots

Slot Management API

var persistence = DialogueCraftPersistence.Instance;

// Save to a named slot with a display name
DialogueCraftPersistence.SaveToSlot("slot1", "Chapter 2 - The Forest");

// Load from a named slot
DialogueCraftPersistence.LoadFromSlot("slot1");

// List all save slots
List<SaveSlotInfo> slots = DialogueCraftPersistence.GetAllSlotInfo();

foreach (var slot in slots)
{
Debug.Log($"{slot.saveName} - {slot.SaveTime} - {slot.PlayTimeFormatted}");
// "Chapter 2 - The Forest" - 3/15/2026 2:30 PM - "1h 45m"
}

// Check if a slot exists
bool exists = persistence.HasSaveSlot("slot1");

// Get the next available slot name (returns null if all full)
string nextSlot = persistence.GetNextAvailableSlot();

// Delete a single slot or all slots
persistence.DeleteSlot("slot1");
persistence.DeleteAllSlots();

SaveSlotInfo

The GetSaveSlotInfo(string slotName) method returns metadata without fully deserializing the save data:

public class SaveSlotInfo
{
public string slotName;
public string saveName;
public long timestamp;
public float playTime;
public bool hasMidDialogueState;

public DateTime SaveTime { get; } // Local DateTime
public string PlayTimeFormatted { get; } // "2h 30m" or "5m 12s"
}

Auto-Save

Configure event-based auto-saving through the Inspector or code. Auto-saves write to a dedicated slot (autoSaveSlotName, default "autosave").

Auto-Save Events

AutoSaveEvent is a flags enum -- combine multiple triggers:

FlagDescription
OnConversationEndAfter each dialogue completes
OnChoiceMadeWhen the player selects a choice
OnNodeVisitedEach time a node is processed
OnVariableChangedWhen any variable changes (batched per frame)
IntervalTimer-based, every autoSaveInterval seconds
var persistence = DialogueCraftPersistence.Instance;

// Enable auto-save on conversation end and variable changes
persistence.autoSaveEvents = AutoSaveEvent.OnConversationEnd | AutoSaveEvent.OnVariableChanged;

// Or add interval-based auto-save (every 60 seconds)
persistence.autoSaveEvents |= AutoSaveEvent.Interval;
persistence.autoSaveInterval = 60f; // 10-600 seconds

Variable-change auto-saves are batched: multiple changes in the same frame produce a single auto-save.

Manual Auto-Save

persistence.AutoSave(); // Saves to autoSaveSlotName

Mid-Dialogue Snapshots

Save and resume a dialogue that is in progress. The snapshot captures the exact position in the graph, including sub-dialogue call stacks, sticky choice stacks, and local variables.

Capturing a Snapshot

var runner = FindObjectOfType<DialogueRunner>();
var persistence = DialogueCraftPersistence.Instance;

// Capture the current dialogue state
persistence.CaptureMidDialogueState(runner);

// Save to a slot (snapshot is included in the save data)
persistence.Save("midgame");

The DialogueStateSnapshot stores:

  • dialogueId -- Which dialogue asset is active
  • currentNodeGuid -- The node the player is currently viewing
  • state -- The DialogueState enum value (WaitingForContinue, WaitingForChoice, etc.)
  • subDialogueStack -- Return points for nested sub-dialogues
  • stickyChoiceStack -- Return points for sticky/hub choices
  • localVariables -- All dialogue-scoped variable values
  • previousNodeGuid -- For sticky choice context

Resuming from a Snapshot

// Load save data
persistence.Load("midgame");

// Check for mid-dialogue state
if (persistence.HasMidDialogueState)
{
// Resume the dialogue on a runner
bool success = persistence.ResumeMidDialogue(runner);
}

ResumeMidDialogue calls runner.RestoreFromSnapshot(), which:

  1. Finds the DialogueAsset by dialogueId
  2. Stops any running dialogue without firing OnDialogueEnd
  3. Restores the graph, node position, and all stacks
  4. Re-presents the current node (text or choices) to the UI
  5. Fires OnDialogueStart
  6. Clears the mid-dialogue state after successful restore

A snapshot is automatically cleared when a new dialogue starts or when a dialogue ends normally.

Actor Persistence

Actor variables and relationships are synced to save data automatically. Control this via Inspector flags:

FlagDefaultDescription
syncActorVariablestrueInclude per-character fields (Friendship, Mood, etc.)
syncRelationshipstrueInclude directional relationships between actors
saveLocalizationPreferencetrueRemember the player's language choice

How Sync Works

Before each save, SyncAllState() exports the current runtime state:

persistence.SyncAllState();
// Equivalent to:
persistence.SyncGlobalVariables(); // DialogueRunner.SharedVariables.Global -> save data
persistence.SyncActorVariables(); // DialogueRunner.SharedVariables actor stores -> save data
persistence.SyncRelationships(); // DialogueRunner.SharedVariables relationships -> save data
persistence.SyncLocalization(); // LocalizationManager.CurrentLanguage -> save data

On load, ApplyToRuntime() replaces all runtime state from save data:

persistence.ApplyToRuntime();
// Clears existing variables, then imports:
// save data -> DialogueRunner.SharedVariables (global, actor, relationships)
// save data -> LocalizationManager.SetLanguage()

New Game

Reset all state and initialize from database defaults:

persistence.NewGame();

This clears all runtime variables, loads default values from VariableDatabase, and initializes actor fields from CharacterDatabase.

Node Visit Tracking

The persistence manager automatically tracks which nodes the player has visited, with enhanced data per visit.

// Check if a node was visited
bool visited = persistence.WasNodeVisited("dialogue_id", "node_guid");

// Get visit count
int count = persistence.GetNodeVisitCount("dialogue_id", "node_guid");

// Get detailed visit data
NodeVisitData data = persistence.GetNodeVisitData("dialogue_id", "node_guid");
if (data != null)
{
Debug.Log($"Visited {data.visitCount} times");
Debug.Log($"First: {data.FirstVisitTime}, Last: {data.LastVisitTime}");
}

// Dialogue-level tracking
int playCount = persistence.CurrentSaveData.GetDialoguePlayCount("dialogue_id");
bool completed = persistence.CurrentSaveData.IsDialogueCompleted("dialogue_id");

Control tracking via Inspector flags:

  • autoTrackVisitedNodes -- Track every node the runner processes (default: true)
  • autoTrackPlayCounts -- Count dialogue starts (default: true)

Runtime API Reference

Static Convenience Methods

These create the singleton if needed:

// Quick save/load (default slot)
DialogueCraftPersistence.QuickSave();
bool loaded = DialogueCraftPersistence.QuickLoad();

// Named slots
DialogueCraftPersistence.SaveToSlot("slot1", "My Save");
bool loaded = DialogueCraftPersistence.LoadFromSlot("slot1");

// JSON export/import
string json = DialogueCraftPersistence.ToJson(prettyPrint: true);
bool success = DialogueCraftPersistence.FromJson(json);

// Slot info
List<SaveSlotInfo> allSlots = DialogueCraftPersistence.GetAllSlotInfo();

// Raw data access (for custom save integration)
DialogueCraftSaveData data = DialogueCraftPersistence.GetSaveData();
DialogueCraftPersistence.SetSaveData(data);

Instance Methods

var p = DialogueCraftPersistence.Instance;

p.Save(); // Save to default slot
p.Save("slot1", "Display Name"); // Save to named slot
p.Load(); // Load from default slot
p.Load("slot1"); // Load from named slot
p.AutoSave(); // Save to auto-save slot
p.NewGame(); // Fresh state from database defaults

p.CaptureMidDialogueState(runner); // Snapshot current dialogue
p.ResumeMidDialogue(runner); // Resume from snapshot

p.SyncAllState(); // Push runtime state to save data
p.ApplyToRuntime(); // Push save data to runtime state

p.DeleteSlot("slot1"); // Delete one slot
p.DeleteAllSlots(); // Delete all slots

p.SubscribeToRunner(runner); // Track a new runner
DialogueCraftPersistence.RegisterRunner(runner); // Static version

Events

var p = DialogueCraftPersistence.Instance;

p.OnBeforeSave += (DialogueCraftSaveData data) => { /* modify before write */ };
p.OnAfterSave += (DialogueCraftSaveData data) => { /* update UI */ };
p.OnBeforeLoad += (string slotName) => { /* prepare for load */ };
p.OnAfterLoad += (DialogueCraftSaveData data) => { /* refresh UI */ };

Custom Save Integration

If your game has its own save system, bypass PlayerPrefs entirely and work with the data objects directly.

Option 1: JSON Strings

Extract and inject the full save as a JSON string:

// Get DialogueCraft state as JSON
string dialogueSaveJson = DialogueCraftPersistence.ToJson();

// Store in your save system
mySaveSystem.Set("dialogue_state", dialogueSaveJson);

// Restore later
string json = mySaveSystem.Get("dialogue_state");
DialogueCraftPersistence.FromJson(json);

Option 2: Data Object

Work with the DialogueCraftSaveData object directly:

// Get the save data object
DialogueCraftSaveData data = DialogueCraftPersistence.GetSaveData();

// Serialize with your system (JsonUtility, MessagePack, etc.)
byte[] bytes = MySerializer.Serialize(data);

// Restore later
DialogueCraftSaveData restored = MySerializer.Deserialize<DialogueCraftSaveData>(bytes);
DialogueCraftPersistence.SetSaveData(restored);

Option 3: Variables Only

If you only need to save variables (not visited nodes or dialogue state), use DialogueVariablesSaveData:

// Export just the variable state
DialogueVariablesSaveData varData = DialogueRunner.SharedVariables.GetSaveData();

// Restore later (full replace -- clears existing state)
DialogueRunner.SharedVariables.LoadSaveData(varData);

Version Migration

Save data includes a version field (currently 2). When loading version 1 data, DialogueCraftSaveData.FromJson() automatically migrates the visit tracking format. Custom serializers should preserve this field.

Dynamic Runners

If you spawn DialogueRunner instances at runtime, register them for tracking:

var runner = Instantiate(runnerPrefab);
DialogueCraftPersistence.RegisterRunner(runner);