| Engine | Godot 4.2 or newer |
|---|---|
| Language | GDScript |
| Type | Autoload singleton (SaveManager) |
| Time to run | ~3 minutes |
| Level | Beginner |
| Output | One file (~70 lines) |
| Dependencies | None. No plugins. |
The prompt
Open a fresh chat at claude.ai/new, paste, and send.
$ Build me a Godot 4.2+ save system as a single GDScript meant to be an autoload singleton. # shape - One script, file save_manager.gd, registered as an Autoload named SaveManager. - Saves to user://save.json using FileAccess (Godot 4 API). - No plugins. # data - Hold the game state in a typed Dictionary called data with sensible defaults, e.g. { "version": 1, "level": 1, "coins": 0, "unlocked": [] }. - A DEFAULTS constant is the single source of truth for shape and defaults. # save - save() writes data to user://save.json as JSON with JSON.stringify, indented. - Open with FileAccess.open(path, FileAccess.WRITE); check it is not null before writing. # load - load_game() reads the file if it exists, parses with JSON.parse_string. - If the file is missing, unreadable, or parse returns null, fall back to a copy of DEFAULTS and do not crash. - Merge loaded values over a copy of DEFAULTS so any key added to DEFAULTS later still exists on old saves (missing-key safety). # versioning - Read the "version" field. If it is older than the current CURRENT_VERSION const, run a migrate() step (leave a clearly marked TODO spot for future migrations) and bump the version. # helpers - get_value(key, fallback) and set_value(key, value) convenience methods. - A delete_save() that removes the file. # constraints - Godot 4 FileAccess and JSON API only (no File class, no parse_json). - Never touch res:// for saving. # return format - One file. No prose before or after the code. - Brief comments only on the merge-over-defaults and the migration hook.
What this gets you
A save system that survives contact with reality: players who quit mid-level, files that get half-written on a crash, and the day three patches from now when you add a new stat and every existing save is suddenly missing that key. All three are handled instead of turning into a bug report.
It is an autoload, so anywhere in your game you can call SaveManager.set_value("coins", 10) and SaveManager.save() without wiring references around.
Why it matters
Merge over defaults
The bug that bites everyone: you ship v1, players build up saves, you add a "settings" key in v2, and now every returning player crashes because their save has no settings key. Loading by merging the saved data on top of a fresh copy of DEFAULTS means new keys always exist with a default. This one pattern prevents a whole category of live-game disasters.
A version field and a migration hook
Sooner or later the shape changes in a way a merge cannot fix, a rename, a units change. A version number and a migrate() step give you a place to transform old data forward. Adding it on day one costs nothing; retrofitting it after launch is painful.
Godot 4 file API
Godot 3 used a File class and parse_json; Godot 4 uses FileAccess and JSON.parse_string / JSON.stringify. Mixing them is the number one reason copied save code fails to run. The prompt pins the version so Claude stays in Godot 4.
What good output looks like
- Uses FileAccess, not File.
FileAccess.open(...)is Godot 4. If you seeFile.new(), that is Godot 3, regenerate with "Godot 4 only". - Null-checks the file handle. After FileAccess.open there should be an
if file == null: return(or an error check via FileAccess.get_open_error). Skipping this crashes on a locked or missing file. - Load merges over a DEFAULTS copy. Look for
data = DEFAULTS.duplicate(true)then merging loaded values in, notdata = parsedoutright. The straight assignment is what loses new keys. - Saves to user://, never res://. res:// is read-only in an exported build. The path must start user://.
How to wire it up
- Save the output as
save_manager.gd. - Open Project Settings › Globals › Autoload, add the script with the node name
SaveManager. - From anywhere, call
SaveManager.set_value("coins", 5)thenSaveManager.save(). - Call
SaveManager.load_game()on startup (for example in your main scene's _ready). - To find the file on desktop, use Editor › Open User Data Folder, you will see
save.json.
Common gotchas
The save file is never created
Either the FileAccess handle came back null and was written to anyway (check for the null guard), or the path points at res:// instead of user://. Only user:// is writable in an export.
Old saves crash after I add a field
The loader is doing data = parsed instead of merging the parsed values over a copy of DEFAULTS. Switch to the merge pattern so new keys always exist.
Parse errors on a hand-edited file
JSON.parse_string returns null on malformed JSON. The loader must treat null as "use defaults" rather than assuming it got a dictionary back.
Where to go next
- Multiple slots. Take a slot number and save to
user://save_{n}.json. Everything else stays the same. - Autosave. Call save() on key events (level complete, quit) instead of every frame. A Timer works for periodic autosave.
- Load level data too. Pair this with the tilemap CSV loader so both progress and level layout come from data files.
Use it however you want
The prompt is CC0, paste it, modify it, ship a game with it. The output Claude gives you is yours. A link back to pixeldex.dev in your project's README helps the next solo dev find this stuff.