【Godot】セーブ/ロードシステムの実装 - JSON、ConfigFile、カスタムリソースの徹底比較

作成: 2025-12-08最終更新: 2025-12-16

Godot Engineでゲームデータを永続化するための主要な3つの方法(JSON、ConfigFile、カスタムリソース)を、エラー処理やパフォーマンス、セキュリティの観点から比較し、具体的な実装例とともに解説します。

ゲーム開発において、プレイヤーの進行状況や設定を永続的に保存し、次回起動時に復元できる セーブ/ロードシステム は、ゲーム体験の根幹をなす重要な要素です。

Godot Engineでは、データを永続化するためのいくつかの強力な組み込み機能が提供されています。本記事では、最も一般的に使用される3つの方法、すなわち JSONConfigFile、そして カスタムリソース(Custom Resource) を徹底的に比較し、それぞれの特徴と具体的な実装方法を解説します。


結論ファースト:3つの手法の使い分け

詳細に入る前に、各手法の最適な使い道をまとめます。

特徴ConfigFileJSONカスタムリソース (推奨)
主な用途設定ファイル (音量, キーコンフィグ)外部API連携, 汎用データゲームのセーブデータ全般
Godot固有型完全サポート非サポート (手動変換が必須)完全サポート
コード量少ない多い (変換処理が煩雑)最も少ない
パフォーマンス高速やや低速 (テキスト解析)高速 (特にバイナリ形式)
改ざん耐性低い (テキスト)低い (テキスト)設定可能 (バイナリ/暗号化)

1. ConfigFile:シンプルな設定情報の王道

ConfigFileは、WindowsのINIファイルのように [section]key = value のペアで構成されるシンプルな形式です。主に、ユーザーが変更可能なゲーム設定の保存に適しています。

実装例:グラフィック設定の保存と読み込み

# SettingsManager.gd
extends Node

const SAVE_PATH = "user://settings.cfg"

# デフォルト設定
var default_settings = {
    "video": { "fullscreen": false, "vsync": true },
    "audio": { "master_volume": 0.8 }
}

func save_settings(settings: Dictionary) -> void:
    var config = ConfigFile.new()
    for section in settings.keys():
        for key in settings[section].keys():
            config.set_value(section, key, settings[section][key])

    var error = config.save(SAVE_PATH)
    if error != OK:
        printerr("設定の保存に失敗しました: %s" % error_string(error))

func load_settings() -> Dictionary:
    var config = ConfigFile.new()

    if not FileAccess.file_exists(SAVE_PATH):
        return default_settings

    var error = config.load(SAVE_PATH)
    if error != OK:
        printerr("設定の読み込みに失敗: %s。デフォルト設定を使用します。" % error_string(error))
        return default_settings

    var loaded_settings = default_settings.duplicate(true)
    for section in default_settings.keys():
        for key in default_settings[section].keys():
            var loaded_value = config.get_value(section, key, default_settings[section][key])
            loaded_settings[section][key] = loaded_value

    return loaded_settings

2. JSON:Web連携と汎用性の高さ

JSONは、その高い可読性と汎用性から、Web APIとの通信や、Godot以外のツールとデータを交換する際に強力な選択肢となります。ただし、Godot固有の型を直接扱えないため、シリアライズ・デシリアライズの際に一手間必要です。

実装例:ヘルパー関数を使った安全なデータ変換

# SaveLoadJSON.gd
extends Node

const SAVE_PATH = "user://save_game.json"

# GodotのデータをJSON互換の形式に変換
func _data_to_json(data):
    if data is Vector2:
        return [data.x, data.y]
    if data is Color:
        return [data.r, data.g, data.b, data.a]
    return data

# JSON形式からGodotのデータに復元
# 注意: JSONパース後、数値はintまたはfloatになるため両方チェックする
func _json_to_data(data):
    if data is Array and data.size() == 2 and (data[0] is float or data[0] is int):
        return Vector2(data[0], data[1])
    if data is Array and data.size() == 4 and (data[0] is float or data[0] is int):
        return Color(data[0], data[1], data[2], data[3])
    return data

func save_game(game_state: Dictionary) -> void:
    var file = FileAccess.open(SAVE_PATH, FileAccess.WRITE)
    if not file:
        printerr("JSONファイルのオープンに失敗しました。")
        return

    var serializable_state = game_state.duplicate(true)
    serializable_state["player_position"] = _data_to_json(game_state["player_position"])

    var json_string = JSON.stringify(serializable_state, "  ")
    file.store_string(json_string)
    file.close()

