【Unity】Managerが増えて壊れた時のBootstrapシーン入門:初期化を1か所に集める

作成: 2026-07-16

GameManager・AudioManager・SaveManager——マネージャーが5個を超えたあたりから、「Gameシーンから再生するとnullで落ちる」「Awakeの順番次第で壊れる」「タイトルへ戻ると常駐オブジェクトが2個に増える」という定番の泥沼が始まります。答えは、常駐するものを最初のBootstrapシーンで1回だけ・決めた順番で組み立てること。初期化順の明示、Additiveなシーン構成、どのシーンから再生しても動く保険まで、Manager群の土台を作り直します。

GameManager を作り、シングルトンDontDestroyOnLoad も覚えた。AudioManager、SaveManager、QuestManager……便利なので増やしていくと、5個を超えたあたりで様子が変わってきます。Gameシーンから直接再生すると nullで即落ちる(タイトルから再生しないとマネージャーが居ない)。AwakeでManager同士が呼び合い、 実行順のくじ引き で壊れたり直ったりする。タイトルへ戻ると常駐オブジェクトが 2個に増えている

1個ずつは正しく作ったのに、組み合わせた途端に壊れる——足りないのは個々の作り方ではなく、 全体を組み立てる場所 です。この記事では、常駐するものを最初の Bootstrapシーン で1回だけ・決めた順番で組み立てる形へ、Manager群の土台を作り直します。

バラバラだったマネージャーをBootstrapが土台の上に組み立てるイメージ

この記事でわかること

  • マネージャーを増やすほど壊れやすくなる 本当の原因(生成場所と初期化順が散らばる)
  • 常駐物を最初のシーンで1回だけ組み立てる Bootstrapシーン の作り方
  • Awakeのくじ引きをやめて、 初期化順を1か所に明示する Initialize() フロー
  • どのシーンから再生しても動く ようにする RuntimeInitializeOnLoadMethod の保険

動作確認環境: Unity 2022.3 LTS / Unity 6

Sponsored

マネージャーが増えるほど壊れる逆説

マネージャーはそれぞれ正しいのに、なぜ増えると壊れるのか。原因は機能ではなく 配置 にあります。よくある構成はこうです——GameManagerはタイトルシーンに、AudioManagerはどのシーンにも念のため置いてあり、SaveManagerはGameシーンだけに居る。それぞれが自分のAwakeで DontDestroyOnLoad し、ついでに他のマネージャーを探して呼ぶ。

マネージャー配置の比較図。左は各シーンにマネージャーが散らばり、Awakeで互いを呼び合って蜘蛛の巣状の依存になり壊れている。右はBootstrapが1か所ですべてのマネージャーを順番に組み立てて各シーンへ供給している

この構成には、生まれつき3つの穴があります。

  • 生成場所が散らばる: どのシーンから再生するかで「居るマネージャー」が変わる。Gameシーンから再生するとGameManagerが居なくてnull——タイトルからの通し再生でしかテストできなくなります
  • 初期化順がくじ引き: UnityはAwakeの実行順を既定では保証しません。AudioManagerのAwakeがSaveManagerより先に走って「保存された音量」を読めない、という壊れ方が 再生するたびに変わる運ゲー になります
  • 重複が生まれる: シーンに置いたマネージャーは、そのシーンへ戻るたびに新しく生成されます。DontDestroyOnLoad 済みの先代と合わせて2個——BGMが二重に鳴るあの現象です(DontDestroyOnLoadの記事 で見た定番事故です)

3つに共通するのは、 「いつ・どこで・どの順で作るか」を誰も決めていない ことです。だから、決める場所を1つ作ります。

Bootstrapシーン:組み立てる場所を1つに決める

Bootstrapシーン は、ゲームの最初に1回だけ読み込まれる、組み立て専用の小さなシーンです。中身はマネージャーを載せたルートオブジェクトが1つだけ。プレイヤーには一瞬も見えないまま、組み立てが終わったらタイトルへ進みます。

起動の流れ図。起動するとまずBootstrapシーンが読み込まれ、セーブ・オーディオ・ゲーム進行のマネージャーを順番に組み立ててDontDestroyOnLoadし、完成してからタイトルシーンへ進むタイムライン
using UnityEngine;
using UnityEngine.SceneManagement;

public class Bootstrapper : MonoBehaviour
{
    // 常駐マネージャーはすべてこのオブジェクトの子に置き、Inspectorでつなぐ
    [SerializeField] private SaveManager saveManager;
    [SerializeField] private AudioManager audioManager;
    [SerializeField] private GameManager gameManager;

