Godotでセーブ周りを実装し始めると、だいたいこんな悩みが出てきますよね。
- プレイヤー、敵、アイテム…それぞれのシーンに
save()/load()的な処理を書き始めて、どこで何を保存しているか分からなくなる - エリア移動のたびに「このシーンにもセーブ処理コピペしないと…」という不安
- 「ボス撃破」「重要アイテム取得」みたいなイベントの直後にオートセーブしたいのに、毎回シグナル接続とファイル書き込み処理を書くのがダルい
Godot標準だけでやろうとすると、
- シーンごとにセーブマネージャーを継承したり
- 各ノードが自分でファイルに書き込んだり
と、継承と依存関係がどんどん増えていきます。
ここはやっぱり「継承よりコンポーネント」ですよね。
今回紹介する 「AutoSaver」コンポーネント は、
- エリア移動(シーン遷移)直前・直後
- 重要イベント(ボス撃破、キーアイテム取得など)の直後
に、バックグラウンドでオートセーブを実行するための、再利用可能なコンポーネントです。
任意のノードにペタッと貼って、シグナルをつなぐだけでOK。
セーブ処理の実装も、専用のコールバックを 1 箇所書くだけで済むようにしています。
【Godot 4】イベント後は勝手に保存!「AutoSaver」コンポーネント
以下が、コピペでそのまま使える AutoSaver.gd のフルコードです。
extends Node
class_name AutoSaver
## オートセーブ用コンポーネント
##
## - 任意のノードにアタッチして使う
## - 「今すぐセーブしてほしい」というタイミングで `request_auto_save()` を呼ぶだけ
## - 実際のセーブ内容は `build_save_data` / `apply_load_data` をオーバーライド or コールバックで実装
##
## 「継承より合成」思想で、どのノードにも後付けできるようにしています。
## セーブデータの保存先パス
@export_file("*.save") var save_path: String = "user://autosave.save"
## セーブ処理を「少し遅らせて」まとめるためのディレイ(秒)
## 連続でセーブ要求が来ても、この時間内なら 1 回にまとめる
@export_range(0.0, 5.0, 0.1) var save_delay: float = 0.3
## セーブ中に UI などで「Saving...」表示を出したいときに使うフラグ
@export var show_saving_flag_on_owner: bool = true
@export var saving_flag_property_name: String = "is_saving"
## デバッグログを出すかどうか
@export var verbose_log: bool = true
## セーブ完了後に呼ばれるコールバック(任意)
## 例: `func _on_saved(success: bool) -> void: ...`
@export var on_saved_callback: Callable
## ロード完了後に呼ばれるコールバック(任意)
@export var on_loaded_callback: Callable
## セーブに含める「グローバル情報」を提供するノード(任意)
## 例: GameState シングルトンなど
@export var global_state_provider: NodePath
## 内部状態
var _save_requested: bool = false
var _is_saving: bool = false
var _save_timer: float = 0.0
func _ready() -> void:
if verbose_log:
print("[AutoSaver] Ready on node: ", get_path())
_save_timer = 0.0
func _process(delta: float) -> void:
if _save_requested and not _is_saving:
_save_timer -= delta
if _save_timer <= 0.0:
_do_save()
## === パブリック API =======================================================
## 外部から「オートセーブしてほしい」ときに呼ぶ関数
## 例: エリア移動直後 / ボス撃破直後 / アイテム取得直後 など
func request_auto_save(reason: String = "") -> void:
if verbose_log:
print("[AutoSaver] Auto-save requested. reason=", reason)
_save_requested = true
_save_timer = save_delay
## ゲーム開始時やロード画面から呼び出す用
func load_auto_save() -> void:
if not FileAccess.file_exists(save_path):
if verbose_log:
print("[AutoSaver] No save file found at: ", save_path)
return
var file := FileAccess.open(save_path, FileAccess.READ)
if file == null:
push_warning("[AutoSaver] Failed to open save file for reading: %s" % save_path)
return
var data_json := file.get_as_text()
file.close()
var result = JSON.parse_string(data_json)
if typeof(result) != TYPE_DICTIONARY:
push_warning("[AutoSaver] Invalid save data format.")
return
var save_data: Dictionary = result
# 自身(オーナー)に適用するデータ
if save_data.has("owner_state"):
apply_load_data(save_data["owner_state"])
# グローバル情報の適用
if save_data.has("global_state"):
_apply_global_state(save_data["global_state"])
if on_loaded_callback.is_valid():
on_loaded_callback.call(save_data)
if verbose_log:
print("[AutoSaver] Loaded auto-save from: ", save_path)
## === セーブ / ロードのコアロジック ======================================
func _do_save() -> void:
_save_requested = false
_is_saving = true
_set_saving_flag(true)
var save_data: Dictionary = {}
# この AutoSaver がアタッチされているノード(owner)の状態を取得
save_data["owner_path"] = owner.get_path()
save_data["owner_state"] = build_save_data()
# グローバル情報も一緒に保存したい場合
var global_state = _collect_global_state()
if global_state != null:
save_data["global_state"] = global_state
# JSON にシリアライズして保存
var json_text := JSON.stringify(save_data)
var file := FileAccess.open(save_path, FileAccess.WRITE)
if file == null:
push_warning("[AutoSaver] Failed to open save file for writing: %s" % save_path)
_finish_save(false)
return
file.store_string(json_text)
file.close()
if verbose_log:
print("[AutoSaver] Auto-saved to: ", save_path)
if on_saved_callback.is_valid():
on_saved_callback.call(true)
_finish_save(true)
func _finish_save(success: bool) -> void:
_is_saving = false
_set_saving_flag(false)
## === オーナーの状態を保存・復元するためのフック ==========================
## セーブデータを組み立てる
## デフォルト実装では、位置・回転・スケールだけを保存
## 必要に応じて、この関数を「スクリプト継承」か「コールバック」などで拡張してください。
func build_save_data() -> Dictionary:
var d: Dictionary = {}
if owner is Node2D:
d["position"] = (owner as Node2D).position
d["rotation"] = (owner as Node2D).rotation
d["scale"] = (owner as Node2D).scale
elif owner is Node3D:
d["position"] = (owner as Node3D).position
d["rotation"] = (owner as Node3D).rotation
d["scale"] = (owner as Node3D).scale
# ここに HP, 所持アイテム, 現在エリア名などを追加してもOK
# d["hp"] = owner.hp
# d["area_name"] = owner.current_area_name
return d
## セーブデータをオーナーに適用する
func apply_load_data(data: Dictionary) -> void:
if owner is Node2D:
var n := owner as Node2D
if data.has("position"):
n.position = data["position"]
if data.has("rotation"):
n.rotation = data["rotation"]
if data.has("scale"):
n.scale = data["scale"]
elif owner is Node3D:
var n3 := owner as Node3D
if data.has("position"):
n3.position = data["position"]
if data.has("rotation"):
n3.rotation = data["rotation"]
if data.has("scale"):
n3.scale = data["scale"]
# ここで HP やアイテムなどを復元してもOK
# if data.has("hp"):
# owner.hp = data["hp"]
## === グローバル状態の取得・適用 ==========================================
func _collect_global_state() -> Dictionary:
if global_state_provider.is_empty():
return {}
var node := get_node_or_null(global_state_provider)
if node == null:
push_warning("[AutoSaver] global_state_provider not found: %s" % global_state_provider)
return {}
if node.has_method("build_global_save_data"):
return node.call("build_global_save_data")
return {}
func _apply_global_state(data: Dictionary) -> void:
if global_state_provider.is_empty():
return
var node := get_node_or_null(global_state_provider)
if node == null:
push_warning("[AutoSaver] global_state_provider not found for loading: %s" % global_state_provider)
return
if node.has_method("apply_global_load_data"):
node.call("apply_global_load_data", data)
## === ユーティリティ =======================================================
func _set_saving_flag(value: bool) -> void:
if not show_saving_flag_on_owner:
return
if owner == null:
return
# owner に任意のプロパティを生やして、UI 側から監視できるようにする
# 例: UI 側で `if player.is_saving: show_saving_icon()` など
@warning_ignore("unsafe_property_access")
owner.set(saving_flag_property_name, value)
使い方の手順
ここでは 2D アクションゲームを例にして、以下の 3 つのケースで使ってみましょう。
- ① プレイヤーの位置やステータスをオートセーブ
- ② エリア移動(シーン遷移)直後にオートセーブ
- ③ ボス撃破イベント直後にオートセーブ
シーン構成例
プレイヤーシーンの例:
Player (CharacterBody2D) ├── Sprite2D ├── CollisionShape2D └── AutoSaver (Node) ← このノードに AutoSaver.gd をアタッチ
ゲーム全体を管理するルートシーンの例:
Main (Node) ├── GameState (Node) ← グローバル情報を持つノード(任意) ├── CurrentArea (Node2D) ← 現在のエリアのルート └── UI (CanvasLayer)
手順①: AutoSaver コンポーネントをシーンに追加
- 上の GDScript を
res://addons/autosaver/AutoSaver.gdなどに保存。 - プレイヤーシーンを開いて、子ノードとして
Nodeを追加し、名前をAutoSaverに変更。 - そのノードに
AutoSaver.gdをアタッチ。 - インスペクターで以下のパラメータを設定:
save_path:user://autosave_player.saveなどsave_delay: 0.3 くらい(連続イベントをまとめたい場合)show_saving_flag_on_owner: true(UIでセーブ中アイコンを出したい場合)
これで、プレイヤーに「オートセーブ能力」が後付けされました。
プレイヤースクリプト側からは、こう呼び出せばOKです。
# Player.gd (CharacterBody2D)
@onready var auto_saver: AutoSaver = $AutoSaver
func _on_area_changed(new_area_name: String) -> void:
# エリア移動直後にオートセーブ
auto_saver.request_auto_save("area_changed to %s" % new_area_name)
func _on_boss_defeated() -> void:
# ボス撃破直後にオートセーブ
auto_saver.request_auto_save("boss_defeated")
手順②: どんな情報を保存するかカスタマイズ
デフォルト実装だと「位置・回転・スケール」しか保存しないので、
プレイヤーの HP や所持アイテムなども保存したい場合は、
AutoSaver をそのまま使うのではなく、プレイヤー側にセーブデータ構築用の関数を用意して、それを呼び出すようにしましょう。
例として、プレイヤー側にこんな関数を用意します:
# Player.gd
var hp: int = 100
var max_hp: int = 100
var inventory: Array[String] = []
func build_player_save_data() -> Dictionary:
return {
"position": position,
"hp": hp,
"max_hp": max_hp,
"inventory": inventory,
}
func apply_player_load_data(data: Dictionary) -> void:
if data.has("position"):
position = data["position"]
if data.has("hp"):
hp = data["hp"]
if data.has("max_hp"):
max_hp = data["max_hp"]
if data.has("inventory"):
inventory = data["inventory"]
そして、AutoSaver 側の build_save_data / apply_load_data を軽く継承して上書きします。
# PlayerAutoSaver.gd
extends AutoSaver
class_name PlayerAutoSaver
func build_save_data() -> Dictionary:
# owner は Player を想定
if owner.has_method("build_player_save_data"):
return owner.call("build_player_save_data")
return super.build_save_data()
func apply_load_data(data: Dictionary) -> void:
if owner.has_method("apply_player_load_data"):
owner.call("apply_player_load_data", data)
else:
super.apply_load_data(data)
シーンでは、先ほどの AutoSaver ノードに、この PlayerAutoSaver.gd をアタッチし直すだけです。
これで「プレイヤー専用のセーブ仕様」をコンポーネント側に閉じ込めつつ、プレイヤー本体のスクリプトはすっきり保てます。
手順③: エリア移動(シーン遷移)でのオートセーブ
エリア遷移を管理する AreaManager 的なノードがあるとします。
Main (Node)
├── GameState (Node)
├── AreaManager (Node)
└── Player (CharacterBody2D)
└── AutoSaver (PlayerAutoSaver)
AreaManager.gd から、プレイヤーの AutoSaver にアクセスして、
シーン切り替え直後に request_auto_save() を呼べばOKです。
# AreaManager.gd
@onready var player: Node = $"../Player"
@onready var auto_saver: AutoSaver = player.get_node("AutoSaver")
func change_area(target_scene_path: String) -> void:
# シーン切り替え処理(例)
var new_scene: PackedScene = load(target_scene_path)
var instance = new_scene.instantiate()
var main := get_parent()
var current_area := main.get_node("CurrentArea")
current_area.queue_free()
instance.name = "CurrentArea"
main.add_child(instance)
# 切り替えが完了した直後にオートセーブ
auto_saver.request_auto_save("area_changed to %s" % target_scene_path)
手順④: ゲーム開始時にオートセーブからロード
タイトル画面やロード画面から「続きから」を選んだときに、
プレイヤーの AutoSaver に対して load_auto_save() を呼び出します。
# TitleScreen.gd
@onready var main: Node = get_tree().root.get_node("Main")
@onready var player: Node = main.get_node("Player")
@onready var auto_saver: AutoSaver = player.get_node("AutoSaver")
func _on_continue_button_pressed() -> void:
auto_saver.load_auto_save()
get_tree().change_scene_to_file("res://scenes/Main.tscn")
メリットと応用
この「AutoSaver」コンポーネントを使うメリットはかなり分かりやすいです。
- セーブ処理がシーンツリーのあちこちに散らばらない
→ セーブに関するコードは基本的に AutoSaver と、その周辺だけ。 - どのノードにも後付けできる
→ プレイヤーだけでなく、動く床、リフト、重要なギミックなどにも簡単にオートセーブ機能を付与できます。 - ノード階層を深くしなくていい
→ 「PlayerWithSave」「BossWithSave」などの継承ツリーを増やさず、コンポーネントを 1 個足すだけで済む。 - セーブのタイミングをイベント側で自由に決められる
→ ボス撃破、会話完了、チェックポイント通過など、シグナルを受け取った側がrequest_auto_save()を呼ぶだけ。
レベルデザイン的にも、
- 「このチェックポイント通過でオートセーブしたい」→ チェックポイントシーンに AutoSaver を追加して、
body_enteredでrequest_auto_save()を呼ぶだけ。 - 「この移動床の位置を復元したい」→ 移動床シーンに AutoSaver を追加して、位置情報を保存するように拡張。
といった感じで、シーンをまたいだセーブ仕様をコンポーネントに集約できるのが気持ちいいですね。
改造案: セーブ回数を制限する(チェックポイント制)
例えば「このステージでは 3 回までしかオートセーブできない」といった、
ローグライク風の制限を入れたい場合は、AutoSaver を少し拡張してみましょう。
# LimitedAutoSaver.gd
extends AutoSaver
class_name LimitedAutoSaver
@export var max_save_count: int = 3
var _current_save_count: int = 0
func request_auto_save(reason: String = "") -> void:
if _current_save_count >= max_save_count:
if verbose_log:
print("[LimitedAutoSaver] Save limit reached. Skipping auto-save.")
return
_current_save_count += 1
super.request_auto_save(reason + " (count=%d/%d)" % [_current_save_count, max_save_count])
このコンポーネントをチェックポイントシーンなどに付けておけば、
「残機制チェックポイント」「高難度モード用の制限付きオートセーブ」みたいな仕掛けも簡単に作れます。
継承ツリーを増やさず、「AutoSaver」「LimitedAutoSaver」「PlayerAutoSaver」などのコンポーネントを組み合わせていくと、
プロジェクト全体の見通しもかなり良くなりますよ。
