Skip to main content
The Dialogue Engine makes it easy to save and load dialogue state, allowing players to continue conversations exactly where they left off.

Understanding Dialogue State

The dialogue state consists of:
  • Current entry ID: Which dialogue entry the player is at
  • Branch ID: Which branch the dialogue is currently in
  • Custom data: Game-specific variables (quest flags, relationship points, etc.)

Basic Save and Load

1

Get the current entry ID

The current entry ID uniquely identifies where the player is in the dialogue:
func save_state() -> void:
    var current_entry_id: int = dialogue_engine.get_current_entry().get_id()
    # Save this ID to a file or database
2

Save to a file

Use Godot’s FileAccess to persist the state:
const SAVE_PATH: String = "user://dialogue_save.dat"

func save_state() -> void:
    var file_handle: FileAccess = FileAccess.open(SAVE_PATH, FileAccess.WRITE)
    file_handle.store_var(dialogue_engine.get_current_entry().get_id())
    print("State Saved")
3

Load from file and restore position

Load the entry ID and use set_current_entry() to restore the position:
func load_state() -> void:
    if FileAccess.file_exists(SAVE_PATH):
        var file_handle: FileAccess = FileAccess.open(SAVE_PATH, FileAccess.READ)
        var entry_id: int = file_handle.get_var()
        
        if dialogue_engine.has_entry_id(entry_id):
            dialogue_engine.set_current_entry(entry_id)
            print("State Loaded")
Always check if an entry ID is valid with has_entry_id() before setting it. Entry IDs can become invalid if you modify your dialogue structure.

Saving Additional Game State

You typically want to save more than just the dialogue position:
const SAVE_PATH: String = "user://game_save.dat"

var counter: int = 0
var log_history: Array = []

func save_state() -> void:
    var file_handle: FileAccess = FileAccess.open(SAVE_PATH, FileAccess.WRITE)
    
    # Save dialogue position
    file_handle.store_var(dialogue_engine.get_current_entry().get_id())
    
    # Save custom game state
    file_handle.store_var(counter)
    file_handle.store_var(log_history)
    
    print("State Saved")

func load_state() -> void:
    if FileAccess.file_exists(SAVE_PATH):
        var file_handle: FileAccess = FileAccess.open(SAVE_PATH, FileAccess.READ)
        
        # Load dialogue position
        var entry_id: int = file_handle.get_var()
        if dialogue_engine.has_entry_id(entry_id):
            dialogue_engine.set_current_entry(entry_id)
        
        # Load custom game state
        counter = file_handle.get_var()
        log_history = file_handle.get_var()
        
        print("State Loaded")

Dynamic Dialogue with Save/Load

You can create infinite or dynamically generated dialogue that persists across sessions:
extends DialogueEngine

const SAVE_PATH: String = "user://save.dat"
var counter: int = 0
var log_history: Array = []

func get_log_history() -> Array:
    return log_history

func _setup() -> void:
    add_text_entry("This is an example of an infinite dynamically generated/saved/loaded dialogue.")
    add_text_entry("You can save the dialogue progress at any time by clicking the save button above.")
    add_text_entry("And when you restart this scene, the dialogue will continue from where it left off.")
    add_text_entry("As the dialogue progresses, the graph in the debugger will update automatically as well.")
    add_text_entry("Let's count to infinity!!")
    
    # Track history
    dialogue_continued.connect(__log_history)
    
    # Generate new entries dynamically
    dialogue_about_to_finish.connect(__continue_counting)
    
    # Load previous state if any
    load_state()

func __log_history(p_dialogue_entry: DialogueEntry) -> void:
    # Always track the log history:
    log_history.push_back(p_dialogue_entry.get_formatted_text())

func __continue_counting() -> void:
    counter += 1
    add_text_entry(str(counter))

func save_state() -> void:
    var file_handle: FileAccess = FileAccess.open(SAVE_PATH, FileAccess.WRITE)
    file_handle.store_var(counter)
    file_handle.store_var(get_current_entry().get_id())
    file_handle.store_var(log_history)
    print("State Saved")

func load_state() -> void:
    if FileAccess.file_exists(SAVE_PATH):
        var file_handle: FileAccess = FileAccess.open(SAVE_PATH, FileAccess.READ)
        counter = file_handle.get_var()
        var entry_id: int = file_handle.get_var()
        
        if has_entry_id(entry_id):
            set_current_entry(entry_id)
        else:
            # Entry doesn't exist yet, create it
            set_current_entry(
                add_text_entry("Let's continue counting!!").get_id()
            )
        
        log_history = file_handle.get_var()
        print("State Loaded")

