Godot 4 Save and Load Games: How to Build a Robust System

Saving and loading games requires you to write your game’s current state to a file. Godot 4 offers several ways to save and load data from files, but in this article, we are going to focus on a single approach. Warning: this article is for advanced game developers, who want to take their game development and coding skills to a whole new level.

The process of getting the data ready to be sent to a different location is called serialization. On the other hand, deserialization of data is the opposite process, where the data is received, interpreted, and prepared for usage. These terms are important for a better understanding of the implementation of save and load game code. Let’s take a closer look at those terms.

What Is Data Serialization and Deserialization?

In computer science, data serialization and deserialization refer to the process of packaging data into a transferable form. For example, transferring data over the internet requires the data to be serialized into a specific form. Once the data reaches its destination, it is deserialized and can be used. The serialized form of the data heavily depends on the requirements and implementation of the source and destination applications.

The same thing is required when writing data from your game to a file on your file system. During game runtime, the data is stored in memory to allow the game to access it quickly.

When the data is saved to a file, it is copied, serialized into the required form, and written onto the disk. Loading the data from the file will result in the opposite process.

Example: Serializing Data to a JSON File

To save a file in JSON format, all the data from your game should be organized to match the JSON format requirements. When loading the data from the file back into the game, it should be deserialized from JSON format and stored in the objects that currently exist in your game.

How Saving and Loading a Game Works in Godot 4

I have seen several tutorials that teach you how to save and load games using Godot’s resource system or using JSON files. Both of these methods are good for debugging or other purposes, but they won’t work in actual game production for several reasons. In contrast, there is another way to save and load data in a safe manner.

Godot allows you to store and retrieve variables from a file using the FileAccess library. With this library, you can create and edit encrypted binary files that will store your data.

Encrypted Binary Files? What? Why?

Let’s first think about the requirements for a “saved game” file:

  • Readability: The file must not be readable; if a user can see the saved game data, they might get some information which they are not supposed to see.
  • Editability: The file can’t be editable; otherwise, anyone can give themselves an advantage in the game or even break the game’s logic.
  • Easy Access: The data in the file must be organized to be easily accessed by your game.
  • Performance: Storing, reading, and parsing the data must be done quickly. This is critical in large games that must load a large amount of data from files.

Binary files contain packed data, which consists of integer bytes. So why encrypted binary files, you ask? Because they provide you with all these benefits, while the other two methods I mentioned will give you almost none of them. And the funny thing is that it’s one of the easiest ways to save data to files.

The Idea Behind Saving and Loading Games From a Binary File

Since binary files don’t have a specific format (unlike JSON, for example), it is up to you to decide what the format should be, depending on your game’s requirements. The implementation in your game must define the order, types, and amounts of variables the file should contain. Essentially, defining your own file format. Otherwise, loading the data back into the game will be incorrect. And the encryption part gives you even more benefits in terms of file security.

In my implementation below, I decided that each object that needs to store and load data, will be responsible to do it on its own internal properties. So the format of the file will be automatically determined by the objects in the game and the order their properties are exported and imported. Let’s see how it’s done in GDScript.



Building a System to Save and Load Games in GDScript

The steps below will guide you through the entire implementation of the save and load game module and objects.

The Code Architecture of the Save and Load System

Many beginner developers tend to build code that is not generic enough or that is only used for a specific object. However, I will not be showing you such an implementation. The following implementation is an industry-grade Save/Load system architecture. So, don’t panic if it’s a bit complicated. Learning how to write proper code and create systems is the best long-term strategy for your games.

I have another article that explains the mindset of designing systems in games. If you want to dramatically improve your game code architecture, you can find it here: How To Use System-driven Design To Create Better Games. So how is our save/load system going to work? Here are the basic components we need to create:

  • The Serialize(file) method: writes the required properties of an object to a file. It’s found in the object class, where the properties are accessible. For each object that has properties which need to be written to a file, this method must be implemented
  • The Deserialize(file) method: reads data from a file and assigns the properties of an object. It’s found in the object class, where the properties are accessible. For each object that has properties which need to be read from a file, this method must be implemented.
  • The LoadSaveGame System Singleton: class that acts as a centralized system for receiving save and load data requests from anywhere in the code. It is automatically loaded when the game starts (AutoLoad) and released when the game ends. It holds the target file object, path, and password for the first and all subsequent file accesses.

Note: You don’t need to implement the Serialize(file) and Deserialize(file) methods for all objects. Just for the ones that must store data and load data from a file.

Example: Saving and Loading Data From A Character Class

For the following example, I used a character class which has three properties: name, health, and speed. I want to save these properties to a file and load them back at a later time.

Step 1: Creating the ‘Serialize’ Method in the Character Script (Character.gd)

As I mentioned before, each property you want to save or load must be in the Serialize(file) and Deserialize(file) methods.

