How to load and save things with Godot: a complete tutorial about serialization

The thing with Godot is that there’s no only one solution, you had many solutions.

But before seeing these solutions, you must know what you want to save first.

I’ll help you defining how and where you can use these saved methods, and you decide what to use, deal? Deal.

Before actually starting coding something…

Define what you want to save first

Don’t say:

“I’m going to use [Format Name Here] to save my things”

This will probably over-complicate the process for you and your team. Decide what you really want to save first, and then say:

“That data is going to be saved using [Format Name Here]

If you want

Why? Because is easier to define what tools do you need according your requirements than having the tools first and then meet the requirements.

For this example, let’s assume that we want to save the player health, player name and player points. An integer, a string and a real(float) value.

Decide what format do you want to use to save your data

This step may be tricky, specially for new users. You know what you want to save, but what kind of file are you going to use? Godot let you use whatever you want, so from all file formats that exist, what would be the “ideal” format?

A while ago I made a little flowchart to help you to decide what you need according what you want to save, here’s an updated version:
flowchart

It can always be modified and improved based on community suggestions.

But should I save nodes?

Most of the time you don’t want to save a node, you want to save the data that node contains.

Save nodes when you want to save the node structure itself, with the current applied values.

Where do I save my data files?

Save to user:// folder.

res:// folder will be read-only when you export the game, but is the perfect path if you’re working with @tool stuff (like plugins).

You can read more about file paths in Godot on its official documentation page.


Available built-in formats

Using ConfigFile

ConfigFile is a helper class that helps you writing INI (.ini) style format, which its structure looks like:

[section]
variable_name = <variable value>

This is widely used when you want to save configuration files, I’ve also seen the usage of this format on some mod loaders, where they define the assets and workflow of the mod.

With Godot, you only need to define a ConfigFile object to save or load the data, and each data/section you need to save or load:

# Let's assume the PlayerNode is the node where we save the player data in game
var PlayerNode:Node

var save_path := "user://player_data.ini"

# To save data
func save() -> void:
  var config_file := ConfigFile.new()

  config_file.set_value("Player", "health", PlayerNode.health)
  config_file.set_value("Player", "name", PlayerNode.name)
  config_file.set_value("Player", "points", PlayerNode.points)

  var error := config_file.save(save_path)
  if error:
    print("An error happened while saving data: ", error)

# To load data
func load() -> void:
  var config_file := ConfigFile.new()
  var error := config_file.load(save_path)

  if error:
    print("An error happened while loading data: ", error)
    return

  PlayerNode.health = config_file.get_value("Player", "health", 1)
  PlayerNode.name = config_file.get_value("Player", "name", "UNDEFINED")
  PlayerNode.points = config_file.get_value("Player", "points", 0.0)

The usage from Godot 3 and Godot 4 is practically the same with code. In Godot 3, you’ll need to use an external text editor to see the saved data, Godot 4 let you see your ConfigFile in editor, through Script editor, loading it as plain text.

The main advantage of using ConfigFile is that Godot knows what is saved to a ConfigFile, so data types are preserved, even if is an Object.

Using JSON

JSON stands for JavaScript Object Notation*, and is widely used for data transference between things that are not directly related but needs and easy and human readable way to transfer that data.

In a file, it may look like a GDScript dictionary:

{
  "variable_name": <variable value>
}

There’s no scenario apart from the data transference that I should recommend this format. Its usage really depends on you, and is ok if you don’t use it for data transference.

# Let's assume the PlayerNode is the node where we save the player data in game
var PlayerNode:Node

var save_path := "user://player_data.json"

# To save data
func save() -> void:
  var data := {
    "name": PlayerNode.name,
    "health": PlayerNode.health,
    "points": PlayerNode.points
  }

  var json_string := JSON.stringify(data)

  # We will need to open/create a new file for this data string
  var file_access := FileAccess.open(save_path, FileAccess.WRITE)
  if not file_access:
    print("An error happened while saving data: ", FileAccess.get_open_error())
    return

  file_access.store_line(json_data)
  file_access.close()

# To load data
func load() -> void:
  if not FileAccess.file_exists(save_path):
    return
  var file_access := FileAccess.open(SAVE_PATH, FileAccess.READ)
  var json_string := file_access.get_line()
  file_access.close()

  var json := JSON.new()
  var error := json.parse(json_string)
  if error:
    print("JSON Parse Error: ", json.get_error_message(), " in ", json_string, " at line ", json.get_error_line())
    return
  # We saved a dictionary, lets assume is a dictionary
  var data:Dictionary = json.data
  PlayerNode.name = data.get("name", "UNDEFINED")
  PlayerNode.health = data.get("health", 1)
  PlayerNode.points = data.get("points", 0.0)

JSON has some limitations* which can be summarized on:

  • It doesn’t recognize what a real (float) or integer (int) value is, it saves a number.
  • It can’t save all data types, only arrays, dictionaries, numbers and strings.

But, at the other hand, since is widely used across the whole internet, there are very good editors for this format, not only your notepad app.

Note: Since Godot 4, you can also edit it with Script editor, editor will load it as plain text (even if it’s defined as resource)

