2025/10/01

- Type
- Learning Resource
- Format
- Study Guide
- Version
- Godot 4.x
- Subject Tags
- Resource-based saving workflow
- Godot 4 improvements explained
- Created
- Updated
- 2025/09/30
- 2025/10/01
Saving and loading game data is essential for most games so players can pick up where they left off. In Godot, you have several ways to save data, but using resources offers an efficient solution that works naturally with how you already build games in Godot.
Resources are Godot's most hands-off way of storing reusable data. You might already use them for character stats, items, or progression. The same engine feature can also handle your save files, which means if you're already using resources for game data, you can save and load them with just a few lines of code.
In this study guide, you'll learn:
Why consider resources for save games?
Compared to JSON or other text formats, resources give you static typing, require less code to save and load back, and work seamlessly with all Godot data types like , Vector2, or Color. NodePath
You can also edit resource files directly in the inspector during development, which makes testing and debugging easier.
Plus, resources can contain other resources. You can build complex save structures naturally: if your game uses resources for inventories or character stats, you can simply reference them from your savegame resource and save and load them directly without writing data conversion code. All in all, they're a really convenient way to serialize and deserialize game data.
Alternatively, you may also consider using
FileAccess.store_var()
andFileAccess.get_var()
for safe (but a little more constraining) binary serialization.The open source demo that comes with this guide offers an example of both approaches (resources and ). You'll find it in the FileAccessDownload section below.
Yes, resources support code execution (Godot scenes and scripts are resources). The same applies to other Godot native text serialization methods like ConfigFile
and str2var
. If you load a resource from an untrusted source, it could run malicious code.
If you need to load resources from untrusted sources, there are two add-ons you can choose from:
ResourceLoader.load()
that quickly checks the content of a text resource (.tres
file) before loading it to make sure it doesn't contain code. It's easy to set up.Alternatively, if you want to ensure that no code can run, consider using FileAccess.store_var()
and FileAccess.get_var()
to save and load data in a binary format.
This method is secure by default because it prevents saving and loading objects, which are what enable code execution in Godot. The tradeoffs are that you'll need to write more code to serialize and deserialize your data, and that it only works with binary data.
With , you typically convert all your game data to and from dictionaries or arrays manually to collect them for saving and loading. FileAccess
Resources and FileAccess.store_var()
are the main two Godot-native methods we use and recommend for saving and loading game data.
When we originally created this demo for Godot 3, resources had two limitations that required workarounds: First, we couldn't easily save and load arrays of resources, like the contents of an inventory. We had to use dictionaries instead. Second, the engine's resource cache would prevent reloading already loaded resources like savegames, forcing us to write extra code to bypass the cache.
Both of these issues are now fixed in Godot 4. You can save and load arrays of resources without any special handling, and the cache works correctly with the CACHE_MODE_IGNORE
flag. You need even less save-game-specific code. Your save feature is simpler than before (which is the main reason resources are appealing for saving and loading in the first place).
The main workflow stays the same: create a custom resource class, add exported variables for your data, and use ResourceSaver.save()
and ResourceLoader.load()
. These operations now work more reliably with fewer edge cases to handle.
Before diving into implementation, it's worth understanding how Godot's different save methods work under the hood. Godot offers multiple engine-native ways to serialize data: resources (.tres
for text and .res
files for binary), ConfigFile
(.cfg
files with an ini-style format), and var_to_str()
for converting variables to strings.
Resources, ConfigFile
, and var_to_str()
all use the same serialization under the hood. They convert your data to a text-based format that Godot can read back later, and they all support Godot-specific data types like , Vector2, and Color. They can also save and load objects and resources directly. NodePath
Objects in Godot can contain and execute code, so these save and load methods support code execution (hence the security considerations mentioned earlier).
The methods ( FileAccessstore_var()
and get_var()
) work differently: they use the engine's var_to_bytes()
and bytes_to_var()
functions that serialize to a binary format and disable object serialization by default.
This means no code execution risk by default. However, you can optionally enable object serialization with these methods, in which case they have the same security implications as resources.
If you're already using custom resources in your game for items, characters, quests, and so on, collecting save data is quick: you just reference those resources from your save game resource and Godot handles the rest.
JSON is widely used and familiar to many developers, so people like to use it when coming from different technologies. But it's limited for saving and loading Godot game data: it doesn't natively support Godot types like or Vector2, so you need to manually convert them to dictionaries or arrays. JSON, because it's a format designed for JavaScript originally (JSON stands for JavaScript Object Notation), also only supports floats for numbers, not integers, so you have to be mindful of that when saving and loading numbers. Color
For most game save data, resources or FileAccess.store_var()
are more efficient and require less conversion code. JSON is better suited for web requests or when you need to exchange data with external applications.
Let's start with the basics. To create a resource-based savegame, you start by making a custom class that extends Resource
. Then, you add exported variables for the data you want to save. Here's a simple example:
class_name SaveGame
extends Resource
@export var coins := 0
@export var player_global_position := Vector2(0, 0)
This save resource contains two pieces of data: the player's coin count and global position in the world. The @export
keyword tells the engine to serialize those values: Godot will save and load them automatically when you use the resource. You don't have to do anything special.
Now let's look at how to actually save and load your game data. You can save any resource by calling ResourceSaver.save()
. Usually, you'll do that from a dedicated script, like your main game script or an autoload that manages savegames:
extends Node
const SAVE_PATH := "user://simple_save.tres"
var save_game: SaveGame = null
func _ready() -> void:
if ResourceLoader.exists(SAVE_PATH):
save_game = ResourceLoader.load(SAVE_PATH, "", ResourceLoader.CACHE_MODE_IGNORE)
else:
save_game = SaveGame.new()
In this code, at the start of the game, we either load an existing savegame or create a new one if none exists.
If the savegame file exists, ResourceLoader.load()
loads the saved resource from disk and gives you back a SaveGame
instance with all your data restored. The CACHE_MODE_IGNORE
flag is important here. It tells Godot to reload the resource from disk instead of using a cached version already loaded in memory. Without this flag, Godot might give you stale data when your data becomes more complex.
Here's how you can save the game:
func save() -> void:
ResourceSaver.save(save_game, SAVE_PATH)
In a single line, you can save all your @export
variables to a .tres
file. The file is human-readable text that you can open in any text editor or directly in Godot's inspector. This makes testing and debugging much easier when you're working on your game. The save happens instantly and synchronously, which means when the function returns, your data is already on disk.
You can expand this code to handle any amount of data. Just add more variables to your SaveGame
resource, and copy them back and forth between your game and the save resource when you need to.
The examples here just show the basics and skip error checks to keep things clear and simple. In a real game, you should check the return value of ResourceSaver.save()
. It returns an error code that you can use to know if an error occurred when trying to write the file:
func write_savegame() -> void:
var error_code := ResourceSaver.save(self, SAVE_PATH)
if error_code != OK:
push_error("Failed to save game: " + error_string(error_code))
The function error_string()
produces a human-readable name for the error code returned by ResourceSaver.save()
.
So far the save code looks simple but also underwhelming: it's easy to save a few variables, but most games have more complex data to track. You'll have characters with stats, inventories filled with items, quests in progress, and so on.
The power of resource-based saves becomes clear when you work with complex game data. Resources can contain other resources or arrays of resources as @export
variables, which lets you organize save data however you'd like.
For example, in an RPG you might have a Character
resource that groups all stats for one character:
class_name Character
extends Resource
@export var display_name := "Godot"
@export var run_speed := 600.0
@export var level := 1
@export var experience := 0
@export var strength := 5
@export var endurance := 5
@export var intelligence := 5
This resource isn't save-specific: you'd use it throughout your game to display the character name and stats in menus, calculate damage in combat, know what speed to use in _physics_process()
, and so on. But because it's a resource, you can also save it directly.
class_name SaveGame
extends Resource
@export var party_members: Array[Character] = []
@export var current_level_path := "res://levels/town.tscn"
@export var player_global_position := Vector2(0, 0)
When you save this SaveGame
resource, Godot automatically serializes the entire array of party members with all the properties of the Character
resource. When you load it back, you get fully populated Character
instances with all their stats. You don't need to write any conversion code or manually reconstruct the nested data.
This approach scales naturally to a fair amount of complexity. You could have an Inventory
resource containing an array of Item
resources or a QuestLog
resource tracking quest progress. We're simply using resources as composable structures we can reference from different parts of our game.
Many developers want to prevent players from editing their save files. The first thing to note is there's always a way for tech-savvy people to mess with game files. People manage to extract and modify data from AAA games with proprietary security features. As an independent developer or small team, you won't be able to prevent that entirely.
What you can do is make it harder for the average player to edit save files without special tools or knowledge.
The simplest approach with resources is to save as binary in release builds instead of text. Change the file extension from .tres
to .res
. Binary files aren't human-readable in a text editor, which stops really easy editing.
You can use OS.is_debug_build()
to use the text format during development and binary for releases. In this example I code a little helper function to get a different savegame path in debug and release builds:
static func get_save_path() -> String:
var extension := ".tres" if OS.is_debug_build() else ".res"
return "user://save" + extension
This way you keep the convenience of readable save files when working on your game while making them harder to edit in the final game (also smaller and faster to read and write as a bonus).
Another option is to use FileAccess.store_var()
with encryption enabled. When you open a file with FileAccess.open_encrypted()
, you pass an encryption key. Godot encrypts the data when saving and decrypts it when loading.
Keep in mind that encryption makes saving and loading slower because of the extra encryption step. This won't matter in small games, but with larger save files you might notice the difference.
Also, the encryption key has to be in your scripts somewhere, so anyone who looks at your game code can find it. You could compile Godot with game file encryption to make finding the key harder, but again, this just raises the bar rather than making it impossible.
To learn more about game file encryption, check out the official docs: Compiling with PCK encryption
For most games, if you want to limit access to editing save files, I recommend just saving as binary as it is enough to prevent casual editing without adding complexity or a performance overhead.
Yes, it's fast! Saving and loading bits of data (strings, numbers, vectors...) is nothing for a modern computer. Godot's scene files (tscn files) are resources. If you've ever made a huge 2D level with a tilemap, you've experienced the engine saving and loading a large resource file quickly (when you double click the scene in the file system dock, for example).
Yes, it does. If you change the structure of your savegame resource or game resources (especially remove or rename variables), old save files might not load correctly anymore. Note that this is true of any resources you would use in your game, savegame or not.
Generally speaking, whenever your data structures change in a game, you have to handle migrating old data to the new format. This is true whether you're using resources, JSON, or any other format. More generally, it's a common challenge in software development. When your serialized data changes, you need to implement data migrations: write code to convert old data to the new format.
When working with resources, if you make major changes to your savegame structure, my recommendation is to:
I actually tend to do the same for any serialized data, not just savegames, based on a little experience working with databases, where schema migrations are a common practice.
Import the zip file in Godot 4.5 or later to explore the code and see how it works. There are two demos:
FileAccess.store_var()
and FileAccess.get_var()
. The game level still uses resources for game data, but the savegame itself is a dictionary saved in binary.Don't stop here. Step-by-step tutorials are fun but they only take you so far.
Try one of our proven study programs to become an independent Gamedev truly capable of realizing the games you’ve always wanted to make.
Get help from peers and pros on GDQuest's Discord server!
20,000 membersJoin ServerThere are multiple ways you can join our effort to create free and open source gamedev resources that are accessible to everyone!
Sponsor this library by learning gamedev with us onGDSchool
Learn MoreImprove and build on assets or suggest edits onGithub
Contributeshare this page and talk about GDQUest onRedditYoutubeTwitter…