Basic Game Save and loading in Godot 4
When developing a game, one of the key features players expect is the ability to save and load their progress. A well-designed save system ensures that players can pick up right where they left off, enhances replayability, and helps maintain player satisfaction. In Godot 4, handling game saves is straightforward and flexible, leveraging Godot’s built-in file APIs and the rich GDScript language. In this post, we’ll walk through the fundamental concepts of creating, writing, and loading save files, as well as some best practices to keep your save system robust.
Why Implement a Custom Save System?
While Godot doesn’t provide a built-in save system out-of-the-box, its file handling tools make it simple to roll your own solution. By creating a custom system, you can:
-
Define your own data formats:
Decide exactly what game data should be persisted (player position, inventory items, level progress, etc.).
-
Maintain flexibility
Expand and update your save data schema easily as your game grows.
-
Control versioning
Handle changes to your save data between game updates with custom logic.
What to Save?
Before you start coding, it’s crucial to determine what needs saving. Typical information might include:
-
Player state
Current position, health, experience points, inventory.
-
World State
NPC positions, quest progression, environment, items
-
Settings
Options like sound volume, graphics quality, or control bindings.
Keep your save data focused and organized. Overloading your save file with unnecessary details will increase complexity and risk bugs. Start small and expand as needed.
Choosing a Data Format
Godot’s File API lets you read and write data in several formats, such as binary, JSON, or Godot’s built-in var serialization. Two common approaches are:
-
JSON Files
Human-readable and easy to debug. Perfect for small-to-medium projects or when you need human readability.
-
Binary/Custom Format
More compact and potentially faster, but less transparent and harder to debug.
Setting Up a Binary Save System
For my game I chose Binary Save System. To get started We’ll create a singleton (autoload) to manage all saving and loading. This centralizes code, making it easier to maintain and call from anywhere in your project.
Creating the Singleton
Create the Script: Create a SaveSystem.gd script in your res:// directory.
Autoload It: In Project > Project Settings > Globals, add SaveSystem.gd, give it a name like SaveSystem, and click “Add”. Now you can access it from all scripts via SaveSystem.
Example Save System Script
Here’s an example using binary operations. We’ll maintain a current_data dictionary as the in-memory representation of our game state. Our binary file will store this dictionary using store_var() and get_var():
# SaveSystem.gd
extends Node
var save_path = "user://save_game.bin"
var current_data = {}
func _ready():
# Load the save if it exists, otherwise initialize defaults
if FileAccess.file_exists(save_path):
load_game()
else:
current_data = {
"player": {
"position": Vector2(0, 0),
"health": 100,
"inventory": []
},
"world": {
"level": 1,
"doors_opened": []
}
}
func save_game():
var file = FileAccess.open(save_path, FileAccess.WRITE)
if file:
# store_var() serializes any Variant (dictionary, array, primitive, etc.)
file.store_var(current_data)
file.close()
func load_game():
if not FileAccess.file_exists(save_path):
return # No file, use defaults or do nothing
var file = FileAccess.open(save_path, FileAccess.READ)
if file:
# get_var() reconstructs the Variant from the binary data
var data = file.get_var()
file.close()
if typeof(data) == TYPE_DICTIONARY:
current_data = data
else:
push_warning("Loaded data is not a dictionary! Resetting to defaults.")
_reset_defaults()
func _reset_defaults():
current_data = {
"player": {
"position": Vector2(0, 0),
"health": 100,
"inventory": []
},
"world": {
"level": 1,
"doors_opened": []
}
}
In this setup:
- current_data: Holds the current in-memory game state.
- save_game(): Writes current_data directly as binary data using store_var().
- load_game(): Reads the binary data using get_var() and updates current_data.
Updating and Saving Player Data
As the player moves through the world or changes state, you’ll need to update current_data accordingly. For example, when the player reaches a checkpoint, you might update their position and inventory, then save:
#### In Player.gd or another relevant script
func save_player_state():
if SaveSystem.current_data.has("player"):
SaveSystem.current_data["player"].position = global_position
SaveSystem.current_data["player"].health = health
SaveSystem.current_data["player"].inventory = inventory
SaveSystem.save_game()
This ensures that the file on disk always reflects the current state of the player’s progress.
Loading Saved Data Back Into the Game
When starting the game or entering a scene, you need to apply the saved data to your game objects
func load_player_state():
var data = SaveSystem.current_data.get("player", null)
if data:
global_position = data.position
health = data.health
inventory = data.inventory
Best Practices and Tips
-
Centralize Logic:
Keep saving and loading logic in a single place (like our SaveSystem) to avoid confusion. -
Test Frequently:
After each gameplay change that affects saved data, test the entire save/load cycle. -
Graceful Handling of Missing or Corrupted Files:
If loading fails, revert to default values and continue. -
Backup Saves:
Before overwriting a file, consider making a backup copy so you can roll back if something fails.
Conclusion
Switching to a binary format for your Godot 4 saves can streamline your save/load process, improve performance, and give you more control over your data format. While not as human-readable as JSON, binary data is easy to handle via store_var() and get_var(), and it provides a convenient solution for many games.
Whether you’re tackling a small indie title or a larger, more complex project, mastering binary-based game saves will serve as a solid foundation for a robust player experience. With the right planning and testing, your players can confidently dive into your world, knowing their progress is both safe and efficiently managed.