Using FileAccess (File)

FileAcess (or just File in Godot 3) is a helper class to write and read files. No fancy stuff, you’re dealing directly with the file itself.

Usually, you use FileAccess in complex scenarios, where you need total control about how the stuff in the file is read or written. Note that most of the FileAcess functions reads and write binary files, only string/text related functions writes and read from human readable text.

This may be useful when you want to write your own format.

# Let's assume the PlayerNode is the node where we save the player data in game
var PlayerNode:Node

var save_path := "user://player_data.dat" # <- custom format

func save(content):
  var file = FileAccess.open(save_path, FileAccess.WRITE)
  file.store_line(PlayerNode.name)
  file.store_32(PlayerNode.health)
  file.store_float(PlayerNode.points)
  file.close()
  

func load():
  var file = FileAccess.open(save_path, FileAccess.READ)
  # Order matters. The same order you use to write, is the order you use to read
  PlayerNode.name = file.get_line()
  PlayerNode.health = file.get_32()
  PlayerNode.points = file.get_float()
  file.close()

If you try to read the generated file with a text editor you’ll see this:

image of binary data displayed as plain text

Your text editor tries to convert the binary data into unicode characters, resulting in that weird looking characters. The real data may look like this:
image of binary data

Using Resources

Resource is a class that you can serialize directly to a file using Godot ResourceSaver and deserialize using ResourceLoader. They were made for data, they live for data.

Majority (if not all) files you see in editor FileSystem are a Resource.

Resources are something that Godot engine can easily understand, and the way that it loads or save stuff can be modified/extended, so it gives you the same power as if you were writing it with other methods.

Additional to that, since is an Object, this data can:

  • Be shared between instances.
  • Hold methods.
  • Emit signals.
  • Define setter and getter for their variables.
  • Be inspected in editor and manipulated with custom tools.

According to docs:

“… their main advantage is in Inspector-compatibility. While nearly as lightweight as Object/RefCounted, they can still display and export properties in the Inspector. This allows them to fulfill a purpose much like sub-Nodes on the usability front, but also improve performance if one plans to have many such Resources/Nodes in their scenes.” - Link to documentation.


Creating a Resource

@tool
extends Resource
class_name PlayerData

@export var name:String = "Default"
@export var health:int = 1
@export var points:float = 0.0

That’s our data class. class_name is optional, but would help us to:

  • Use it in other scripts.
  • Make it appear in the resource creation list.
  • Static type.

Using the Resource

Using our current configuration, saving data would look like this:

# Let's assume the PlayerNode is the node where we save the player data in game
var PlayerNode:Node

var save_path := "user://player_data.tres" # <- tres is Text RESource

func save() -> void:
  var data := PlayerData.new()
  data.name = PlayerNode.name
  data.health = PlayerNode.health
  data.points = PlayerNode.points

  var error := ResourceSaver.save(data, save_path)
  if error:
    print("An error happened while saving data: ", error)

func load() -> void:
  var data:PlayerData = load(save_path)
  PlayerNode.name = data.name
  PlayerNode.health = data.health
  PlayerNode.points = data.points

This works, but we miss the power of sharing the data between instances, what we can do is to update the data hold in the node:

# node script
@export data:PlayerData = PlayerData.new()

and now we can just do on our save/load script:

func save() -> void:
  ResourceSaver.save(PlayerNode.data, save_path)

func load() -> void:
  PlayerNode.data = load(save_path)

This way you keep your data and your node code separated, so the data doesn’t rely on the node and you can share/modify it and let other node handle it.


Notice that PlayerData is not your data, is just your data structure. The actual data is on data variable, that we exported on the PlayerNode

This is because PlayerData is a Script. By itself, it doesn’t do anything and, just like the script of your nodes, it needs to be created (similar to what you do when you attach the script to the node).

But I heard from someone that “resources are insecure”

Image about game encription

Generally, you don’t worry about security.

“… there’s no good reason to prevent malicious users from running arbitrary code, even if you did not provide an official way to do it. But there’s also no good way.” - xananax)

Yes, is true that Resources can execute arbitrary code, but this is true for all resources, not only yours. This is even true for the files you see in FileSystem, and all things loaded from FileAcess that allows to bind complete objects (which includes the script). Is a more complex topic than just “I don’t want to be hacked”, a topic that we don’t need to deal with.

Your resource files are not something that you’ll share with the user, and if you’ll do, you can write custom format parsers to read/write from file, so there’s no problem at the end. This is a long and delicate issue, experts on the matter are discussing about it, but you can still use the resources in the mean time.

I still want to use resources, but I’m worried about security

This is a personal note, but if my previous statement didn’t convinced you, you can still use another advantage of resources: the ability to be able to load and save from custom formats.

Using FormatLoader and FormatSaver

From here I will touch an “advanced” scenario, so I will assume that you will look into documentation when something is not explained and that you knows the basics already.

Godot let you use your custom custom format as if it were a resource, letting you define the steps that it requires to load or save that resource.

Long ago I’ve made an example repository to try this feature: GitHub - AnidemDex/Godot-CustomResource: This is a repository where I make experiments with custom resources in Godot. Hope you may find it informative and useful

