GameManager を作り、シングルトン と DontDestroyOnLoad も覚えた。AudioManager、SaveManager、QuestManager……便利なので増やしていくと、5個を超えたあたりで様子が変わってきます。Gameシーンから直接再生すると nullで即落ちる(タイトルから再生しないとマネージャーが居ない)。AwakeでManager同士が呼び合い、 実行順のくじ引き で壊れたり直ったりする。タイトルへ戻ると常駐オブジェクトが 2個に増えている。
1個ずつは正しく作ったのに、組み合わせた途端に壊れる——足りないのは個々の作り方ではなく、 全体を組み立てる場所 です。この記事では、常駐するものを最初の Bootstrapシーン で1回だけ・決めた順番で組み立てる形へ、Manager群の土台を作り直します。
この記事でわかること
- マネージャーを増やすほど壊れやすくなる 本当の原因(生成場所と初期化順が散らばる)
- 常駐物を最初のシーンで1回だけ組み立てる Bootstrapシーン の作り方
- Awakeのくじ引きをやめて、 初期化順を1か所に明示する
Initialize()フロー- どのシーンから再生しても動く ようにする
RuntimeInitializeOnLoadMethodの保険
動作確認環境: Unity 2022.3 LTS / Unity 6
マネージャーが増えるほど壊れる逆説
マネージャーはそれぞれ正しいのに、なぜ増えると壊れるのか。原因は機能ではなく 配置 にあります。よくある構成はこうです——GameManagerはタイトルシーンに、AudioManagerはどのシーンにも念のため置いてあり、SaveManagerはGameシーンだけに居る。それぞれが自分のAwakeで DontDestroyOnLoad し、ついでに他のマネージャーを探して呼ぶ。

この構成には、生まれつき3つの穴があります。
- 生成場所が散らばる: どのシーンから再生するかで「居るマネージャー」が変わる。Gameシーンから再生するとGameManagerが居なくてnull——タイトルからの通し再生でしかテストできなくなります
- 初期化順がくじ引き: UnityはAwakeの実行順を既定では保証しません。AudioManagerのAwakeがSaveManagerより先に走って「保存された音量」を読めない、という壊れ方が 再生するたびに変わる運ゲー になります
- 重複が生まれる: シーンに置いたマネージャーは、そのシーンへ戻るたびに新しく生成されます。
DontDestroyOnLoad済みの先代と合わせて2個——BGMが二重に鳴るあの現象です(DontDestroyOnLoadの記事 で見た定番事故です)
3つに共通するのは、 「いつ・どこで・どの順で作るか」を誰も決めていない ことです。だから、決める場所を1つ作ります。
Bootstrapシーン:組み立てる場所を1つに決める
Bootstrapシーン は、ゲームの最初に1回だけ読み込まれる、組み立て専用の小さなシーンです。中身はマネージャーを載せたルートオブジェクトが1つだけ。プレイヤーには一瞬も見えないまま、組み立てが終わったらタイトルへ進みます。

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に依存している」という関係が、シグネチャとしてコードに残ります。FindObjectOfType や Instance で暗黙に探すのをやめると、依存の全体像はBootstrapper1ファイルを読むだけで把握できるようになります。
どのシーンから再生しても動く保険
Bootstrapを作った直後に、開発体験の問題が1つ生まれます。 Gameシーンを開いて再生ボタンを押すと、Bootstrapを通っていないのでマネージャーが誰も居ない ——毎回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層に整理されます。

- 常駐層: 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でもパズルでもランゲームでも、シーンをまたぐゲームなら同じ手順です。

手順は4つだけです。
- Bootstrapシーンを新規作成: 空のシーンに
BootstrapRootオブジェクトを置き、子にSaveManager・AudioManager・GameManagerを並べ、親にBootstrapperを付けてInspectorでつなぐ - 各シーンからマネージャーを撤去: Title・Game・Resultに置いてあったマネージャーを全部削除する(ここが一番気持ちいい瞬間です)
- Awakeの依存処理をInitialize()へ移す: 「他のマネージャーを探して呼ぶ」行を各自のAwakeから抜き、
Initialize()に移して、Bootstrapperに順番どおり並べる - 保険を仕込む:
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行を挿しますか?