
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-systemsAdd your badge
Show developers this skill is listed on Skillselion. Paste this into your README.
| Installs | 317 |
|---|---|
| repo stars | ★ 454 |
| Last updated | July 28, 2026 |
| Repository | thedivergentai/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
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 theuser://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
DictionaryorResourceinstead. - 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()oradd()inside a loop over the same dictionary causes iteration errors. Usedata.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
StringUUIDs 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
trueallows 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
Vector3to a string or dictionary. For strict data types, usevar_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 dataPattern 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 data3. 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 true4. 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
# save_load_patterns.gd
extends Node
# 1. Generating Save Data via Groups
# EXPERT NOTE: Gather all nodes tagged "Persist" seamlessly across the Entire SceneTree.
func serialize_world_state() -> Array[Dictionary]:
var nodes := get_tree().get_nodes_in_group(&"Persist")
# map returns a new array of dictionaries provided by the node's individual save() methods
var save_data: Array[Dictionary] = []
for node in nodes:
if node.has_method(&"save"):
save_data.append(node.call(&"save"))
return save_data
# 2. Writing JSON to user://
# EXPERT NOTE: Safely open the persistent user directory and stringify data for human-readability.
func save_to_json_file(path: String, data: Dictionary) -> void:
var file := FileAccess.open("user://" + path, FileAccess.WRITE)
if file:
file.store_line(JSON.stringify(data))
# 3. Reading and Parsing JSON
# EXPERT NOTE: Reads files line-by-line to extract dictionaries safely.
func load_from_json_file(path: String) -> Variant:
if not FileAccess.file_exists("user://" + path):
return null
var file := FileAccess.open("user://" + path, FileAccess.READ)
var json_string := file.get_as_text()
return JSON.parse_string(json_string)
# 4. Binary Serialization (Fastest)
# EXPERT NOTE: Use Godot's native Variant serialization for massive performance and complex types.
func save_binary_snapshot(path: String, data: Dictionary) -> void:
var file := FileAccess.open("user://" + path, FileAccess.WRITE)
if file:
# 'false' explicitly disables object decoding (full Objects) for security.
file.store_var(data, false)
# 5. Saving Config Files (INI format)
# EXPERT NOTE: Create human-readable preference settings for simple key-value pairs.
func update_config_setting(section: String, key: String, value: Variant) -> void:
var config := ConfigFile.new()
config.load("user://settings.cfg") # Load existing
config.set_value(section, key, value)
config.save("user://settings.cfg")
# 6. Validating File Existence
# EXPERT NOTE: Always check if a save exists before attempting to read it to prevent errors.
func check_save_data_integrity(path: String) -> bool:
return FileAccess.file_exists("user://" + path)
# 7. Generating Unique IDs
# EXPERT NOTE: Create persistent IDs for nodes so they reconnect cleanly after load.
func get_persist_id(node: Node) -> String:
# Uses scene unique ID if available, or custom logic
return str(node.get_instance_id())
# 8. Safely Destroying Old State before Load
# EXPERT NOTE: Delete old persistent objects before loading new ones to prevent duplication.
func wipe_persist_group() -> void:
var save_nodes := get_tree().get_nodes_in_group(&"Persist")
for node in save_nodes:
node.queue_free()
# 9. Saving Resources Directly
# EXPERT NOTE: Serialize full Godot Resource objects directly to disk (.tres or .res).
func save_stat_resource(res: Resource, path: String) -> void:
ResourceSaver.save(res, "user://" + path)
# 10. Threaded Scene/File Loading
# EXPERT NOTE: Push heavy scene loading or resource parsing to background cores.
func load_level_async(scene_path: String) -> void:
ResourceLoader.load_threaded_request(scene_path)
# skills/save-load-systems/code/save_migration_manager.gd
extends Node
## Save System Expert Pattern
## Implements AES256 Encryption and Schema Versioning.
const SAVE_PATH = "user://savegame.dat"
const ENCRYPTION_KEY = "SECRET_XP_KEY_2026" # Should be stored securely
const CURRENT_VERSION = 2
# 1. Encryption Protocols
# Expert logic: Protect player data from trivial hex editing.
func save_game(data: Dictionary) -> void:
data["_version"] = CURRENT_VERSION
var file = FileAccess.open_encrypted_with_pass(SAVE_PATH, FileAccess.WRITE, ENCRYPTION_KEY)
if file:
file.store_var(data)
file.close()
func load_game() -> Dictionary:
if not FileAccess.file_exists(SAVE_PATH): return {}
var file = FileAccess.open_encrypted_with_pass(SAVE_PATH, FileAccess.READ, ENCRYPTION_KEY)
if not file: return {}
var data = file.get_var()
file.close()
# 2. Data Migration Framework
# Professional pattern: Handle 'Schema Evolution' automatically.
var save_version = data.get("_version", 1)
if save_version < CURRENT_VERSION:
data = _migrate_data(data, save_version)
return data
func _migrate_data(old_data: Dictionary, from_version: int) -> Dictionary:
print("Migrating save from v", from_version, " to v", CURRENT_VERSION)
if from_version == 1:
# Add a new 'mana' field that didn't exist in v1
old_data["mana"] = 100
from_version = 2
return old_data
## EXPERT NOTE:
## Use 'ResourceSaver Serialization': To save complex custom
## Resources (like inventory or full game-states), use
## 'ResourceSaver.save(my_resource, path)'. This preserves types and
## nested objects better than JSON.
## NEVER save absolute machine paths in save files; always use
## 'user://' to ensure cross-platform compatibility (Windows vs Mac).
## For 'Cloud-Proxy Syncing', implement a 'Snapshot' system that
## creates a temporary unencrypted buffer specifically for Steam
## Cloud or Epic Online Services APIs.
# skills/save-load-systems/scripts/save_system_encryption.gd
extends Node
## Save System Encryption Expert Pattern
## Encrypted save files with AES-256 and optional compression.
class_name SaveSystemEncryption
const ENCRYPTION_KEY := "your-32-character-key-here!!" # 32 bytes for AES-256
const SAVE_PATH := "user://savegame.enc"
func save_encrypted(data: Dictionary) -> bool:
var json_string := JSON.stringify(data)
var buffer := json_string.to_utf8_buffer()
# Optional: Compress
var compressed := buffer.compress(FileAccess.COMPRESSION_DEFLATE)
# Encrypt with AES-256
var ctx := AESContext.new()
var key := ENCRYPTION_KEY.to_utf8_buffer()
ctx.start(AESContext.MODE_ECB_ENCRYPT, key)
var encrypted := ctx.update(compressed)
ctx.finish()
# Save to file
var file := FileAccess.open(SAVE_PATH, FileAccess.WRITE)
if not file:
return false
file.store_buffer(encrypted)
file.close()
return true
func load_encrypted() -> Dictionary:
if not FileAccess.file_exists(SAVE_PATH):
return {}
var file := FileAccess.open(SAVE_PATH, FileAccess.READ)
if not file:
return {}
var encrypted := file.get_buffer(file.get_length())
file.close()
# Decrypt
var ctx := AESContext.new()
var key := ENCRYPTION_KEY.to_utf8_buffer()
ctx.start(AESContext.MODE_ECB_DECRYPT, key)
var compressed := ctx.update(encrypted)
ctx.finish()
# Decompress
var buffer := compressed.decompress_dynamic(-1, FileAccess.COMPRESSION_DEFLATE)
# Parse JSON
var json_string := buffer.get_string_from_utf8()
var json := JSON.new()
if json.parse(json_string) != OK:
push_error("Failed to parse decrypted save data")
return {}
return json.data
## EXPERT NOTES:
## - ENCRYPTION_KEY should be generated per project, store securely
## - Use CryptoKey for better key management
## - This prevents casual save editing, NOT determined attackers
## - For multiplayer/leaderboards, validate server-side
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.