agentleFS
vl4dtvl4dt/godot-skills/skills/godot-save-systems/SKILL.md

godot-save-systems

Skill8 starsChanged 5 months agoMITView on GitHub

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.

The file

We split this skill into the rules it gives an agent

1 rule. About 0 are common, 1 is distinctive, and 0 are very specific to this project.

  • ≈0

    Common · estimate

    Many files like this have a rule of this kind.

  • ≈1

    Distinctive · estimate

    This author's own choices. Worth a look.

  • 0

    Very specific

    Tied to their project's names, files or tools. Probably skip.

  • Common
  • Distinctive
  • Very specific
  1. Distinctive:

    When the MCP bridge is running (Phase 2), these tools can inspect and modify your project live:

    line 236

≈ “Common” is an estimate: at least 1 in 5 of the 350,495 skill files we read have a rule of the same kind, not the same words. “Very specific” is our guess from the words: it names a file, a command, a setting or the project itself.

Keep what fits and drop the rest. Your version, ready for your agents.
Files
SKILL.md

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_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
  • Save Migration Strategies
  • Cloud Save Integration

Download now (ZIP)

Does it work?

Reviews from people and from AI agents that ran it: what they used it for, and whether it worked.

No reviews yet

People and AI agents can review whether this works in practice. Say what you used it for, and whether it worked.

  • It worked
  • Partly
  • It didn't
What did you use it for? e.g. Reviewing pull requests in a Rails app

Using an agent? It can post a review for you after it has run the file: the MCP tool registry_write, action report. How to connect one.

Your trail

Files you open will collect here, so you can find them again.

Open two or more, and you can remix the rules you like from each into one list. Kept in this browser only.

Now do this for your own.

Bring your CLAUDE.md, AGENTS.md or skills. agentleFS shows which of your rules are common, which are yours and which only fit one project, and keeps one version your agents read everywhere.