    private void Awake()
    {
        DontDestroyOnLoad(gameObject); // 常駐化はここ1回だけ

        // 初期化順を、ここに1回だけ・上から順に書く
        saveManager.Initialize();              // 1. セーブデータを読む
        audioManager.Initialize(saveManager);  // 2. 保存された音量を反映
        gameManager.Initialize();              // 3. ゲーム進行を準備

        SceneManager.LoadScene("Title");       // 組み立て完了、タイトルへ
    }
}

ポイントは3つあります。まず、常駐マネージャーを 1つのルートオブジェクトの子 に集めたこと。DontDestroyOnLoad はルートに1回で済み、「常駐しているものの一覧」がヒエラルキーでそのまま見えます。次に、他のシーンには マネージャーを一切置かない こと。生成場所が1つなら重複は原理的に起きません。そして、依存の向きが Bootstrap→各マネージャーの一方通行 になったこと。マネージャー同士が探し合う蜘蛛の巣が、上から配る木構造に変わります。

tips: この「依存関係を組み立てる唯一の場所」は、設計用語で Composition Root と呼ばれます。VContainerのようなDIコンテナが自動化しているのも本質的にはこれです——手動のBootstrapで考え方を体に入れておくと、将来の導入がスムーズです。

初期化順はAwakeではなくInitialize()で明示する

先ほどのコードで、各マネージャーは Initialize() という 自前のメソッド で初期化していました。Awakeがあるのになぜ?——ここがBootstrap設計の芯です。

Awakeは 呼ばれる順番を選べません。Script Execution Orderで並べ替える手もありますが、設定画面の奥に依存関係が隠れてしまい、コードを読んでも順番が分かりません。そこでルールを1本引きます。

  • Awakeに書いてよいのは、自分だけで完結する準備 (自分のフィールドの初期化など)
  • 他のマネージャーに触る処理は Initialize() 移し、呼ぶ順番はBootstrapだけが決める
public class AudioManager : MonoBehaviour
{
    private float bgmVolume;

    // Awakeには書かない:この時点でSaveManagerが準備済みか分からないから

    public void Initialize(SaveManager save)
    {
        // 呼ばれた時点でSaveManagerの準備完了が保証されている
        bgmVolume = save.Data.bgmVolume;
        ApplyVolume();
    }
}

Initialize(SaveManager save) のように 必要な相手を引数で受け取る のもポイントです。「AudioManagerはSaveManagerに依存している」という関係が、シグネチャとしてコードに残ります。FindObjectOfTypeInstance で暗黙に探すのをやめると、依存の全体像はBootstrapper1ファイルを読むだけで把握できるようになります。

Sponsored

どのシーンから再生しても動く保険

Bootstrapを作った直後に、開発体験の問題が1つ生まれます。 Gameシーンを開いて再生ボタンを押すと、Bootstrapを通っていないのでマネージャーが誰も居ない ——毎回Bootstrapシーンへ戻ってから再生するのは、正直やっていられません。

どのシーンから再生しても動く保険の比較図。左はGameシーンから直接再生してマネージャー不在でnullエラー。右はRuntimeInitializeOnLoadMethodがシーン読み込み前にBootstrapプレハブを自動生成し、同じ土台が立ち上がる

そこで、Unityが用意している RuntimeInitializeOnLoadMethod を保険に使います。これは「シーンが読み込まれる前に必ず呼ばれるstaticメソッド」を登録できる属性で、ここで「Bootstrapがまだ居なければ、プレハブから生成する」を仕込みます。

using UnityEngine;

public static class BootstrapLoader
{
    [RuntimeInitializeOnLoadMethod(RuntimeInitializeLoadType.BeforeSceneLoad)]
    private static void EnsureBootstrap()
    {
        if (Object.FindAnyObjectByType<Bootstrapper>() != null) return; // もう居るなら何もしない

        var prefab = Resources.Load<GameObject>("BootstrapRoot"); // Resources/BootstrapRoot.prefab
        Object.Instantiate(prefab);
    }
}

準備は2つ。マネージャー一式を載せたルートを BootstrapRootプレハブ にして Resources フォルダへ置くこと、そして Bootstrapper の最後の LoadScene("Title") を「Bootstrapシーンから起動した時だけ」実行するよう分けること(エディタで任意のシーンから再生した時は、今のシーンをそのまま使いたいからです)。

これで どのシーンから再生ボタンを押しても、同じ土台が最初に立ち上がります。ビルドではBuild Settingsの先頭に置いたBootstrapシーンから正規の順で起動し、エディタでは保険が効く——通し再生でしかテストできない問題が消えます。

シーン構成:常駐層と入れ替え層

Bootstrapができると、シーンの見方が2層に整理されます。