func load_game() -> Dictionary:
    if not FileAccess.file_exists(SAVE_PATH):
        return {}

    var file = FileAccess.open(SAVE_PATH, FileAccess.READ)
    if not file:
        printerr("JSONファイルの読み込みに失敗しました。")
        return {}

    var parse_result = JSON.parse_string(file.get_as_text())
    file.close()

    if parse_result is Dictionary:
        var loaded_data = parse_result
        loaded_data["player_position"] = _json_to_data(loaded_data["player_position"])
        return loaded_data
    else:
        printerr("JSONのパースに失敗しました。")
        return {}

3. カスタムリソース:Godotにおけるセーブ/ロードの推奨手法

Resourceを継承したカスタムクラス(カスタムリソース)は、Godotの思想に最も合致した、最も推奨されるセーブ手法です。@exportアノテーションを付けた変数は、エンジンが自動的にシリアライズ・デシリアライズしてくれるため、コード量が劇的に削減され、型安全も保証されます。

ステップ1:セーブデータ用リソースクラスの作成

# SaveGame.gd
class_name SaveGame
extends Resource

# 基本的なプレイヤー情報
@export var player_name: String = "Hero"
@export var health: int = 100
@export var global_position: Vector2 = Vector2.ZERO

# 複雑なデータ構造
@export var inventory: Dictionary = {}
@export var unlocked_levels: Array[String] = []

ステップ2:セーブ/ロード管理クラスの実装

# SaveManager.gd
extends Node

const SAVE_PATH_TRES = "user://savegame.tres" # テキスト形式 (デバッグ向き)
const SAVE_PATH_RES = "user://savegame.res"   # バイナリ形式 (高速・改ざん耐性)

var current_save: SaveGame

func save_game(is_binary: bool = true) -> void:
    if not current_save:
        printerr("セーブデータがありません。")
        return

    var path = SAVE_PATH_RES if is_binary else SAVE_PATH_TRES
    var error = ResourceSaver.save(current_save, path)
    if error != OK:
        printerr("セーブに失敗しました: %s" % error_string(error))
    else:
        print("ゲームを正常にセーブしました: %s" % path)

func load_game() -> bool:
    var path = SAVE_PATH_RES if ResourceLoader.exists(SAVE_PATH_RES) else SAVE_PATH_TRES

    if not ResourceLoader.exists(path):
        print("セーブファイルが見つかりません。新しいゲームを開始します。")
        current_save = SaveGame.new()
        return false

    # CACHE_MODE_IGNORE を指定して、キャッシュではなくファイルから直接読み込む
    var loaded_resource = ResourceLoader.load(path, "", ResourceLoader.CACHE_MODE_IGNORE)
    if loaded_resource is SaveGame:
        current_save = loaded_resource
        print("ゲームを正常にロードしました: %s" % path)
        return true
    else:
        printerr("セーブファイルの読み込みに失敗しました。")
        current_save = SaveGame.new()
        return false

# ゲームの状態をリソースに反映
func update_save_data(player: Node2D, inventory_data: Dictionary):
    if not current_save:
        current_save = SaveGame.new()
    current_save.global_position = player.global_position
    current_save.inventory = inventory_data

パフォーマンスとセキュリティ:.tres vs .res

  • .tres (テキスト形式): 人間が読めるためデバッグが容易ですが、ファイルサイズが大きく、改ざんも簡単です。
  • .res (バイナリ形式): 高速でファイルサイズも小さいですが、人間には読めません。改ざんは.tresより困難ですが、暗号化なしでは完全な保護にはなりません。リリースビルドではこちらを推奨します。

よくある間違いとベストプラクティス

よくある間違いベストプラクティス
絶対パスや res:// に保存する必ず user:// パスを使用する。これにより、OSごとの書き込み権限問題を回避できる。
エラーハンドリングを怠るsave()load() の戻り値を必ずチェックし、失敗した場合の処理を記述する。
Godot固有型をJSONに直接渡すJSONを使う場合は、Vector2やColorなどを配列に変換するヘルパー関数を用意する。
セーブ/ロード中のフリーズ大きなデータの読み書きは Thread を使って非同期で行う。
セーブデータのバージョンを考慮しないリソースに version 変数を追加し、古いバージョンからの変換処理を実装する。

まとめ

本記事では、Godot Engineにおける3つの主要なセーブ/ロード手法を、実践的な観点から解説しました。

特徴ConfigFileJSONカスタムリソース
主な用途設定ファイル外部連携、静的データゲームのセーブデータ
Godot固有型完全サポート非サポート(手動変換必要)完全サポート
コード量少ない多い(変換処理が必要)最も少ない

ほとんどのゲーム開発シーンにおいて、カスタムリソース がその型安全性、コードの簡潔さ、そしてパフォーマンスの観点から最適な選択肢です。まずはカスタムリソースでの実装を検討し、必要に応じて ConfigFileJSON を使い分けるのが、堅牢かつ効率的な開発への近道と言えるでしょう。