Coding Quests
The Scroll Library
Tutorials

How to Build a Save System in Godot 4

February 12, 2026Updated June 11, 20264 min read

I put off building a save system for months on my first project. It felt like plumbing, boring code you write after the fun stuff. That was a mistake. The longer you wait, the more systems you have to retrofit, and the file I/O is honestly the easy part in Godot 4. It's the early decisions that bite you later.

Here's what I wish someone had told me up front.

The Two Approaches to Saving Data in Godot 4

There are two fundamental ways to save data in Godot:

  1. ConfigFile for key-value storage. Settings, options, simple flags.
  2. FileAccess plus JSON or binary for full control over complex game state.

Most games need both, so don't agonize over picking one.

ConfigFile for Settings

ConfigFile is Godot's built-in INI-style storage. Sections, keys, typed values, and you write zero parsing code.

GDScript
# Save settings
func save_settings() -> void:
var config := ConfigFile.new()
config.set_value("audio", "master_volume", 0.8)
config.set_value("audio", "music_volume", 0.6)
config.set_value("video", "fullscreen", true)
config.set_value("video", "vsync", true)
config.save("user://settings.cfg")
# Load settings
func load_settings() -> void:
var config := ConfigFile.new()
if config.load("user://settings.cfg") != OK:
return # No settings file yet, use defaults
master_volume = config.get_value("audio", "master_volume", 0.8)
music_volume = config.get_value("audio", "music_volume", 0.6)
fullscreen = config.get_value("video", "fullscreen", false)

The third argument in get_value is the default, returned when the key doesn't exist. Don't skip it. It's the thing that lets you add a new setting in update 1.2 without breaking the config file a player saved back in 1.0.

Important: Save to user://, never res://. The user:// directory is writable at runtime and maps to the player's app data folder. res:// is your project directory, and it's read-only in exported builds. Saving there works in the editor, then silently fails for every player.

Saving Game State with JSON

Settings are the warm-up. The real work is game state: position, health, inventory, quest progress. For that you want structured serialization, and JSON is the simplest place to start.

GDScript
func save_game() -> void:
var data := {
"player": {
"position": {"x": player.position.x, "y": player.position.y, "z": player.position.z},
"health": player.health,
"level": player.level,
},
"inventory": player.inventory.serialize(),
"quests": quest_manager.serialize(),
"timestamp": Time.get_datetime_string_from_system(),
}
var file := FileAccess.open("user://savegame.json", FileAccess.WRITE)
file.store_string(JSON.stringify(data, "\t"))
GDScript
func load_game() -> void:
if not FileAccess.file_exists("user://savegame.json"):
return
var file := FileAccess.open("user://savegame.json", FileAccess.READ)
var json := JSON.new()
if json.parse(file.get_as_text()) != OK:
push_error("Failed to parse save file")
return
var data: Dictionary = json.data
player.position = Vector3(
data.player.position.x,
data.player.position.y,
data.player.position.z
)
player.health = data.player.health
player.level = data.player.level
player.inventory.deserialize(data.inventory)
quest_manager.deserialize(data.quests)

The Serialize and Deserialize Pattern

Here's what actually keeps a save system maintainable: each object serializes itself. The moment you write one giant save_game() that reaches into the player, the inventory, and four different managers, you've built a function that breaks every time anything changes.

GDScript
# InventoryComponent.gd
func serialize() -> Array:
var items := []
for item in inventory_items:
items.append({
"id": item.id,
"quantity": item.quantity,
"durability": item.durability,
})
return items
func deserialize(data: Array) -> void:
inventory_items.clear()
for item_data in data:
var item := Item.new()
item.id = item_data.id
item.quantity = item_data.quantity
item.durability = item_data.get("durability", 100) # Default for old saves
inventory_items.append(item)

Look at data.get("durability", 100). That one line handles saves created before durability existed. Every field you add later needs a default like this, or players who update will crash on load. I learned that one the hard way.

Save File Versioning

Your save format will change. Not might. Will. Stamp a version number into every file from day one:

GDScript
const SAVE_VERSION := 2
func save_game() -> void:
var data := {
"version": SAVE_VERSION,
"player": player.serialize(),
# ...
}
func load_game() -> void:
# ... parse file ...
var version: int = data.get("version", 1)
if version < 2:
# Migrate old save format
data = migrate_v1_to_v2(data)

Now you can ship updates without nuking anyone's progress. Version 1 saves get migrated up, version 2 loads directly. Twenty minutes of work that spares you the worst kind of negative review.

Multiple Save Slots

Nothing clever here, just a naming convention and a loop:

GDScript
func get_save_path(slot: int) -> String:
return "user://save_slot_%d.json" % slot
func get_all_saves() -> Array[Dictionary]:
var saves: Array[Dictionary] = []
for i in range(1, 4): # 3 slots
var path := get_save_path(i)
if FileAccess.file_exists(path):
var file := FileAccess.open(path, FileAccess.READ)
var json := JSON.new()
if json.parse(file.get_as_text()) == OK:
saves.append({"slot": i, "data": json.data})
return saves

Common Save System Mistakes

The first one everybody hits: shoving a Vector3 straight into JSON. JSON has no idea what a Godot type is. Break it into plain numbers:

GDScript
# Wrong - won't serialize
data["position"] = player.position
# Right - breaks into primitives
data["position"] = {
"x": player.position.x,
"y": player.position.y,
"z": player.position.z,
}

A few more from real projects. Check FileAccess.file_exists() before loading, because first-time players have no save file. Save on events (new area, quest complete, manual save), not every frame. An autosave timer every 5 minutes is fine.

And assume save files will get corrupted eventually. A crash mid-write, a full disk, a cloud sync fight. Wrap your parse logic in error checks and keep a backup of the previous save. Players forgive a lost five minutes. They don't forgive a lost 40 hours.

Going Deeper

This covers the foundation, and honestly it's enough for most small games. A production system adds layers on top: encrypted saves, cloud sync, save file thumbnails, and full world-state serialization where every opened chest stays opened.

Most of what you'll serialize is inventory data. If you haven't built that part yet, my Godot 4 inventory system tutorial uses the same serialize pattern, so the two snap together.

And if you want to build the whole thing properly, step by step, the Save and Load quest goes from these basics to a polished system with multiple slots, migration, and error recovery across 14 lessons.

Whatever you do, don't bolt saving on the week before release. Past me would like a word.

FAQ

Where does Godot save user files?

In your OS app data folder, under user://. On Windows that's %APPDATA%\Godot\app_userdata\YourProjectName, with equivalents under ~/.local/share on Linux and ~/Library/Application Support on macOS. Quickest way to find it: open the Project menu in the editor and hit "Open User Data Folder."

Should I use JSON or Resources for save files?

JSON, for most games. Saving custom Resources with ResourceSaver is convenient, but a Resource file can carry embedded scripts, so loading a save that a player (or a cheater) edited is a real security hole. JSON is plain text, easy to inspect, and can't execute anything.

How often should my game save?

On meaningful events: area transitions, quest completions, manual saves. An autosave timer is fine too. Writing to disk every frame is wasted work and a good way to cause stutter, so don't.

godottutorialsave-system

Stop reading. Start building.

Beat the demo boss by writing real Godot code, then build this for real in the Save + Load System Quest.

Free, and no card needed. Built by a real person, with new quests every month.

Get the next Godot build in your inbox

New quests, project breakdowns, and game-dev tips. Free, no spam, unsubscribe anytime.

Written by Coding Quests

We teach Godot 4 by making you build complete systems: inventories, save systems, action roguelike controllers, enemy AI. The scrolls are free. The quests are where it sticks.