Claude
Skills
Sign in
Back

godot-save-load-systems

Included with Lifetime
$97 forever

Expert blueprint for save/load systems using JSON/binary serialization, PERSIST group pattern, versioning, and migration. Covers player progress, settings, game state persistence, and error recovery. Use when implementing save systems OR data persistence. Keywords save, load, JSON, FileAccess, user://, serialization, version migration, PERSIST group.

Generalscripts

What this skill does


# Save/Load Systems

JSON serialization, version migration, and PERSIST group patterns define robust data persistence.

## NEVER Do

- **NEVER save without a version field** — When you update your game's data structure, old saves will break. Always include a `"version": "1.0.0"` field and implement migration logic.
- **NEVER use absolute OS paths** — Hardcoding `C:/Users/...` will break on every other machine. Always use the `user://` protocol, which Godot maps to the correct OS-specific app data folder.
- **NEVER attempt to save Node references directly** — Nodes are objects, not raw data. Extract the necessary primitive data (positions, health, levels) into a `Dictionary` or `Resource` instead.
- **NEVER forget to close FileAccess handles** — Leaving a file open can lead to handle leaks and save-file corruption. In Godot 4, files auto-close when the variable goes out of scope, but explicit `close()` is safer for long-running logic.
- **NEVER use JSON for very large binary data** — Storing 10MB of texture data as Base64 in JSON is slow and bloats file size. Use binary `store_var()` or separate dedicated asset files.
- **NEVER trust loaded data without validation** — Users can edit save files. Always use `data.get("field", default_value)` and validate that numbers are within expected ranges to prevent crashes.
- **NEVER trigger a save during high-frequency physics or animation updates** — A crash mid-write will corrupt the file. Save only on explicit game events like entering a menu, finishing a level, or at a checkpoint.
- **NEVER modify a save Dictionary while iterating over its keys** — Calling `erase()` or `add()` inside a loop over the same dictionary causes iteration errors. Use `data.duplicate()` to iterate safely.
- **NEVER store raw passwords or sensitive credentials in unencrypted JSON** — If you have sensitive data, use `FileAccess.open_encrypted_with_pass()` to secure it.
- **NEVER use ResourceLoader.load() for massive scenes on the main thread** — It causes a visible freeze. Use `ResourceLoader.load_threaded_request()` to load levels in the background.
- **NEVER rely on get_instance_id() for cross-session identification** — These IDs are assigned at runtime and change every time the game restarts. Generate your own persistent `String` UUIDs for game objects.
- **NEVER forget to call duplicate(true) on a loaded Resource stats block** — If multiple enemies load the same "goblin_stats.tres", they will all share the same health pool unless duplicated.
- **NEVER use the "allow_objects" flag in store_var/get_var for untrusted data** — Setting this to `true` allows full object decoding, which is a major security risk for saves downloaded from the web.
- **NEVER use JSON for data requiring strict type preservation** — JSON converts `Vector3` to a string or dictionary. For strict data types, use `var_to_bytes()` or a binary format.
- **NEVER leave internal metadata (set_meta) in persistent dictionaries** — This unnecessarily inflates save file size. Clean your dictionaries before serialization.

---

## Available Scripts

> **MANDATORY**: Read the appropriate script before implementing the corresponding pattern.

### [save_load_patterns.gd](scripts/save_load_patterns.gd)
10 Expert patterns: PERSIST group serialization, binary snapshots, JSON safe-parsing, and threaded loading.

### [save_migration_manager.gd](scripts/save_migration_manager.gd)
Expert save file versioning with automatic migration between schema versions.

### [save_system_encryption.gd](scripts/save_system_encryption.gd)
AES-256 encrypted saves with compression to prevent casual save editing.

---

### Pattern 1: JSON Save System (Recommended for Most Games)

#### Step 1: Create SaveManager AutoLoad