func clear_state() -> void:
    if FileAccess.file_exists(SAVE_PATH):
        DirAccess.remove_absolute(SAVE_PATH)
        print("State Cleared")
The dialogue_about_to_finish signal is perfect for generating new dialogue entries dynamically, creating endless conversations.

Entry ID Validation

When loading saved games, always validate entry IDs:
func load_state() -> void:
    if FileAccess.file_exists(SAVE_PATH):
        var file_handle: FileAccess = FileAccess.open(SAVE_PATH, FileAccess.READ)
        var saved_entry_id: int = file_handle.get_var()
        
        # Check if the entry still exists
        if dialogue_engine.has_entry_id(saved_entry_id):
            dialogue_engine.set_current_entry(saved_entry_id)
            print("Loaded successfully at entry %d" % saved_entry_id)
        else:
            # Entry doesn't exist - dialogue structure may have changed
            print("Warning: Saved entry ID %d no longer exists" % saved_entry_id)
            # Handle gracefully - maybe start from beginning
            dialogue_engine.reset()

Saving Entire Dialogue Trees

You can also save and restore the entire dialogue tree structure:
func save_dialogue_tree() -> void:
    var file_handle: FileAccess = FileAccess.open(SAVE_PATH, FileAccess.WRITE)
    
    # Get the entire dialogue tree data
    var dialogue_data: Array[Dictionary] = dialogue_engine.get_data()
    
    # Save it
    file_handle.store_var(dialogue_data)
    file_handle.store_var(dialogue_engine.get_current_entry_id())

func load_dialogue_tree() -> void:
    if FileAccess.file_exists(SAVE_PATH):
        var file_handle: FileAccess = FileAccess.open(SAVE_PATH, FileAccess.READ)
        
        # Restore the dialogue tree
        var dialogue_data: Array[Dictionary] = file_handle.get_var()
        dialogue_engine.set_data(dialogue_data)
        
        # Restore position
        var entry_id: int = file_handle.get_var()
        dialogue_engine.set_current_entry(entry_id)
Saving the entire tree is useful for procedurally generated dialogue that needs to persist exactly as it was. However, it uses more storage than just saving the entry ID.

Key Methods

MethodDescription
get_current_entry() -> DialogueEntryReturns the current dialogue entry
get_current_entry_id() -> intReturns the current entry ID
set_current_entry(id: int)Sets the current entry by ID
has_entry_id(id: int) -> boolChecks if an entry ID exists
get_data() -> Array[Dictionary]Returns the entire dialogue tree data
set_data(data: Array[Dictionary])Restores dialogue tree from data
reset()Resets dialogue to the beginning

Best Practices

Entry IDs can become invalid if you modify your dialogue script:
if dialogue_engine.has_entry_id(saved_id):
    dialogue_engine.set_current_entry(saved_id)
else:
    # Handle invalid ID gracefully
    dialogue_engine.reset()
Just the entry ID might not be enough. Save related game state too:
file.store_var({
    "entry_id": dialogue_engine.get_current_entry_id(),
    "branch_id": dialogue_engine.get_branch_id(),
    "quest_flags": quest_flags,
    "relationships": npc_relationships
})
Add a version number to handle dialogue changes:
const SAVE_VERSION: int = 1

func save_state() -> void:
    file.store_var(SAVE_VERSION)
    file.store_var(dialogue_engine.get_current_entry_id())
    # ... other data

func load_state() -> void:
    var version: int = file.get_var()
    if version != SAVE_VERSION:
        print("Save file is outdated")
        return
    # ... load data
If saving entire trees, compress the data:
var json_data: String = JSON.stringify(dialogue_engine.get_data())
var compressed: PackedByteArray = json_data.to_utf8_buffer().compress()
file.store_var(compressed)

Common Patterns

Auto-save on dialogue finish

func _setup() -> void:
    # ... setup dialogue ...
    
    dialogue_finished.connect(func() -> void:
        save_state()
    )

Load on startup

func _ready() -> void:
    dialogue_engine = DialogueScript.new()
    # Dialogue engine automatically calls load_state() in _setup()

Checkpoint system

var checkpoints: Array[int] = []

func create_checkpoint() -> void:
    checkpoints.append(dialogue_engine.get_current_entry_id())

func restore_checkpoint(index: int) -> void:
    if index < checkpoints.size():
        dialogue_engine.set_current_entry(checkpoints[index])

Next Steps

Build docs developers (and LLMs) love