Skills/godot-save-systems/SKILL.md

248 lines
6.5 KiB
Markdown

---
name: godot-save-systems
description: "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."
license: MIT
compatibility:
- godot-4.0
- godot-4.1
- godot-4.2
- godot-4.3
- godot-4.4
- godot-4.5
- godot-4.6
- godot-4.7
metadata:
author: vl4dt
version: 0.1.0
tags:
- godot
- save-system
- persistence
- serialization
- migration
created: 2026-06-21
---
# Save Systems in Godot
Patterns for persisting game state reliably.
## Resource-Based Saves (Recommended)
Godot's `Resource` system provides native serialization with type safety:
```gdscript
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
```gdscript
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
```gdscript
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:
```gdscript
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:
```gdscript
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:
```gdscript
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:
```gdot
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
```gdscript
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_COMPRESS` for 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 scripts
- **`project_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.
## References
- [Resource Save Patterns](references/resource-saves.md)
- [Save Migration Strategies](references/save-migration.md)
- [Cloud Save Integration](references/cloud-saves.md)