6.5 KiB
6.5 KiB
| name | description | license | compatibility | metadata | |||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| godot-save-systems | Save system patterns for Godot 4.x: resource-based saves, JSON vs binary serialization, versioning/migration strategies, cloud save integration, and corruption recovery. Use when implementing persistence in Godot games. | MIT |
|
|
Save Systems in Godot
Patterns for persisting game state reliably.
Resource-Based Saves (Recommended)
Godot's Resource system provides native serialization with type safety:
class_name SaveData extends Resource
const SAVE_VERSION := 1
@export var save_version: int = SAVE_VERSION
@export var timestamp: float = 0.0
@export var player_position: Vector2 = Vector2.ZERO
@export var player_health: int = 100
@export var inventory: Array[Resource] = []
@export var quest_flags: Dictionary = {}
Save Manager
class_name SaveManager extends Node
const SAVE_PATH := "user://savegame.tres"
func save(data: SaveData) -> Error:
data.timestamp = Time.get_unix_time_from_system()
var error = ResourceSaver.save(data, SAVE_PATH)
if error != Error.OK:
push_error("SaveManager: Failed to save — %d" % error)
return error
func load() -> SaveData:
if not FileAccess.file_exists(SAVE_PATH):
return null
var data = ResourceLoader.load(SAVE_PATH) as SaveData
if not data:
push_error("SaveManager: Failed to load save file")
return null
return data
func delete() -> void:
DirAccess.remove_absolute(SAVE_PATH)
func has_save() -> bool:
return FileAccess.file_exists(SAVE_PATH)
Multiple Save Slots
func get_slot_path(slot_index: int) -> String:
return "user://savegame_%d.tres" % slot_index
# Save slot metadata for UI display
class_name SlotInfo extends Resource
@export var slot_index: int = 0
@export var timestamp: float = 0.0
@export var player_level: int = 1
@export var thumbnail: Texture2D = null
JSON Serialization
For human-readable saves or cross-platform data exchange:
func save_as_json(data: SaveData, path: String) -> Error:
var dict := {
"version": data.save_version,
"timestamp": data.timestamp,
"position": { "x": data.player_position.x, "y": data.player_position.y },
"health": data.player_health,
"inventory": [],
"quests": data.quest_flags
}
var json = JSON.stringify(dict)
var file = FileAccess.open(path, FileAccess.WRITE)
if not file:
return Error.CANT_OPEN
file.store_string(json)
return Error.OK
func load_from_json(path: String) -> SaveData:
var file = FileAccess.open(path, FileAccess.READ)
if not file:
return null
var json_str = file.get_as_text()
var parse_result = JSON.parse_string(json_str)
if parse_result is Dictionary:
return _dict_to_save_data(parse_result as Dictionary)
return null
Versioning and Migration
Handle save format changes across game updates:
func load() -> SaveData:
var raw = ResourceLoader.load(SAVE_PATH) as SaveData
if not raw:
return null
# Migrate older save formats to current version
while raw.save_version < SaveData.SAVE_VERSION:
raw = _migrate(raw, raw.save_version)
return raw
func _migrate(data: SaveData, from_version: int) -> SaveData:
match from_version:
0:
# v0 → v1: add quest_flags field
data.quest_flags = {}
data.save_version = 1
_:
push_warning("SaveManager: Unknown migration from v%d" % from_version)
return data
Corruption Recovery
Protect against corrupted save files:
func load_safe() -> SaveData:
var data = load()
if data and _validate(data):
return data
# Try backup
if FileAccess.file_exists(SAVE_PATH + ".bak"):
push_warning("SaveManager: Primary save corrupted, loading backup")
var temp = SAVE_PATH
OS.move_to_trash(SAVE_PATH) # Move corrupted file
DirAccess.copy_absolute(SAVE_PATH + ".bak", SAVE_PATH)
return load()
push_error("SaveManager: No valid save found")
return null
func _validate(data: SaveData) -> bool:
if not data:
return false
if data.save_version < 1:
return false
# Add more validation checks
return true
# Create backup on successful save
func save_with_backup(data: SaveData) -> Error:
# Backup current save
if FileAccess.file_exists(SAVE_PATH):
DirAccess.copy_absolute(SAVE_PATH, SAVE_PATH + ".bak")
return save(data)
Cloud Save Integration Pattern
Abstract cloud provider behind an interface:
class_name CloudSaveProvider extends RefCounted
virtual func upload(data: String, key: String) -> bool:
return false
virtual func download(key: String) -> String:
return ""
virtual func delete(key: String) -> bool:
return false
# Usage: implement concrete providers
# class_name GooglePlaySaveProvider extends CloudSaveProvider
# class_name AppleCloudSaveProvider extends CloudSaveProvider
Auto-Save Strategies
class_name AutoSaver extends Node
@export var interval_seconds: float = 30.0
var _timer: Timer
var _save_manager: SaveManager
func _ready() -> void:
_timer = Timer.new()
_timer.wait_time = interval_seconds
_timer.timeout.connect(_on_auto_save)
add_child(_timer)
_timer.start()
func _on_auto_save() -> void:
var data = _build_save_data()
_save_manager.save(data)
Godot 4.7 Save Notes
- ResourceSaver supports
ResourceSaver.FLAG_COMPRESSfor smaller save files - ConfigFile — Use for settings/preferences (separate from game saves)
MCP Bridge Tools (Optional — Live Editor Integration)
When the MCP bridge is running (Phase 2), these tools can inspect and modify your project live:
script_read/script_edit— Read and modify save system scriptsproject_info— Check user:// directory paths for save location debugging
Note: MCP tools require the Godot editor to be running with the MCP plugin enabled. Skills work independently without the bridge.