Loading...
Loading...
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.
npx skill4agent add thedivergentai/gd-agentic-skills godot-save-load-systemsMANDATORY - For Production: Read save_migration_manager.gd before shipping to handle schema changes.
"version": "1.0.0"FileAccess.open("C:/Users/...")user://save_data["player"] = $Playerplayer.save_data()var file = FileAccess.open(...).close()close()store_vardata.get("health", 100)_physics_process# 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")# 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
)# 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)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 datapersist# 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)user://# ✅ Good - platform-independent
const SAVE_PATH := "user://savegame.save"
# ❌ Bad - hardcoded path
const SAVE_PATH := "C:/Users/Player/savegame.save"user://%APPDATA%\Godot\app_userdata\[project_name]~/Library/Application Support/Godot/app_userdata/[project_name]~/.local/share/godot/app_userdata/[project_name]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 datafunc 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 truevar 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")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")# ✅ Solution: Store as x, y, z components
"position": {"x": pos.x, "y": pos.y}
# Then reconstruct:
position = Vector2(data.position.x, data.position.y)# ✅ Store resource paths as strings
"texture_path": texture.resource_path
# Then reload:
texture = load(data.texture_path)