【Godot】Implementing Save/Load Systems - Comprehensive Comparison of JSON, ConfigFile, and Custom Resources

Created: 2025-12-08

A comprehensive comparison of the three main methods for persisting game data in Godot Engine (JSON, ConfigFile, Custom Resources) with concrete implementation examples for beginners.

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

ProsCons
SimplicityNot suitable for complex hierarchical structures or array storage
Human-readableInefficient 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

ProsCons
VersatilityCannot directly save Godot-specific types
Complex StructuresRequires 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

ProsCons
Type SafetyRisk of external tampering
Direct Support for Godot-Specific TypesNot 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?

FeatureConfigFileJSONCustom Resource
Main UseSettings filesComplex data, external integrationEntire game save data
Data StructureSimple key/valueComplex hierarchies, arrays, dictionariesComplex hierarchies, Godot-specific types
Godot-Specific TypesSupportedNot supported (manual conversion required)Fully Supported
Code AmountSmallLarge (conversion processing needed)Smallest

Practical Usage: Guidelines for Choosing

  1. ConfigFile: Use for game settings (volume, graphic settings, key bindings)
  2. JSON: Use for data integration with external tools, large amounts of static data (item lists)
  3. 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.