doc 1 of 4

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#

PartYou buildThe idea
01An audit of your own project, and a save schemaDeciding what to save is the hard part
02Working save and load with FileAccess and JSONThe file API, and where saves go
03Versioning, migration, corrupt-file handlingYour 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.