Save & Load — the system that punishes bad architecture
Saving is the honest audit of your codebase. Every shortcut you took — state hidden inside a UI node, a value you can only recover by reading a sprite's position, a manager that rebuilds itself from the scene tree — surfaces here, all at once, as a bug you can't fix without rewriting something.
Which is why the useful framing isn't "how do I write a file." Writing the file is twenty lines and you'll learn it in part 02. The framing is: what, exactly, is the truth of my game, and can I name all of it?
The test#
Here's a question you can answer today, about a project you already have:
If I deleted every visual node — every sprite, every label, every particle — how much information would be lost?
Nothing? Saving is nearly free. Serialise the data objects, done.
Quite a lot? That's not a saving problem. It's a structure problem that saving is reporting to you. Fix it there, and saving becomes free as a side effect.
This shelf teaches both halves: how to identify and shape the truth, then how to write it down safely.
What this shelf teaches#
| Part | You build | The idea |
|---|---|---|
| 01 | An audit of your own project, and a save schema | Deciding what to save is the hard part |
| 02 | Working save and load with FileAccess and JSON | The file API, and where saves go |
| 03 | Versioning, migration, corrupt-file handling | Your day-one save will meet your day-200 code |
Three rules that make it fall out#
Never save anything you can recompute. Save hp: 40; don't save the health bar's width.
Save the inventory; don't save the label text. Derived values in a save file are values that
can disagree with their source, and one of them will be wrong.
Save what a thing is and what changed about it — not the thing itself. For a spawned enemy: its scene path, its position, its current hp. Loading means spawn-then-apply, not reconstructing an object graph.
Version it from the first line you write. A save file with no version field is a save file you can never change. Part 03 is about this, and it costs one line to be ready for it today.
Resources or JSON?#
Godot lets you save custom Resource objects straight to disk with ResourceSaver, which is
seductive: one call, no schema, types preserved. For save games, prefer JSON anyway.
A .tres records the script it uses, and loading it brings that script along. For your own
project files that's a feature. For a file sitting in the player's writable folder — editable
by them, by malware, or by whoever they traded saves with — it's code execution wearing a save
icon. JSON can only ever be data.
Be aware this is a stronger line than the engine's documentation draws. Godot warns
explicitly about object deserialization via FileAccess.store_var/get_var, but its
Saving games page
happily calls load() on a path taken from a save file with no caveat at all. Treat the rule
here as informed practice rather than official policy — it costs you nothing and closes a real
hole.
The cost is real and worth naming: JSON has no Vector2, no Color, no custom classes. You
convert on the way out and back. Part 02 shows the conversions, and they're less annoying than
they sound — they're also exactly the discipline that stops presentational junk leaking into
your saves.
user://, not res://#
res:// is your project folder. In the editor it's writable, so saving there appears to work.
In an exported game it is read-only (often packed inside the executable), and your save
silently fails for every actual player.
Saves go to
user:// — a real
per-user, per-project writable directory (%APPDATA%\Godot\app_userdata\<project> on Windows).
You can open it from the editor: Project → Open User Data Folder.
This is one of the most common shipping bugs in hobby Godot projects, it never reproduces in the editor, and it's why the rhythm game project carries it as a known defect for its audit stage.
Prerequisites#
You need custom Resources and a data layer worth saving —
the inventory shelf is the natural warm-up, and part 02 uses
it as the worked example. Familiarity with dictionaries and Vector2 is assumed.