Why use a ResourceFormat?

Because you want to use resources, but also want to define the steps it takes to save it or load it.

Is this safe?

As safe as loading/saving plain text files.


Before creating a ResourceFormat we need to define the Resource. Since we have already made one for our examples, we’ll use PlayerData as our Resource.

To achieve this tutorial goal:

  • We will save a plain text file with a custom format.
  • The file extension will be .custom.
  • To keep tutorial “simple”, the format style will be Comma-Separated Values (csv), where:
<player name>, <player health>, <player points>

Creating a ResourceFormatLoader

ResourceFormatLoader is used by the engine with ResourceLoader to determine how to load something.

@tool
extends ResourceFormatLoader # <- Class we need to define the loader
class_name PlayerDataFormatLoader # <- Required in order to register it in engine.

## Define which extensions (the file ending, after the "." character) you can handle
## with this loader.
func _get_recognized_extensions() -> PackedStringArray:
	return PackedStringArray(["custom"])

## Return the type of resource this loader will return.
## Since is a custom resource, we must return "Resource"
func _get_resource_type(path: String) -> String:
    return "Resource"

## Return the class name registered for our custom resource if any.
func _get_resource_script_class(path:String) -> String:
  return "PlayerData"

## Return if we handle that type of object.
## Since we handle a custom resource, we check against ClassDB
func _handles_type(type: String) -> bool:
	return ClassDB.is_parent_class(type, "Resource")

## Define how to load our resource, or return an ERROR if something went wrong.
func _load(path:String, original_path:String, use_sub_threads:bool, cache_mode:int):
  var file = FileAccess.open(path, FileAccess.READ)
  if file == null:
    return FileAccess.get_open_error()
  
  var line = file.get_line()
  var properties = line.split(",")
  if properties.size() != 3:
	return ERR_PARSE_ERROR
  
  var data := PlayerData.new()
  data.name = properties[0]
  data.health = int(properties[1])
  data.points = float(properties[2])
  return data

Now you should be able to do:

func load() -> void:
  PlayerNode.data = load("custom_player_data.custom")

The previous script defines a loader for a specific file, in a specific format for our custom resource. All defined methods must be implemented in order to be able to work.

The most important part here is the _load method. This is the part where you define how to read the file, and what is its resource equivalent.

Creating a ResourceFormatSaver

Just as ResourceFormatLoader, engine uses many ResourceFormatSaver to determine how to save the resource through ResourceSaver.

@tool
extends ResourceFormatSaver # <- Class we need to define a saver
class_name PlayerDataFormatSaver # <- Required in order to register it in engine

## Return which extension does that `resource` takes.
## Since we just handle one type and one format, we return "custom"
## for all types.
func _get_recognized_extensions(resource:Resource) -> PackedStringArray:
  return PackedStringArray(["custom"])


## Here we see if that resource is the type we need.
##
## Multiple resources can inherit from the same class,
## and they can even modify the structure of the class or be pretty similar to it,
## so you must verify if that resource is the actual kind of resource
## you can save, and if it's not you let other ResourceFormatSaver deal with it.
##
## Since we just assumes that our type will be the same always, we just handle
## it as is, but ideally you should filter it.
func _recognize(resource:Resource) -> bool:
  return resource is PlayerData

## Save the actual resource to disk and return OK, or return an Error if something fails.
func _save(resource:Resource, path:String, flags:int):
  if not(resource is PlayerData):
    return ERR_INVALID_DATA

  var data_string:String = "{player_name},{player_health},{player_points}"
  var data = {
    "player_name":resource.name,
    "player_health":resource.health,
    "player_points":resource.points,
  }
  data_string = data_string.format(data)
  
  var file = FileAccess.open(path, FileAccess.WRITE)
  if file == null:
    return FileAccess.get_open_error()
  
  file.store_string(data_string)
  file.close()
  return OK

Now you should be able to do:

func save() -> void:
  ResourceSaver.save(PlayerNode.data, "custom_player_data.custom")

What we do is to verify the resource type and the file extension, and then define how the resource of our type is saved in the given path.

The most important part is _save, where we define the file equivalent of the given Resource.

Going further with a ResourceImporter

A resource importer is an extra step in the resource load, were we first create a new file from a given file, save it to disk and tell godot to use that created file as the resource instead of the original file. See for example png/jpg (image) files, engine import them as ctex files and then returns a Texture from that generated ctex file. You can read more about this in:

Summary

Godot can load and save from and to many formats.

It includes some tools to load and save from JSON and INI style files. You can also load and save custom file formats using FileAcess (File in Godot 3).

Godot has its own data type for handling data, Resource, which can be used as an object and modified the way the engine saves and load it from disk.


And… that’s it, hope you find it useful. If you had any feedback, something that you think can be improved or just want to say anything don’t be afraid of doing so.

I have been seeing many questions about how to “properly” save things with Godot in the discord server, so I wanted to write a complete tutorial about how to save stuff, from easy to advanced solutions, for beginners and advanced users, hoping this help for future humans.

Edit: Whoops, forgot to mention, the original post was made in godotforums, but I reviewed and expanded it a little here

4 Likes