本記事は「Unity GameManager 役割」をテーマに、実装パターンを実験→比較→再現手順の順で整理した検証ログです。
狙いは2つだけです。
- GameManagerを“何でも屋”にしない境界線を、実装で見える化する
- チーム/個人どちらでも再現できる設計手順を、最小コードで提示する
キーワードへの配慮(Unity GameManager / unity ゲームマネージャー シングルトン / 設計 / 使い方 / 取得)は本文の検証に自然に含めています。
類似記事の知見は抽象化のみで引用・言い換えはしていません。
前提:今回の検証範囲とゴール
対象は「1シーン以上、スコア加算とライフ、ステート遷移(Start/Play/Pause/GameOver)を持つ軽量2D」。
GameManagerの候補タスクを列挙し、責務をどこまで持たせるかを3パターンで検証します。
- ステート管理(タイトル→プレイ→ポーズ→リザルト)
- スコア・ライフの保持とリセット
- UI更新のトリガー(文言や数値の差し替え)
- 敵/障害物のスポーンON/OFF
- BGM/SEの切り替え
ゴールは「役割の切り方の判断材料」を作ること。
ベストを断定せず、再現手順を示します。
比較対象:3つの実装パターンと期待・懸念
検証したのは次の3案です。
各案は実装を最小に寄せ、差分のみを強調します。
- <strong>A:一本化(“なんでも屋”GameManager)</strong> 期待:着手が速い/参照が一本で迷わない 懸念:肥大化・結合度↑・テストしづらい/変更の波及が大きい
- <strong>B:状態だけ(狭義のGameManager)+各役割に委譲</strong> 期待:依存の方向が明確/変更の波及が小さい 懸念:小粒スクリプトが増える/配線の手間が増える
- <strong>C:イベント駆動(GameManagerはハブ)+ScriptableObjectでデータ分離</strong> 期待:UIや音の入替が疎結合/テストが容易 懸念:学習コスト・イベントの流れが見えづらい(慣れが必要)
再現手順:各パターンを最小コードで
まず共通の列挙型とイベント雛形を置きます(BとCで使います)。
<pre><code>public enum GameState { Title, Playing, Paused, GameOver }</code></pre>
【A:一本化】——最短で動くが、役割が集中する版 <pre><code class="language-csharp">// A: なんでも屋 GameManager(MonoBehaviour) using UnityEngine; using UnityEngine.Events; public class GameManager : MonoBehaviour { public static GameManager Instance { get; private set; } [Header("Refs")] [SerializeField] PlayerController player; [SerializeField] Spawner spawner; [SerializeField] UIHUD hud; [SerializeField] AudioSource bgm; [Header("Game Data")] [SerializeField] int life = 3; [SerializeField] int score = 0; public GameState State { get; private set; } = GameState.Title; void Awake() { if (Instance != null && Instance != this) { Destroy(gameObject); return; } Instance = this; DontDestroyOnLoad(gameObject); } void Start() => GoTitle(); public void StartGame() { score = 0; life = 3; player.ResetPlayer(); spawner.enabled = true; bgm.Play(); State = GameState.Playing; hud.SetMode("PLAY"); hud.UpdateScore(score); hud.UpdateLife(life); } public void Pause(bool on) { State = on ? GameState.Paused : GameState.Playing; Time.timeScale = on ? 0f : 1f; hud.SetMode(on ? "PAUSE" : "PLAY"); } public void AddScore(int value) { score += value; hud.UpdateScore(score); // 直接UI書き換え } public void LoseLife() { life--; hud.UpdateLife(life); // 直接UI書き換え if (life <= 0) GameOver(); } void GameOver() { spawner.enabled = false; bgm.Stop(); State = GameState.GameOver; hud.SetMode("RESULT"); hud.SetResult(score); } public void GoTitle() { Time.timeScale = 1f; spawner.enabled = false; State = GameState.Title; hud.SetMode("TITLE"); } }</code></pre> 所見:
- 「Unity GameManager 取得」は GameManager.Instance でどこからでも触れる
- 小規模なら十分。<br>ただしUI/BGM/Spawnerの具体呼び出しが増え、肥大化の兆候が早い
【B:状態だけ】——GameManagerは“状態と遷移”に絞る版 <pre><code class="language-csharp">// B: 狭義GameManager(状態+通知) // 依存は「上位→下位」の一方向。
下位(UI/BGM/Spawn)は購読のみ。
using UnityEngine; using UnityEngine.Events; public class GameRoot : MonoBehaviour { public static GameRoot Instance { get; private set; } public GameState State { get; private set; } = GameState.Title; // シンプルなイベント(Cでより汎用化) public UnityEvent<GameState> OnStateChanged = new UnityEvent<GameState>(); public UnityEvent<int> OnScoreChanged = new UnityEvent<int>(); public UnityEvent<int> OnLifeChanged = new UnityEvent<int>(); [SerializeField] int life = 3; [SerializeField] int score = 0; void Awake() { if (Instance != null && Instance != this) { Destroy(gameObject); return; } Instance = this; DontDestroyOnLoad(gameObject); } void Start() => SetState(GameState.Title); public void StartGame() { score = 0; life = 3; OnScoreChanged.Invoke(score); OnLifeChanged.Invoke(life); SetState(GameState.Playing); } public void Pause(bool on) => SetState(on ? GameState.Paused : GameState.Playing); public void AddScore(int v) { score += v; OnScoreChanged.Invoke(score); } public void LoseLife() { life--; OnLifeChanged.Invoke(life); if (life <= 0) SetState(GameState.GameOver); } void SetState(GameState next) { // グローバル副作用はここだけ(TimeScale等) if (next == GameState.Paused) Time.timeScale = 0f; else Time.timeScale = 1f; State = next; OnStateChanged.Invoke(State); } }</code></pre> UI・サウンド・スポーナーは“購読側”に分離します。
<pre><code class="language-csharp">// UI側(購読者の一例) using UnityEngine; using UnityEngine.UI; public class UIHUD : MonoBehaviour { [SerializeField] Text modeText, scoreText, lifeText; void OnEnable() { GameRoot.Instance.OnStateChanged.AddListener(OnMode); GameRoot.Instance.OnScoreChanged.AddListener(s => scoreText.text = $"Score:{s}"); GameRoot.Instance.OnLifeChanged.AddListener(l => lifeText.text = $"Life:{l}"); } void OnDisable() { if (GameRoot.Instance == null) return; GameRoot.Instance.OnStateChanged.RemoveListener(OnMode); GameRoot.Instance.OnScoreChanged.RemoveAllListeners(); GameRoot.Instance.OnLifeChanged.RemoveAllListeners(); } void OnMode(GameState st) => modeText.text = st.ToString().ToUpper(); }</code></pre> 所見:
- GameManager(ここではGameRoot)は“歯車”ではなく“司令塔”。<br>UI直叩きが消え、依存方向が明確
- 小さなクラスが増えるが、改修点の見通しは良好。<br>ユニットテストもしやすい
【C:イベント駆動+ScriptableObject】——ハブ化とデータ分離 <pre><code class="language-csharp">// C: ScriptableObjectベースのGameConfig(データを分離) using UnityEngine; [CreateAssetMenu(menuName="Config/GameConfig")] public class GameConfig : ScriptableObject { public int initialLife = 3; public int scorePerCoin = 10; }</code></pre> <pre><code class="language-csharp">// C: ハブ的GameManager(状態+C#イベント) using UnityEngine; using System; public class GameHub : MonoBehaviour { public static GameHub I { get; private set; } [SerializeField] GameConfig config; public GameState State { get; private set; } = GameState.Title; public int Life { get; private set; } public int Score { get; private set; } public event Action<GameState> StateChanged; public event Action<int> ScoreChanged; public event Action<int> LifeChanged; void Awake() { if (I != null && I != this) { Destroy(gameObject); return; } I = this; DontDestroyOnLoad(gameObject); } void Start() => GoTitle(); public void StartGame() { Life = config.initialLife; Score = 0; Emit(); SetState(GameState.Playing); } public void AddScoreCoin() { Score += config.scorePerCoin; ScoreChanged?.Invoke(Score); } public void HitDamage(int dmg = 1) { Life -= dmg; LifeChanged?.Invoke(Life); if (Life <= 0) SetState(GameState.GameOver); } public void Pause(bool on) => SetState(on ? GameState.Paused : GameState.Playing); public void GoTitle() => SetState(GameState.Title); void SetState(GameState s) { Time.timeScale = (s == GameState.Paused) ? 0 : 1; State = s; StateChanged?.Invoke(State); } void Emit() { ScoreChanged?.Invoke(Score); LifeChanged?.Invoke(Life); } }</code></pre> 所見:
- パラメータ差し替え(難易度/チューニング)がプレハブ非依存で安全になる
- アセット単位でレビュー/共有がしやすい。<br>大きめのプロジェクトほど効きやすい
観測結果:規模別・変更別の“ハマりどころ”
実験は、次の変更ケースを順に適用して差を観察しました。
- UIをUGUI→TextMeshProに変更
- スコア加算の条件を「一定時間」→「イベント受信」に変更
- BGMをシーン常駐からアセット切替式に変更
結果の要点:
- A(一本化): ・UI変更のたびにGameManagerを編集。<br>ビルド前の影響範囲レビューが毎回必要 ・BGM差替も同様に直結。<br>改修速度は速いが、コンフリクト頻度が上がる
- B(状態だけ): ・UI入替は購読側だけの差替で済む。<br>GameManagerは無改修 ・「取得」はGameRoot.Instanceのみで統一され、参照迷子が起きにくい
- C(イベント+SO): ・チューニングはSOのみで完了。<br>環境差(Dev/Prod)で値を切り替えやすい ・一方でイベントの“流れ”はドキュメント必須(命名規約と購読箇所の一覧化が鍵)
“歯車にならない”周辺のメモ(アイコン/命名/安全策)
関連KWの「unity gamemanager 歯車にならない」は、主に「スクリプトのアイコン(ギアに見える)」「コンポーネント化できない」系の混同が原因で検索されることが多い印象です。
今回の検証で再現した範囲では、以下で回避・切り分けできました。
- <strong>クラス名=ファイル名=MonoBehaviour継承を厳守</strong> クラス名とファイル名不一致はAddComponent不可の定番。<br>まずここを確認
- <strong>Editor拡張のあたりを一時無効化</strong> 独自のカスタムエディタ/アイコン上書きがあると見た目が変わる。<br>Editorフォルダを一時リネームして検証
- <strong>ScriptableObjectと混同しない</strong> SOはインスペクタで“歯車っぽい”見た目になりやすい。<br>用途が違うので挙動の違いは仕様
いずれも設計の話とは独立なので、<h2>の範囲決めと混ぜないのが無難です。
[H2]判断基準:どのパターンをいつ選ぶ?
結論は“規模と変更頻度”で決めるのが最短です。
断定はしませんが、次の指針は再現性がありました。
- プロトタイプ/1人開発/画面1~2枚: → <strong>A:一本化</strong>。<br>チュートリアルや社内サンプルでも最速。<br> ただし、UI/BGM/Spawnの具体呼び出しは<strong>1ファイル内でのメソッド分割とセクションコメント</strong>で肥大化を抑える
- 小~中規模/2~5人/UI改修が週1以上: → <strong>B:状態だけ</strong>。<br>GameManagerは遷移とグローバル副作用のみ。<br> UIやサウンドは“購読者”に寄せる。<br>参照の向きを「上→下」に固定
- 中~大規模/複数プラットフォーム/運用長期: → <strong>C:イベント+SO</strong>。<br>データ分離でビルド差分を小さく、ABテストや難易度違いに耐える。<br> イベント命名規約(OnXxxChanged/RequestXxx)と、購読一覧のドキュメントが前提
再現用チェックリスト(差分で潰せるよう最短化)
導入時に“迷い”を減らすため、各案のチェック項目を実装順に並べます。
これ通りに進めると、この記事の挙動をほぼ再現できます。
- <strong>共通</strong> GameState列挙を用意 空シーンに「常駐オブジェクト」を作成(DontDestroyOnLoad)
- <strong>A:一本化</strong> GameManagerを追加し、Player/Spawner/UIHUD/AudioSourceをSerializeFieldで束ねる StartGame/Pause/AddScore/LoseLife/GameOver/GoTitleを直呼びで実装 UI更新はhud.UpdateXxx()で直接反映(最小ルール:UI更新は1か所に集約)
- <strong>B:状態だけ</strong> GameRootを作り、OnStateChanged/OnScoreChanged/OnLifeChangedをUnityEventで公開 UIHUDなど購読側でOnEnable/OnDisableにAdd/RemoveListener Spawner/BGMも購読者に分け、状態遷移のみに反応してON/OFF
- <strong>C:イベント+SO</strong> GameConfig(ScriptableObject)を作り、初期ライフ/加点などの値を保持 GameHubにAction<>イベントを持たせ、SOの値で初期化 すべての購読者はGameHub.Iのイベントだけを見る(他参照禁止)
まとめ:GameManagerは「交通整理」であって“施工”ではない
検証の結論はシンプルでした。
GameManager(あるいはGameRoot/Hub)の役割は「ゲーム全体の状態を決め、必要最小限の副作用(TimeScaleなど)を担い、あとは“通知”する」に留めるのが、規模が大きくなるほど効きます。
一方で、短期プロトタイプや学習用途では“一本化”の速度が正義です。
迷ったら次の2問に答えるだけで方針は決まります。
- UIや音の入替が週に何回あるか? → あるならAは避ける、B/Cへ
- パラメータの調整が頻繁か? → 頻繁ならCでSO化。<br>そうでなければBで十分
最後に、どの案を選んでも「取得の統一(Instanceひとつ)」「依存は上→下」「UI更新は購読者側」の3点を守るだけで、将来の“歯車化(肥大化の隠喩)”はだいぶ防げます。
もし運用に入ってパラメータの手触りを上げたいと感じたら、ScriptableObject化から小さく始めるのが安全でした。
——運用中の小ネタ:難易度ごとのGameConfigを複製して、ビルド前に差し替えるだけでABテストの下地になります。
アセット差分なのでGitのレビューも軽いです。
(余談)開発備品や参考書を探すとき、私はついエディタ拡張やテスト周りの道具に目がいきがちなのですが、入門〜中級の実装で詰まるのは「責務の線引き」だったりします。
そういう時はハードより知識投資が効くので、設計まわりのガイドや実例をまとめて扱えるストアがあると助かります。
文脈が合えばUnity入門の森ショップの棚を眺めて、必要分だけ拾うのも悪くない選択肢でした。