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 コンポーネントをシーンに追加

  1. 上の GDScript を res://addons/autosaver/AutoSaver.gd などに保存。
  2. プレイヤーシーンを開いて、子ノードとして Node を追加し、名前を AutoSaver に変更。
  3. そのノードに AutoSaver.gd をアタッチ。
  4. インスペクターで以下のパラメータを設定:
    • 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_enteredrequest_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」などのコンポーネントを組み合わせていくと、
プロジェクト全体の見通しもかなり良くなりますよ。