In game development, a save/load system that permanently saves player progress and settings and restores them on the next startup is a crucial element forming the foundation of the game experience.
Godot Engine provides several powerful built-in features for data persistence. This article, aimed especially at beginner to intermediate developers, comprehensively compares the three most commonly used methods—JSON, ConfigFile, and Custom Resources—and explains each method's characteristics with concrete implementation examples.
Data Persistence: Why This Topic Is Important
Godot Engine extensively uses its own data types like Vector2 and Color. Understanding the mechanism (serialization and deserialization) for safely and efficiently saving and restoring these Godot-specific data types on the file system is key to building a robust save system.
1. ConfigFile: Simple Structure Ideal for Settings Files
The ConfigFile class manages data in sections with key-value pairs, similar to Windows INI files or Unix configuration files.
ConfigFile Pros and Cons
| Pros | Cons |
|---|---|
| Simplicity | Not suitable for complex hierarchical structures or array storage |
| Human-readable | Inefficient for large amounts of game data |
| Godot Native |
Implementation Example Using ConfigFile
extends Node
const SAVE_PATH = "user://settings.cfg"
func save_settings(volume: float, fullscreen: bool) -> void:
var config = ConfigFile.new()
config.set_value("audio", "master_volume", volume)
config.set_value("video", "fullscreen", fullscreen)
var error = config.save(SAVE_PATH)
if error != OK:
print("Failed to save settings: ", error)
func load_settings() -> Dictionary:
var config = ConfigFile.new()
var error = config.load(SAVE_PATH)
if error != OK:
return {"volume": 1.0, "fullscreen": false}
var volume = config.get_value("audio", "master_volume", 1.0)
var fullscreen = config.get_value("video", "fullscreen", false)
return {"volume": volume, "fullscreen": fullscreen}
2. JSON: Versatility and Support for Complex Data Structures
JSON (JavaScript Object Notation) is a lightweight data exchange format widely used in the web world.
JSON Pros and Cons
| Pros | Cons |
|---|---|
| Versatility | Cannot directly save Godot-specific types |
| Complex Structures | Requires manual conversion for reading/writing |
| Easy to Debug |
Implementation Example Using JSON
extends Node
const SAVE_PATH = "user://game_save.json"
func save_game_data(player_position: Vector2, inventory: Array) -> void:
var save_data = {
"player_pos": [player_position.x, player_position.y],
"inventory": inventory,
"timestamp": Time.get_unix_time_from_system()
}
var json_string = JSON.stringify(save_data, "\t")
var file = FileAccess.open(SAVE_PATH, FileAccess.WRITE)
if file:
file.store_string(json_string)
file.close()
func load_game_data() -> Dictionary:
if not FileAccess.file_exists(SAVE_PATH):
return {}
var file = FileAccess.open(SAVE_PATH, FileAccess.READ)
if file:
var json_string = file.get_as_text()
file.close()
var parse_result = JSON.parse_string(json_string)
if parse_result is Dictionary:
var loaded_data = parse_result
var pos_array = loaded_data.get("player_pos", [0.0, 0.0])
loaded_data["player_pos"] = Vector2(pos_array[0], pos_array[1])
return loaded_data
return {}
3. Custom Resource: The Definitive Godot-Native Save Method
In Godot Engine, the most recommended and powerful save method is using Custom Resources.
Custom Resource Pros and Cons
| Pros | Cons |
|---|---|
| Type Safety | Risk of external tampering |
| Direct Support for Godot-Specific Types | Not suitable for integration with external applications |
| Minimal Code |
Implementation Example Using Custom Resource
# SaveGame.gd
class_name SaveGame
extends Resource
@export var coins := 0
@export var player_global_position := Vector2(0, 0)
@export var unlocked_levels := []
# SaveManager.gd
extends Node
const SAVE_PATH = "user://game_save.tres"
var current_save: SaveGame = null
func load_game() -> void:
if ResourceLoader.exists(SAVE_PATH):
current_save = ResourceLoader.load(SAVE_PATH, "", ResourceLoader.CACHE_MODE_IGNORE)
else:
current_save = SaveGame.new()
func save_game() -> void:
var error = ResourceSaver.save(current_save, SAVE_PATH)
if error != OK:
print("Save failed: ", error)
Comprehensive Comparison: Which Method Should You Choose?
| Feature | ConfigFile | JSON | Custom Resource |
|---|---|---|---|
| Main Use | Settings files | Complex data, external integration | Entire game save data |
| Data Structure | Simple key/value | Complex hierarchies, arrays, dictionaries | Complex hierarchies, Godot-specific types |
| Godot-Specific Types | Supported | Not supported (manual conversion required) | Fully Supported |
| Code Amount | Small | Large (conversion processing needed) | Smallest |
Practical Usage: Guidelines for Choosing
- ConfigFile: Use for game settings (volume, graphic settings, key bindings)
- JSON: Use for data integration with external tools, large amounts of static data (item lists)
- Custom Resource: Most recommended for game save data (player state, inventory, map information, etc.)
Summary
When implementing save/load systems in Godot Engine, ConfigFile is optimal for settings files, JSON for versatile data exchange, and Custom Resource for the main game save data.
Especially Custom Resource aligns best with Godot's design philosophy, and by simply using @export variables and ResourceSaver/ResourceLoader, you can persist complex game data surprisingly easily.