【Godot】Autoloadによるシーンをまたぐデータ管理

作成: 2025-06-20最終更新: 2026-07-07

GodotのAutoloadを使って、スコア、設定、音量、シーン遷移などゲーム全体で共有するデータと機能を管理する方法を解説します。

ステージをクリアして次のシーンへ移動したら、スコアが0に戻ってしまった。町に戻ったら、さっき開けた宝箱がまた閉じている。音量設定をタイトル画面とゲーム画面の両方で使いたい。

こうした「シーンをまたいでも残したい情報」を扱うために使うのが Autoload です。便利ですが、何でも入れると巨大なグローバル変数置き場になってしまいます。この記事では、Autoloadの使いどころと引き際を、ゲーム実装の例で整理します。

AutoloadがシーンをまたいでスコアやHP、設定を保持するイメージ

この記事でわかること

  • Autoloadがシーン切り替え後も残る理由
  • PlayerDataAudioSave などの分け方
  • UI更新をシグナルで疎結合にする方法
  • Autoload、シーン引き継ぎ、Resourceの使い分け
Sponsored


Autoloadは「ゲーム中ずっと残るノード」

Autoloadは、ゲーム起動時にGodotが自動で読み込み、シーンが切り替わっても残し続けるノードまたはスクリプトです。登録した名前は、どのスクリプトからでも PlayerData.score のように参照できます。

PlayerDataがStage 1、Town、Bossのどのシーンからも参照される図

通常のノードは、シーンを切り替えると古いシーンごと破棄されます。Autoloadはシーンの外側に常駐するため、スコア、音量設定、セーブ管理のような情報を保てます。

ただし、どこからでも触れるということは、どこからでも壊せるということでもあります。Autoloadは「便利な倉庫」ではなく、ゲーム全体で1つだけ存在すべき責務を置く場所 と考えましょう。


ゲームでは何に使う?

Autoloadが向いているものは、シーンをまたいで必要になる実行時の状態や、ゲーム全体で共通の機能です。

Autoloadによく置く5つの役割。PlayerData・Audio・Save・SceneTransition・GameEventsをアイコンで示す
  • PlayerData: スコア、所持金、現在HP、開封済み宝箱、取得済みアイテム。
  • Audio: BGM再生、効果音再生、音量設定。
  • Save: セーブ/ロード、ファイルパス、バージョン管理。
  • SceneTransition: フェード付きシーン遷移、ロード画面。
  • GameEvents: 実績解除、設定変更、ポーズ通知などのシグナルバス。

逆に、特定ステージだけの敵、弾、足場、UIの一時表示はAutoloadに入れません。それらはそのシーンの中で完結させる方が、再利用しやすくバグも追いやすいです。


Autoloadの登録方法

Autoloadはプロジェクト設定から登録します。

  1. res://autoload/PlayerData.gd のようにスクリプトを作る。
  2. Godotの「プロジェクト設定」を開く。
  3. 「Autoload」タブでスクリプトを選ぶ。
  4. ノード名を PlayerData にして「追加」する。
GodotのAutoload設定画面

登録後は、どのスクリプトからでも PlayerData という名前でアクセスできます。


実践:PlayerDataを作る

RPGやアクションゲームでよく使う、スコア、HP、宝箱の開封状態を管理する例です。

ここで大事なのは、scorecurrent_health を単なるグローバル変数として置くだけで終わらせないことです。値を書き換える入口を add_score()take_damage() のような関数に集めると、「スコアが増えたら通知する」「HPが0未満にならないようにする」といったルールも同じ場所にまとめられます。

まず全体像を図で見ると、関係はこうなります。

EnemyやTreasureChestがPlayerDataの関数を呼び、PlayerDataが値を更新してUIや実績へシグナル通知する流れ

左側の EnemyTreasureChest は、PlayerData の関数を呼ぶだけです。中央の PlayerData は、スコアやHPを更新し、変更が起きたことをシグナルで知らせます。右側の HPBarScoreLabelAchievement は、その通知を受け取って自分の表示や処理だけを行います。

この流れにしておくと、「値を変える場所」と「画面に表示する場所」が混ざりません。コードを読むときも、まず PlayerData を見ればデータ更新のルールが分かり、UI側を見れば表示の作り方が分かります。

# res://autoload/PlayerData.gd
extends Node

