Now liveThe Skillselion MCP - thousands of ranked skills, loaded into your agent mid-task. No install.Get it →
thedivergentai avatar

Godot Save Load Systems

  • 317 installs
  • 454 repo stars
  • Updated July 28, 2026
  • thedivergentai/gd-agentic-skills

godot-save-load-systems is a Godot 4 agent skill that implements JSON and binary save/load systems with versioning, migration, encryption, and PERSIST-group patterns for developers persisting game state.

About

godot-save-load-systems is a GD-Agentic-Skills microskill for Godot 4 that teaches production-grade persistence patterns to coding agents. The SKILL.md documents JSON and binary save flows with FileAccess, the user:// protocol, PERSIST group auto-collection, schema versioning, rolling backups, and SHA-256 integrity checks. Developers reach for godot-save-load-systems when adding player progress saves, settings files, auto-save timers, or migration logic between game updates. The skill bundles 3 GDScript scripts—save_load_patterns.gd with 10 expert patterns, save_migration_manager.gd, and save_system_encryption.gd for AES-256 encrypted saves—and lists 14 NEVER rules covering version fields, path safety, and untrusted data validation. It targets Godot 4.x projects where corrupted or editable saves would break player trust.

  • godot-save-load-systems

Godot Save Load Systems by the numbers

  • 317 all-time installs (skills.sh)
  • +28 installs in the week ending Aug 5, 2026 (Skillselion tracking)
  • Ranked #1,291 of 4,347 Backend & APIs skills by installs in the Skillselion catalog
  • Data as of Aug 5, 2026 (Skillselion catalog sync)
npx skills add https://github.com/thedivergentai/gd-agentic-skills --skill godot-save-load-systems

Add your badge

Show developers this skill is listed on Skillselion. Paste this into your README.

Listed on Skillselion
Installs317
repo stars454
Last updatedJuly 28, 2026
Repositorythedivergentai/gd-agentic-skills

How do you build versioned save systems in Godot 4?

Use godot-save-load-systems for development tasks

Who is it for?

Godot 4 game developers implementing player progress, settings persistence, auto-save, or save migration before shipping builds.

Skip if: Teams needing cloud sync backends, non-Godot engines, or UI-only work without file persistence requirements.

When should I use this skill?

A Godot 4 project needs save/load systems, schema versioning, encrypted saves, or PERSIST-group node serialization.

What you get

SaveManager AutoLoad scripts, JSON or binary save files, migration logic, PERSIST-group serializers, and encrypted backup patterns under user://.

  • SaveManager scripts
  • save game files
  • migration functions

By the numbers

  • Bundles 3 GDScript helper scripts for patterns, migration, and encryption
  • Lists 14 NEVER rules and 10 expert patterns in save_load_patterns.gd
  • Documents 3 core implementation patterns: JSON, binary, and PERSIST group

Files

SKILL.mdMarkdownGitHub ↗

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

10 Expert patterns: PERSIST group serialization, binary snapshots, JSON safe-parsing, and threaded loading.

save_migration_manager.gd

Expert save file versioning with automatic migration between schema versions.

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
# 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
# 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
# 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:

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:

# 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

# ✅ Good - platform-independent
const SAVE_PATH := "user://savegame.save"

# ❌ Bad - hardcoded path
const SAVE_PATH := "C:/Users/Player/savegame.save"

`user://` paths:

  • Windows: %APPDATA%\Godot\app_userdata\[project_name]
  • macOS: ~/Library/Application Support/Godot/app_userdata/[project_name]
  • Linux: ~/.local/share/godot/app_userdata/[project_name]

2. Version Your Save Format

const SAVE_VERSION := "1.0.0"

func save_game(data: Dictionary) -> void:
    data["version"] = SAVE_VERSION
    # ... save logic

func load_game() -> Dictionary:
    var data := # ... load logic
    if data.get("version") != SAVE_VERSION:
        push_warning("Save version mismatch, migrating...")
        data = migrate_save_data(data)
    return data

3. Handle Errors Gracefully