```gdscript
# save_manager.gd
extends Node

const SAVE_PATH := "user://savegame.save"

## Save data to JSON file
func save_game(data: Dictionary) -> void:
    var save_file := FileAccess.open(SAVE_PATH, FileAccess.WRITE)
    if save_file == null:
        push_error("Failed to open save file: " + str(FileAccess.get_open_error()))
        return
    
    var json_string := JSON.stringify(data, "\t")  # Pretty print
    save_file.store_line(json_string)
    save_file.close()
    print("Game saved successfully")

## Load data from JSON file
func load_game() -> Dictionary:
    if not FileAccess.file_exists(SAVE_PATH):
        push_warning("Save file does not exist")
        return {}
    
    var save_file := FileAccess.open(SAVE_PATH, FileAccess.READ)
    if save_file == null:
        push_error("Failed to open save file: " + str(FileAccess.get_open_error()))
        return {}
    
    var json_string := save_file.get_as_text()
    save_file.close()
    
    var json := JSON.new()
    var parse_result := json.parse(json_string)
    if parse_result != OK:
        push_error("JSON Parse Error: " + json.get_error_message())
        return {}
    
    return json.data as Dictionary

## Delete save file
func delete_save() -> void:
    if FileAccess.file_exists(SAVE_PATH):
        DirAccess.remove_absolute(SAVE_PATH)
        print("Save file deleted")
```

#### Step 2: Save Player Data

```gdscript
# player.gd
extends CharacterBody2D

var health: int = 100
var score: int = 0
var level: int = 1

func save_data() -> Dictionary:
    return {
        "health": health,
        "score": score,
        "level": level,
        "position": {
            "x": global_position.x,
            "y": global_position.y
        }
    }

func load_data(data: Dictionary) -> void:
    health = data.get("health", 100)
    score = data.get("score", 0)
    level = data.get("level", 1)
    if data.has("position"):
        global_position = Vector2(
            data.position.x,
            data.position.y
        )
```

#### Step 3: Trigger Save/Load

```gdscript
# game_manager.gd
extends Node

func save_game_state() -> void:
    var save_data := {
        "player": $Player.save_data(),
        "timestamp": Time.get_unix_time_from_system(),
        "version": "1.0.0"
    }
    SaveManager.save_game(save_data)

func load_game_state() -> void:
    var data := SaveManager.load_game()
    if data.is_empty():
        print("No save data found, starting new game")
        return
    
    if data.has("player"):
        $Player.load_data(data.player)
```

### Pattern 2: Binary Save System (Advanced, Faster)

For large save files or when human-readability isn't needed:

```gdscript
const SAVE_PATH := "user://savegame.dat"

func save_game_binary(data: Dictionary) -> void:
    var save_file := FileAccess.open(SAVE_PATH, FileAccess.WRITE)
    if save_file == null:
        return
    
    save_file.store_var(data, true)  # true = full objects
    save_file.close()

func load_game_binary() -> Dictionary:
    if not FileAccess.file_exists(SAVE_PATH):
        return {}
    
    var save_file := FileAccess.open(SAVE_PATH, FileAccess.READ)
    if save_file == null:
        return {}
    
    var data: Dictionary = save_file.get_var(true)
    save_file.close()
    return data
```

### Pattern 3: PERSIST Group Pattern

For auto-saving nodes with the `persist` group:

```gdscript
# Add nodes to "persist" group in editor or via code:
add_to_group("persist")

# Implement save/load in each persistent node:
func save() -> Dictionary:
    return {
        "filename": get_scene_file_path(),
        "parent": get_parent().get_path(),
        "pos_x": position.x,
        "pos_y": position.y,
        # ... other data
    }

func load(data: Dictionary) -> void:
    position = Vector2(data.pos_x, data.pos_y)
    # ... load other data

# SaveManager collects all persist nodes:
func save_all_persist_nodes() -> void:
    var save_nodes := get_tree().get_nodes_in_group("persist")
    var save_dict := {}
    
    for node in save_nodes:
        if not node.has_method("save"):
            continue
        save_dict[node.name] = node.save()
    
    save_game(save_dict)
```

## Best Practices

### 1. Use `user://` Protocol
```gdscript
# ✅ Good - platform-

Related in General