signal score_changed(score: int)
signal health_changed(current_health: int, max_health: int)

var score: int = 0
var max_health: int = 100
var current_health: int = 100
var opened_chests: Dictionary = {}

func add_score(amount: int) -> void:
    score += amount

    # スコア変更をUIや実績システムへ通知する
    score_changed.emit(score)

func take_damage(amount: int) -> void:
    current_health = max(0, current_health - amount)

    # HP表示側が自分で更新できるように、変更後の値を渡す
    health_changed.emit(current_health, max_health)

func is_chest_opened(chest_id: String) -> bool:
    return opened_chests.get(chest_id, false)

func mark_chest_opened(chest_id: String) -> void:
    opened_chests[chest_id] = true

signal score_changed(score: int) は、「スコアが変わったときに外へ知らせます」という宣言です。PlayerData 自身は、どのUIが表示しているか、実績システムがあるか、効果音を鳴らすかを知りません。ただ「スコアが変わった」という事実だけを通知します。

敵を倒したら、スコアを加算します。

# Enemy.gd
func die() -> void:
    # 敵は保存場所を知らず、PlayerDataへスコア加算だけ依頼する
    PlayerData.add_score(100)
    queue_free()

敵側も、PlayerData.score += 100 と直接書くのではなく、add_score() を呼びます。こうしておくと、あとから「コンボ倍率をかける」「スコア上限を作る」「スコア変更時に実績を確認する」といったルールを追加しても、敵のコードを全部探して直す必要がありません。

宝箱は、シーンに戻ってきたときも開封済みか確認できます。

# TreasureChest.gd
@export var chest_id: String = "forest_01_gold_chest"
var is_opened := false

func _ready() -> void:
    # シーンに入り直した時も、Autoloadに残った開封状態を読む
    is_opened = PlayerData.is_chest_opened(chest_id)
    $Sprite2D.frame = 1 if is_opened else 0

func open() -> void:
    if is_opened:
        return

    is_opened = true

    # 開封状態と報酬を、シーンをまたいで残るデータへ反映する
    PlayerData.mark_chest_opened(chest_id)
    PlayerData.add_score(50)
    $Sprite2D.frame = 1

宝箱の例では、chest_id がかなり重要です。"forest_01_gold_chest" のように宝箱ごとのIDを決めておくことで、シーンを出入りしても「この宝箱は開封済みか」を同じキーで確認できます。IDが重複すると別の宝箱まで開いた扱いになるので、マップ名やエリア名を含めた分かりやすい名前にしておくと安全です。

このように「シーンが消えても残したい状態」はAutoloadと相性が良いです。ただし、何でも PlayerData に置くのではなく、プレイヤーの進行状況として残す意味があるものに絞ります。画面に一瞬出るダメージ表示、敵の現在位置、ステージ内だけのスイッチ状態などは、そのシーンのノードで管理した方が自然です。


シグナルでUI更新を切り離す

Autoloadを使うと、ついUIを直接触りたくなります。

たとえば「スコアが増えたから、画面のラベルを書き換えたい」と考えるのは自然です。そこで PlayerData から直接 ScoreLabel を探して更新すると、一見すぐ動きます。

# 悪い例: PlayerDataがUIの場所を知っている
func add_score(amount: int) -> void:
    score += amount

    # NG: Autoloadが具体的なUI階層を知ってしまう
    get_node("/root/Main/HUD/ScoreLabel").text = str(score)

しかし、この書き方には落とし穴があります。

  • HUD の名前を変えると、get_node("/root/Main/HUD/ScoreLabel") が壊れる。
  • タイトル画面やリザルト画面では、そもそも Main/HUD/ScoreLabel が存在しないかもしれない。
  • スコア表示を ScoreLabel から ScorePanel に作り直すたび、Autoload側のコードまで直す必要がある。
  • スコア変更に反応したいものが増えると、PlayerData がUI、効果音、実績、セーブ処理を全部知り始める。

つまり、PlayerData が「スコアを持つ係」だけでなく、「どの画面のどのラベルを更新するか」まで背負ってしまいます。これが進むと、Autoloadがゲーム全体の細かいUI構造に依存し、画面を作り替えるたびに壊れやすくなります。

そこで、Autoloadは状態変更をシグナルで通知し、UI側が聞く形にします。役割を分けると、次のようになります。

