「せっかくのプレイ記録が消えた」「ロードしたら座標がぐちゃぐちゃになっていた」——セーブ機能は、地味なのに一度バグると一番プレイヤーを怒らせる部分です。しかもGodotには保存方法が複数あり、どれを使えばいいのか最初は迷いがちです。
この記事では、Godotでデータを永続化する3つの定番、 ConfigFile ・ JSON ・ カスタムリソース を、それぞれ「どんな時に使うか」「どう書くか」「改ざんにどう備えるか」まで、図とコードで整理します。最後には、実際に「どこでも中断できるチェックポイントセーブ」を1本組み立てます。
この記事でわかること
- セーブ先の大前提——
user://パス とFileAccessの基礎- ConfigFile ——設定情報を手軽に保存する
- JSON ——外部連携向き。Godot固有型の変換のコツ
- カスタムリソース ——セーブデータ全般の推奨手法(
ResourceSaver/ResourceLoader).tresと.resの違い、 改ざん対策 の現実的なライン
まず結論:3つの手法の使い分け
細かい話に入る前に、最適な使い道を先に押さえましょう。 迷ったらカスタムリソース 、というのが基本の指針です。

| 特徴 | ConfigFile | JSON | カスタムリソース(推奨) |
|---|---|---|---|
| 主な用途 | 設定ファイル(音量・キーコンフィグ) | 外部API連携・汎用データ | ゲームのセーブデータ全般 |
| Godot固有型 | 対応(Vector2などをそのまま) | 非対応(手動変換が必須) | 対応(そのまま保存できる) |
| コード量 | 少ない | 多い(変換処理が煩雑) | 最も少ない |
| 速度 | 速い | やや遅い(テキスト解析) | 速い(特にバイナリ形式) |
| 改ざん耐性 | 低い(テキスト) | 低い(テキスト) | 設定可能(バイナリ・暗号化) |
ざっくり言えば、 「人がいじる設定」はConfigFile、「他のツールと共有するデータ」はJSON、「プレイ記録そのもの」はカスタムリソース 。この対応を頭に入れておくと、以降の実装がすっと入ってきます。
セーブ先の大前提:user:// と FileAccess
3つの手法に共通する土台が2つあります。 どこに保存するか(user://) と、 どうやってファイルを読み書きするか(FileAccess) です。ここを最初に押さえておくと、あとの話がぶれません。
保存先は必ず user://
Godotのパスには res:// と user:// の2種類があります。
res://:プロジェクトのファイル置き場。 ビルド後は読み取り専用 になり、書き込めません。user://:OSが 用意する 書き込み可能な専用フォルダ への別名。セーブデータはここに置きます。
user:// の実体は、OSごとに違う場所へ自動でマッピングされます。コードでは user:// と書くだけでよく、OSの違いを気にせずに済むのがポイントです。

res:// に保存しようとして「エディタでは動くのに、書き出したゲームでセーブできない」というのは定番のつまずきです。 セーブ先は必ず user:// 、と覚えておきましょう。
ファイルの読み書きは FileAccess
実際にファイルを開いて中身を読み書きするのが FileAccess です。JSONを扱うときはこれを直接使います(ConfigFileとカスタムリソースは、内部でファイル入出力までやってくれるので直接触りません)。
# 書き込み:WRITEで開くと、無ければ新規作成・有ればまっさらにして書き込む
var file := FileAccess.open("user://memo.txt", FileAccess.WRITE)
if file: # open失敗時はnullが返るので必ず確認
file.store_string("Hello Save")
file.close() # 閉じ忘れると書き込みが確定しないことがある
# 読み込み:READで開く
if FileAccess.file_exists("user://memo.txt"): # 存在チェックを先に
var read := FileAccess.open("user://memo.txt", FileAccess.READ)
print(read.get_as_text())
read.close()
要点は3つです。 open() は失敗すると null を返すので必ずチェックすること、 close() を忘れない こと、そして読む前に file_exists() で存在を確認 すること。この3つを守るだけで、セーブまわりのクラッシュはぐっと減ります。
ConfigFile:設定情報の王道
ConfigFile は、WindowsのINIファイルのように [section] と key = value のペアでデータを持つ形式です。 ユーザーがいじる設定 ——音量、フルスクリーンの切り替え、キーコンフィグなど——の保存にうってつけです。Godot固有の型もそのまま扱え、FileAccess を直接触らずに読み書きできます。
![ConfigFileの構造図。設定データが[video]セクション(fullscreen/vsync)と[audio]セクション(master_volume)に分かれ、それぞれkey = value形式で1つの.cfgファイルに保存される様子](/73e28a67105642d82a297d70548b9b2a/20251208_save-load-system_configfile-structure.webp)
# settings_manager.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:
for key in settings[section]:
config.set_value(section, key, settings[section][key])
var err := config.save(SAVE_PATH) # 保存の戻り値は必ず確認
if err != OK:
printerr("設定の保存に失敗: %s" % error_string(err))
func load_settings() -> Dictionary:
var config := ConfigFile.new()
# ファイルが無ければデフォルトを返す(初回起動など)
if config.load(SAVE_PATH) != OK:
return default_settings.duplicate(true)
var result := default_settings.duplicate(true)
for section in default_settings:
for key in default_settings[section]:
# 第3引数にデフォルト値を渡せば、キー欠損でも安全に読める
result[section][key] = config.get_value(section, key, default_settings[section][key])
return result
ポイントは、 get_value() の第3引数にデフォルト値を渡せる ことです。設定項目を後から増やしても、古いセーブに無いキーは自動的にデフォルトで埋まります。「アップデートで設定が増えたら古いセーブが読めなくなった」という事故を、この一手で防げます。
JSON:Web連携と外部ツール
JSONは、可読性が高くエンジンに依存しない形式です。 Web APIとの通信 や、 Godot以外のツール(表計算・自作エディタなど)とのデータ交換 で強みを発揮します。
ただし弱点が1つ。 Godot固有の型(Vector2やColorなど)をそのまま扱えません 。保存するときは配列や辞書などの素朴な形に変換し、読むときに元の型へ戻す——この「往復の変換」を自分で用意する必要があります。
![JSONでのGodot固有型の往復変換の図。保存時はVector2(100,200)を配列[100,200]へ変換してテキスト化し、読み込み時は配列[100,200]をVector2へ復元する双方向の流れ](/c255c6f1e887744a1f4fcf20bf7155a4/20251208_save-load-system_json-convert.webp)
# save_load_json.gd
extends Node
const SAVE_PATH := "user://save_game.json"
# Godotの型 → JSONで扱える素朴な形へ
func _to_json(value):
if value is Vector2:
return { "_type": "Vector2", "x": value.x, "y": value.y }
return value
# JSONの形 → Godotの型へ復元
func _from_json(value):
# JSONのオブジェクトはDictionaryとして復元される
if value is Dictionary and value.get("_type") == "Vector2":
return Vector2(value["x"], value["y"])
return value
func save_game(state: Dictionary) -> void:
var file := FileAccess.open(SAVE_PATH, FileAccess.WRITE)
if not file:
printerr("JSONの書き込みに失敗しました")
return
var data := state.duplicate(true)
data["player_position"] = _to_json(state["player_position"]) # 変換をかけてから
file.store_string(JSON.stringify(data, "\t")) # 第2引数はインデント(見やすくする)
file.close()
func load_game() -> Dictionary:
if not FileAccess.file_exists(SAVE_PATH):
return {}
var text := FileAccess.get_file_as_string(SAVE_PATH) # 開いて読んで閉じるを1行で
var parsed = JSON.parse_string(text) # 失敗時はnullが返る
if parsed is Dictionary:
parsed["player_position"] = _from_json(parsed["player_position"])
return parsed
printerr("JSONのパースに失敗しました")
return {}
もう1つの落とし穴が 数値の型 です。JSONをパースすると、整数もすべて float として復元されることがあります。「HPが 100 のはずが 100.0 になり、== の比較が通らない」といった不一致は、読み込み後に int() で明示的に変換しておくと防げます。
このように、JSONは「素朴なデータを、Godotの外へ持ち出す/外から持ち込む」用途には最適ですが、 プレイ記録の保存にはやや手間がかかる のが実情です。そこで登場するのが次のカスタムリソースです。
カスタムリソース:セーブデータの推奨手法
Resource を継承したカスタムクラス(カスタムリソース)は、Godotの思想に最も合った、 セーブデータ向けの本命 です。@export を付けた変数は エンジンが自動でシリアライズ・デシリアライズ してくれるため、変換処理をまるごと書かずに済み、Vector2やColorも そのまま保存 できます。
カスタムリソースそのものの作り方(
class_name・@export・継承・duplicate())は カスタムリソースの作成と活用 で詳しく解説しています。ここではそれを ファイルに保存・復元する 部分に絞ります。
ステップ1:セーブデータ用のリソースを定義する
保存したい項目を @export で並べた「入れ物」を1つ作ります。これがセーブデータの設計図になります。
# save_game.gd
class_name SaveGame
extends Resource
@export var player_name: String = "Hero"
@export var health: int = 100
@export var position: Vector2 = Vector2.ZERO # 固有型もそのまま保存できる
@export var inventory: Dictionary = {}
@export var cleared_levels: Array[String] = []
@export var save_version: int = 1 # 後述のバージョン管理用
ステップ2:ResourceSaver と ResourceLoader で保存・復元する
保存は ResourceSaver.save()、復元は ResourceLoader.load() の2つだけ。JSONのような変換コードは要りません。

# save_manager.gd(Autoloadに登録して使うと便利)
extends Node
const SAVE_PATH := "user://savegame.res" # バイナリ形式(後述)
var current_save: SaveGame
func save_game() -> void:
if current_save == null:
current_save = SaveGame.new()
var err := ResourceSaver.save(current_save, SAVE_PATH) # 引数の順は (リソース, パス)
if err != OK:
printerr("セーブに失敗: %s" % error_string(err))
func load_game() -> bool:
if not ResourceLoader.exists(SAVE_PATH):
current_save = SaveGame.new() # 無ければ新規データで開始
return false
# CACHE_MODE_IGNOREで、キャッシュではなくファイルから確実に読み直す
var res := ResourceLoader.load(SAVE_PATH, "", ResourceLoader.CACHE_MODE_IGNORE)
if res is SaveGame: # 型で受け取れるので安全
current_save = res
return true
printerr("セーブファイルの読み込みに失敗しました")
current_save = SaveGame.new()
return false
注意 :
ResourceSaver.save()の引数は(リソース, パス)の順です。Godot 3では(パス, リソース)と逆だったため、古い記事を参考にするときは読み替えてください。
load_game() で res is SaveGame と 型で受け取れる のが、この手法の気持ちよさです。JSONのように「辞書のどのキーが何型か」を気にする必要がなく、current_save.health のように 補完の効く形 でアクセスできます。
.tres と .res の使い分け
ResourceSaver.save() の保存先は、拡張子で形式が決まります。

.tres(テキスト形式) :中身を人間が読めます。差分が見えるので Gitとの相性が良く、デバッグ向き 。開発中はこちらが便利です。.res(バイナリ形式) :中身は読めませんが、 速くてファイルも小さい 。改ざんも.tresよりは手間がかかります。 リリースビルドはこちら が定番です。
開発中は .tres で中身を確認しながら作り、出荷時に .res へ切 り替える、という運用が扱いやすいです。
セキュリティの注意 :カスタムリソースは便利ですが、
.tres/.resは内部にスクリプト参照を埋め込めるため、 信頼できない出所のセーブファイル(他人が配布した改造セーブなど)をそのままロードすると、埋め込まれたコードが実行される リスクがあります。自分のゲーム内で完結するローカルセーブなら問題になりませんが、セーブの共有・投稿を許すゲームでは、そうしたデータには JSON を使うか、内容を検証してから読むのが安全です。
実践:どこでも中断できるチェックポイントセーブを組む
RPGのセーブポイント、ローグライクの「やめる」中断セーブ、オープンワールドのオートセーブ——ジャンルは違っても、やることは同じです。 散らばったゲームの状態を1つのセーブデータに集めて保存し、起動時に読み戻して各所へ配り直す 。ここまでのカスタムリソースを使って、この流れを実際に組んでみましょう。
