ゲーム開発において、プレイヤーの進行状況や設定を永続的に保存し、次回起動時に復元できる セーブ/ロードシステム は、ゲーム体験の根幹をなす重要な要素です。
Godot Engineでは、データを永続化するためのいくつかの強力な組み込み機能が提供されています。本記事では、最も一般的に使用される3つの方法、すなわち JSON、ConfigFile、そして カスタムリソース(Custom Resource) を徹底的に比較し、それぞれの特徴と具体的な実装方法を解説します。
結論ファースト:3つの手法の使い分け
詳細に入る前に、各手法の最適な使い道をまとめます。
| 特徴 | ConfigFile | JSON | カスタムリソース (推奨) |
|---|---|---|---|
| 主な用途 | 設定ファイル (音量, キーコンフィグ) | 外部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つの主要なセーブ/ロード手法を、実践的な観点から解説しました。
| 特徴 | ConfigFile | JSON | カスタムリソース |
|---|---|---|---|
| 主な用途 | 設定ファイル | 外部連携、静的データ | ゲームのセーブデータ |
| Godot固有型 | 完全サポート | 非サポート(手動変換必要) | 完全サポート |
| コード量 | 少ない | 多い(変換処理が必要) | 最も少ない |
ほとんどのゲーム開発シーンにおいて、カスタムリソース がその型安全性、コードの簡潔さ、そしてパフォーマンスの観点から最適な選択肢です。まずはカスタムリソースでの実装を検討し、必要に応じて ConfigFile や JSON を使い分けるのが、堅牢かつ効率的な開発への近道と言えるでしょう。