役割担当すること知らなくてよいこと
PlayerDataスコアを保存する、スコア変更を通知するどのUIが表示するか
ScoreLabel通知を受けて自分の文字を変えるスコアがどの敵から増えたか
Achievement必要なら同じ通知を受けて実績を確認するUIの場所
# ScoreLabel.gd
extends Label

func _ready() -> void:
    # UI側がPlayerDataの通知を聞きに行く
    PlayerData.score_changed.connect(_on_score_changed)

    # 表示開始時に現在値も反映する
    _on_score_changed(PlayerData.score)

func _on_score_changed(score: int) -> void:
    text = "Score: %d" % score

この形なら、PlayerDataScoreLabel の存在を知りません。もう少し噛み砕くと、PlayerData は「スコアが変わりました。新しい値は100です」と放送するだけです。誰が聞いているかは気にしません。

一方、ScoreLabel は自分から PlayerData.score_changed を聞きに行きます。通知が届いたら、自分の text を更新します。スコアを表示するUIが ScoreLabel から ScorePanel に変わった場合も、新しいUI側で同じシグナルを聞けばよいので、PlayerData は変更しません。

_ready() の中で _on_score_changed(PlayerData.score) を一度呼んでいるのも重要です。シグナルは「これから起きる変更」を知らせるものなので、UIが表示された時点ですでにスコアが100だった場合、そのままでは初期表示が更新されません。最初に現在値を読み込んでおくと、画面を開いた瞬間から正しいスコアを表示できます。

この考え方は、スコア以外にも使えます。

  • HPが変わったら、HPバーだけが自分を更新する。
  • 音量設定が変わったら、設定画面のスライダーとAudio管理がそれぞれ反応する。
  • ポーズ状態が変わったら、メニュー、入力制御、BGMがそれぞれ必要な処理をする。

Autoloadは「状態を持つ・通知する」まで。UIは「通知を受けて表示する」まで。この境界を守ると、後から画面を差し替えたり、表示を増やしたりしても壊れにくくなります。

シグナル自体の考え方は、 シグナルによるノード間の連携 で詳しく扱っています。


詰め込みすぎを防ぐ分割

初心者がやりがちな失敗は、GameManager というAutoloadに全部入れることです。

GameManagerに詰め込みすぎず、GameState、Audio、Save、Settingsへ責務を分ける図

最初は1つでも構いませんが、役割が増えたら分けます。

分ける理由は、ファイルをきれいに見せるためだけではありません。GameManager.gd にスコア、音量、セーブ、シーン遷移、設定、実績を全部入れると、変更の影響範囲が読みにくくなります。音量設定を直しただけなのにセーブ処理を壊す、シーン遷移の修正中にスコア更新まで触ってしまう、という事故が起きやすくなります。

Autoload置くもの
GameStateスコア、進行度、ポーズ状態
PlayerDataHP、所持金、装備、開封済み宝箱
AudioBGM、SE、音量設定
Saveセーブ/ロード、ファイルアクセス
Settings画面設定、キー設定、言語設定

たとえば、音量スライダーを作っているときに触るべきファイルが Audio.gdSettings.gd だけなら、調査範囲がかなり狭くなります。セーブデータの形式を変えるときも、主に Save.gd を見ればよくなります。これは、後から自分で直すときにも、チームで分担するときにも効いてきます。

判断基準は、 「このファイルの責任を1文で説明できるか?」 です。説明が「スコアも音もセーブもシーン遷移も全部」になったら、分割のタイミングです。

分割しすぎにも注意します。最初から CoinManagerGemManagerKeyManager のように細かく分けすぎると、逆にどこを見ればよいか分からなくなります。小さなゲームなら、まず PlayerDataAudioSave くらいの大きさで十分です。役割が増えてから分ける、という順番で問題ありません。


Autoload以外を選ぶ場面

Autoloadは万能ではありません。次の3つは使い分けましょう。

Autoload、引き継ぎ、Resourceの使い分け
方法向いているもの
Autoloadゲーム全体で共有する実行時状態スコア、設定、音量、セーブ管理
シーン引き継ぎ次のシーンだけに渡したい一時情報戦闘結果、選択したステージ番号
Resource設計データ、編集可能なデータ表武器データ、敵ステータス、アイテム定義