シーンの2層構造の図。下の常駐層にはBootstrapで組み立てたセーブ・オーディオ・ゲーム進行のマネージャーが最後まで残り続け、上の入れ替え層ではタイトル・ゲーム・リザルトのシーンが順に載せ替えられる
  • 常駐層: Bootstrapが組み立てた DontDestroyOnLoad のマネージャー群。起動から終了まで1個ずつ居続ける
  • 入れ替え層: Title・Game・Resultなどの中身のシーン。シーン管理の記事LoadScene で丸ごと入れ替わる

中身のシーンは「その場面のものだけ」を持ち、常駐が必要なものは一切持ちません。シーンを何回行き来しても、常駐層は増えも減りもしない——これが2層構成の保証です。

tips: さらに進めると、常駐層を「Persistentシーン」としてAdditiveロード(LoadSceneMode.Additive)で持ち、中身のシーンだけを載せ替える構成もあります。広いマップの分割ロードなどで使う発展形ですが、まずはDontDestroyOnLoadの2層で十分です。

実践:Title→Game→ResultでAudioとSaveだけ残す

仕上げに、GameManagerの記事 で作った60秒コインゲーム(Title→Game→Result)をBootstrap構成へ載せ替えます。RPGでもパズルでもランゲームでも、シーンをまたぐゲームなら同じ手順です。

実践の構成図。Bootstrapシーンでセーブ・オーディオ・ゲーム進行を組み立てた後、タイトル・ゲーム・リザルトを行き来しても、下の常駐層でBGMとセーブデータが途切れず残り続ける

手順は4つだけです。

  1. Bootstrapシーンを新規作成: 空のシーンに BootstrapRoot オブジェクトを置き、子に SaveManagerAudioManagerGameManager を並べ、親に Bootstrapper を付けてInspectorでつなぐ
  2. 各シーンからマネージャーを撤去: Title・Game・Resultに置いてあったマネージャーを全部削除する(ここが一番気持ちいい瞬間です)
  3. Awakeの依存処理をInitialize()へ移す: 「他のマネージャーを探して呼ぶ」行を各自のAwakeから抜き、Initialize() に移して、Bootstrapperに順番どおり並べる
  4. 保険を仕込む: BootstrapRoot をプレハブ化してResourcesへ置き、BootstrapLoader を追加。Build Settingsの先頭をBootstrapシーンにする

Playを押して確かめましょう。まずBootstrapから通しで再生——タイトルのBGMが鳴り、Game→Resultと進んでタイトルへ戻っても、 BGMは途切れず、ヒエラルキーの常駐オブジェクトは最初から最後まで同じ1セット のはずです。次にGameシーンを直接開いて再生——保険が効いて、マネージャーが揃った状態でゲームが始まります。最後にResultからタイトルへ何往復かして、常駐層が増えていないことを確認したら完成です。

ポイントは2つ。 生成と初期化順を1か所(Bootstrapper)に集めたから、「どこから再生しても・何往復しても」同じ土台が保証される こと。そして 依存を引数で渡す形にしたから、マネージャーが増えてもBootstrapperを読めば全体像が分かる ことです。

おまけ:先に知っておくと良いこと

  • Singletonをやめる必要はない: Instance での参照が便利な場面は残ります。Bootstrapと組み合わせるなら「生成はBootstrapだけが行い、SingletonのAwake生成ロジックは重複自壊の保険に格下げする」のが現実的です。詳しくは シングルトンの記事
  • マネージャー間の通知は疎結合に: Bootstrapは「組み立て」を1か所にする話で、実行中のやり取りまで直結にする話ではありません。ゲーム中のイベント通知は イベントチャネル が引き続き活躍します
  • DIコンテナはこの延長線上: VContainerやZenjectは、この記事で手書きした「組み立て」を属性や設定で自動化するものです。チーム開発や大規模化で手動Bootstrapperが長くなりすぎたら検討します
  • ロード画面もBootstrapの隣に: 起動時のロードが長くなってきたら、非同期ロード と組み合わせて、Bootstrapの組み立て中にロゴやロード画面を出す構成に発展できます

まとめ

  • マネージャーが増えて壊れる原因は、 生成場所・初期化順・重複 を誰も決めていないこと
  • Bootstrapシーン で常駐物を1回だけ・決めた順番で組み立てる。他のシーンにはマネージャーを置かない
  • 依存のある初期化はAwakeから Initialize() へ移し、順番はBootstrapperに1回だけ書く。必要な相手は引数で渡す
  • RuntimeInitializeOnLoadMethod の保険で、どのシーンから再生しても同じ土台が立ち上がる
  • シーンは 常駐層と入れ替え層 の2層で考える

マネージャーを「増やすと壊れるもの」から「増やしても土台が受け止めるもの」へ——次にマネージャーを1つ足す時、あなたはBootstrapperのどの行に、その1行を挿しますか?