# Serialize the object's properties and write them to the given file
func Serialize(file : FileAccess) -> void:
	file.store_pascal_string(m_Name)	# Store a string
	file.store_32(m_Health)				# Store a 32-bit integer
	file.store_float(m_Speed)			# Store a 32-bit float

Step 2: Creating the ‘Deserialize’ Method in the Character Script (Character.gd)

# Read the object's properties from the given file and deserialize them
func Deserialize(file : FileAccess) -> void:
	m_Name = file.get_pascal_string()	# Get a string
	m_Health = file.get_32()			# Get a 32-bit integer
	m_Speed = file.get_float()			# Get a 32-bit float

Strings, integers and floats are not the only variable types Godot can import or export. There are many other types you can use in your games. Go to https://docs.godotengine.org/en/stable/tutorials/io/binary_serialization_api.html to see all available types.

Note: Pascal Strings vs. Regular Strings

A string is a collection of characters in memory. To store or retrieve a string from a file, the software needs some additional information, such as the encoding and number of characters in the string. A regular string will only contain the text itself, without the additional information. In contrast, the pascal string will include the text as well as the additional information.

To retrieve small strings like in my example, you should always use the pascal string methods. The regular string methods are used to write and read entire text files, which do not include other variables like integers or floats.

Okay, I understand the implementation of the Character class, but who calls these ‘Serialize’ and ‘Deserialize’ methods?

Step 3: Implementing the SaveLoadGame Singleton Script (SaveLoadGame.gd)

There must be a centralized system that manages the file access, password and object method calls. I called this class ‘SaveLoadGame’ and added it to the AutoLoad tab in the project settings, which essentially makes it a singleton. Then, I created the Initialize(path, password) and Clear() methods to initialize and clear the module’s properties.

extends CharacterBody2D
class_name SaveLoadGame

var m_File : FileAccess		# File object opened by the FileAccess library
var m_FilePath : String		# File path to the requested file
var m_Password : String		# Password for file encryption and decryption

# Fill the file data in the local variables
func Initialize(path : String, password : String) -> void:
	m_FilePath = path
	m_Password = password

# Clear the file data in the local variables
func Clear() -> void:
	m_File = null
	m_FilePath = ""
	m_Password = ""

Next, I implemented the OpenFile(access) and CloseFile() methods to open and close the requested file. The access parameter defines whether the next file accesses will be read or write operations. Note that I opened the file with password encryption.

# Open a file encrypted with the given password
func OpenFile(access : FileAccess.ModeFlags) -> int:
	
	# Try opening an encrypted file with write access
	m_File = FileAccess.open_encrypted_with_pass(m_FilePath, access, m_Password)
	
	# Return the assigned file index (handle)
	return FileAccess.get_open_error() if (m_File == null) else OK
	
# Open a file encrypted with the given password
func CloseFile() -> void:
	m_File = null

And finally, I added the main methods, which are the Serialize(object) and Deserialize(object) methods. They are the main methods that call the object’s Serialize(file) and Deserialize(file) methods.

# Serialize the object's properties and write them to the file with the given ID
func Serialize(object) -> void:
	object.Serialize(m_File)
	
# Read the object's properties from the given file and deserialize them
func Deserialize(object) -> void:
	object.Deserialize(m_File)

Note: In case your game contains complex and nested data structures that need to be stored in files, each object must call the Serialize(file) and Deserialize(file) methods of its internal data structures. This way, the saving and loading of data will be done in an ordered manner and will prevent parsing errors.



How to Use the SaveLoadGame Module in Your Game

In this section, I will show you how you can use the module we built to save your object’s properties into encrypted binary files. The following example assumes you are familiar with Signals in Godot. If not, you can check out my article about Signals: How to Use Signals for Node Communication (With Examples).

Example: Saving and Loading a Game Using Buttons

Suppose you have a ‘Save’ button and a ‘Load’ button. Both of these buttons are connected to 2 methods via signals.

In this example, I added three LineEdit fields so I can manually change the values of the character I mentioned above. The implementation below demonstrates how to use the SaveLoadGame module we created to save and load data from a file once the buttons are clicked.

Implementing the Basic Functionallity of the Character Class (Character.gd)

In the code below, I defined the character’s properties, the target file path and the encryption/decryption password. I also got the LineEdit nodes and implemented the _ready() and _exit_tree() methods.

var m_Name : String = ""
var m_Health : int = 0
var m_Speed : float = 0

var m_Password = "123456"
var m_GameStateFile = "user://savedgame1.sav"		# File path to the saved game state

@onready var NodeLineEditName : LineEdit = get_node("LineEditName")
@onready var NodeLineEditHealth : LineEdit = get_node("LineEditHealth")
@onready var NodeLineEditSpeed : LineEdit = get_node("LineEditSpeed")

# Node is initialized
func _ready() -> void:
	SaveLoadModule.Initialize(m_GameStateFile, m_Password)
	