武器の攻撃力や敵の最大HPのような「設計データ」は、Autoloadの変数に直書きするより、Resourceに分けた方が管理しやすいです。実行中に変わる「現在の所持金」や「開封済み宝箱」はAutoloadが向いています。

ここで迷いやすいのは、「ゲーム中ずっと使うデータなら全部Autoloadでよいのでは?」という点です。ポイントは、実行中に変わる状態か、制作時に決めておく設計データか です。

たとえば、ポーションの回復量や剣の攻撃力は、ゲームを作る側が事前に決めるデータです。こういうものはResourceにしておくと、エディタ上で編集しやすく、アイテムごとのデータも増やしやすくなります。

一方、プレイヤーが今ポーションを何個持っているか、どの宝箱を開けたか、現在の所持金はいくらか、という情報はプレイ中に変わります。これは実行時の状態なので、Autoloadやセーブデータで管理する対象になります。

Resourceの使い方は、 カスタムリソースの作成と活用 につながります。


よくあるつまずき

どこから値が変わったか分からない

PlayerData.score += 100 をどこからでも書けるようにすると、スコアが増えた原因を追いにくくなります。値の変更は関数経由にします。

これは小さなプロジェクトではすぐ問題になりません。敵を倒したときだけスコアが増えるなら、直接書いても動きます。しかし、宝箱、クエスト報酬、コンボボーナス、広告視聴報酬、デバッグコマンドなど、スコアを変える場所が増えると「なぜ今100点増えたのか」を追うのが難しくなります。

# 推奨: 変更の入口を関数に集約する
func add_score(amount: int) -> void:
    score += amount

    # 変更後に必ず通知する流れをここへ集約する
    score_changed.emit(score)

関数に集約しておけば、print_debug() を入れて原因を調べる場所も1つになります。スコア加算のSE、実績チェック、上限処理などを追加するときも、この関数へ足せば済みます。

テストしにくくなる

Autoloadへの直接依存が多いと、ユニットテストで差し替えにくくなります。計算だけの関数はAutoloadから切り出し、引数と戻り値で確認できる形にするとテストしやすいです。

初心者のうちはテストを書かなくても構いません。ただ、計算とAutoloadの状態変更を分ける考え方は普段の実装でも役立ちます。たとえば「コンボ倍率を含めた報酬計算」は、PlayerData の中で直接 score を書き換える前に、ただの計算関数として切り出せます。

func calculate_reward(base_score: int, combo: int) -> int:
    return base_score * max(1, combo)

この関数は、Autoloadに登録されていなくても確認できます。calculate_reward(100, 3) なら300になる、combo が0でも最低1倍になる、というように、ゲーム画面を動かさずに考えられます。状態を変える処理と、値を計算する処理を分けるだけで、バグの原因を切り分けやすくなります。

GUTでのテストは、 GUTでGodotのユニットテストを実装する で扱っています。

Autoload同士が循環参照する

GameStateAudio を呼び、AudioGameState を呼び返すようになると、依存関係が分かりにくくなります。通知だけなら signal を使い、片方向の依存に寄せましょう。

循環参照は、最初は見えにくい問題です。たとえば GameState が「ポーズになったからBGMを止めて」と Audio を呼び、Audio が「BGMが止まったからGameStateを更新して」と呼び返すと、どちらが主導しているのか分からなくなります。処理順によっては、片方の初期化が終わる前にもう片方を触ってしまうこともあります。

この場合は、どちらか一方を主役に決めます。GameStatepause_changed をシグナルで通知し、Audio がそれを聞いてBGMを調整する、という形なら流れは一方向です。GameStateAudio の中身を知りません。Audio は通知を受けて自分の仕事をするだけです。


まとめ

Autoloadは、シーンをまたいで残したいデータや、ゲーム全体で1つだけ存在する機能を置くための仕組みです。

  • スコア、設定、セーブ、音量管理などに向いている。
  • 特定シーンだけの状態は入れない。
  • UI更新はAutoloadから直接触らず、シグナルで通知する。
  • 役割が増えたら GameStateAudioSave のように分ける。
  • 設計データはResource、一時的な受け渡しはシーン引き継ぎも検討する。

迷ったときは、 「これはゲーム全体で1つだけ残るべきものか?」 と考えてください。答えが明確にYesならAutoload。そうでなければ、シーン内管理やResourceの方がきれいに収まることが多いです。