248 lines
6.5 KiB
Markdown
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)
|