func save_game(data: Dictionary) -> bool:
    var save_file := FileAccess.open(SAVE_PATH, FileAccess.WRITE)
    if save_file == null:
        var error := FileAccess.get_open_error()
        push_error("Save failed: " + error_string(error))
        return false
    
    save_file.store_line(JSON.stringify(data))
    save_file.close()
    return true

4. Auto-Save Pattern

var auto_save_timer: Timer

func _ready() -> void:
    # Auto-save every 5 minutes
    auto_save_timer = Timer.new()
    add_child(auto_save_timer)
    auto_save_timer.wait_time = 300.0
    auto_save_timer.timeout.connect(_on_auto_save)
    auto_save_timer.start()

func _on_auto_save() -> void:
    save_game_state()
    print("Auto-saved")

Testing Save Systems

func _ready() -> void:
    if OS.is_debug_build():
        test_save_load()

func test_save_load() -> void:
    var test_data := {"test_key": "test_value", "number": 42}
    save_game(test_data)
    var loaded := load_game()
    assert(loaded.test_key == "test_value")
    assert(loaded.number == 42)
    print("Save/Load test passed")

Common Gotchas

Issue: Saved Vector2/Vector3 not loading correctly

# ✅ Solution: Store as x, y, z components
"position": {"x": pos.x, "y": pos.y}

# Then reconstruct:
position = Vector2(data.position.x, data.position.y)

Issue: Resource paths not resolving

# ✅ Store resource paths as strings
"texture_path": texture.resource_path

# Then reload:
texture = load(data.texture_path)

Best Practices

1. Use `user://` Protocol - Platform-independent paths 2. Version Your Save Format - Migration support 3. Handle Errors Gracefully - Validate file opening 4. Auto-Save Pattern - Periodic background saves

---

Elite Godot 4.x Patterns

1. Encrypted Save Data

Secure sensitive player progress using FileAccess.open_encrypted_with_pass(). This uses AES-256 encryption to prevent casual save editing.

# save_manager.gd
func save_encrypted(data: Variant, password: String) -> void:
    var file := FileAccess.open_encrypted_with_pass(SAVE_PATH, FileAccess.WRITE, password)
    if file:
        file.store_var(data, true) # Binary serialization
        file.close()

2. Rolling Save Slots (Backup System)

Prevent data loss during crashes by creating a rolling backup before overwriting the primary save file.

# save_manager.gd
func _create_backup() -> void:
    if FileAccess.file_exists(SAVE_PATH):
        DirAccess.copy_absolute(SAVE_PATH, BACKUP_PATH)

func safe_save(data: Variant) -> void:
    _create_backup()
    save_encrypted(data, "pass")

3. Save Integrity Validator (SHA-256)

Verify that a save file has not been tampered with or corrupted by comparing its cryptographic hash.

# save_validator.gd
func verify_save_integrity(path: String, expected_hash: String) -> bool:
    var current_hash := FileAccess.get_sha256(path)
    return current_hash == expected_hash

# On Load:
func load_with_validation() -> Variant:
    if not verify_save_integrity(SAVE_PATH, stored_hash):
        return _load_from_backup() # Fallback to backup
    return load_encrypted(SAVE_PATH, "pass")

Reference

  • Master Skill: godot-master

Related skills

How it compares

Use godot-save-load-systems for local Godot persistence; pair with other GD-Agentic-Skills for scene loading or networking when saves must sync online.

FAQ

What save formats does godot-save-load-systems support?

godot-save-load-systems documents JSON saves for most games and binary store_var/get_var snapshots for larger files. It recommends user:// paths, version fields on every save, and migration logic when schemas change between releases.

What scripts ship with godot-save-load-systems?

godot-save-load-systems bundles 3 GDScript scripts: save_load_patterns.gd with 10 expert patterns, save_migration_manager.gd for schema migrations, and save_system_encryption.gd for AES-256 encrypted compressed saves.

Does godot-save-load-systems cover cloud saves?

godot-save-load-systems focuses on local persistence with FileAccess, user:// storage, backups, and integrity validation. Cloud sync or online leaderboard storage is outside this skill's documented patterns.

Backend & APIsbackendintegrations

This week in AI coding

Five minutes, every Monday - the tools, releases and tactics for developers.

unsubscribe anytime.