# Node is destroyed
func _exit_tree() -> void:
	SaveLoadModule.Clear()

What About Some Helper Methods?

I added a few methods to help connecting the user interface to the character’s properties.

# Update the user interface with the current property values
func _UpdateUserInterface() -> void:
	NodeLineEditName.text = str(m_Name)
	NodeLineEditHealth.text = str(m_Health)
	NodeLineEditSpeed.text = str(m_Speed)
	
# Update the Object's properties according to the values in the user interface
func _UpdateObjectProperties() -> void:
	m_Name = str(NodeLineEditName.text)
	m_Health = int(NodeLineEditHealth.text)
	m_Speed = float(NodeLineEditSpeed.text)
	
# Checks if any of the user interface fields are empty
func _AreUserInterfaceFieldsEmpty() -> bool:
	return (NodeLineEditName.text == "" or		\
			NodeLineEditHealth.text == "" or	\
			NodeLineEditSpeed.text == "")

The Final Step Is the Button Press Callbacks

The Save button and Load button callbacks are called _OnButtonSaveGamePressed() and _OnButtonLoadGamePressed() respectively. In each of them, I first open the file with the appropriate access, serialize/deserialize, and finally close the file.

# Save the object's properties to the file
func _OnButtonSaveGamePressed():
	if (not _AreUserInterfaceFieldsEmpty()):	# Verify all user interface fields have values
		_UpdateObjectProperties()				# Pull the text from the user interface into the object's properties
		
		var status = SaveLoadModule.OpenFile(FileAccess.WRITE)	# Open the file with a write access
		if (status != OK):	
			print("Unable to open the file %s. Received error: %d" % [m_GameStateFile, status])
			return
			
		SaveLoadModule.Serialize(self)				# Write the object's properties into the file
		SaveLoadModule.CloseFile()					# Close the file

# Load the object's properties from the file
func _OnButtonLoadGamePressed():
	var status = SaveLoadModule.OpenFile(FileAccess.READ)	# Open the file with a read access
	if (status != OK):
		print("Unable to open the file %s. Received error: %d" % [m_GameStateFile, status])
		return
	
	SaveLoadModule.Deserialize(self)			# Read data from the file and update the object's properties
	SaveLoadModule.CloseFile()					# Close the file
	
	_UpdateUserInterface()			# Push the values from the properties to the user interface

Note: In the example above, I showed you how to serialize and deserialize one object. But the reality is that you will need to serialize or deserialize multiple objects at once. To do that, simply call the SaveLoadModule.Serialize(object) and SaveLoadModule.Deserialize(object) methods from an external script (not the object’s script like I did here), multiple times with all the objects you have. Don’t forget to close the file at the end.

Final Thoughts

I know it’s not an easy system to implement and might be confusing to some people, but I think this is the best approach to take for several reasons. It provides the security benefits I mentioned above, is very flexible in terms of the properties you want to write and read, and is generic enough to be reused in any game you create in the future.

To learn about other methods for saving and loading games in Godot, you can refer to the official Godot documentation page: https://docs.godotengine.org/en/stable/tutorials/io/saving_games.html.

If you read the entire article and didn’t give up, you are awesome! Taking your games to the next level is not rocket science, and I have plenty more articles on my Night Quest Games Blog that will help you be a better game developer. See you there!

If the information in this article was helpful to you, please consider supporting this blog through a donation. Your contributions are greatly appreciated and allow me to continue maintaining and developing this blog. Thank you!

4 thoughts on “Godot 4 Save and Load Games: How to Build a Robust System”

  1. I have a question – instead of having every object saving it’s properties in different scripts, why not have another singleton with all the object properties? As they are changed/new objects added in, they can update the singleton with that info, then when it is time to save, only the one script needs to be doing the work? I am new to game dev so I’m curious if there is a drawback to the method I just described?

    1. Hi Nadina,

      This is a great question which I thought about a lot. After trying both options I came to the conclusion that doing a singleton which holds all information has several drawbacks:
      1. As your game grows, the data structures will change and evolve, and since you hold the same structures both in the node itself and in the singleton script, you will have to make sure they are both updated. Easier said than done.
      2. In large games, holding the same variables in two different places will waste a lot of memory.
      3. One of the core principles of Object Oriented Programming is the Encapsulation principle, which states that every object has to handle it’s own functionality. By delegating the serialization / deserialization functionality, you are breaking this principle and opening your code to a bunch of possible bugs that are incredibly difficult to debug.

      With that said, if your game is simple enough and you feel more comfortable doing it with a singleton, go ahead and try it. But for large scale games, I suspect this method will be much more wasteful and difficult to maintain.

      Hope my comment helps!

      1. Yes, it really does! I am building a simple game for now, so in my mind using the singleton makes sense. In the future, I will absolutely keep it in the objects.

Leave a Comment

Your email address will not be published. Required fields are marked *