スコアはプレイヤーが持ち、ゲームオーバー判定は敵が行い、リスタート処理はUIボタンの中——プロトタイプを超えて本格的なゲームを作り始めると、「ゲーム全体の状態を誰が管理するのか」が曖昧なまま、ロジックがあちこちに散らばっていきます。こうなると、仕様変更のたびに複数のスクリプトを行脚する羽目になります。
この問題を解決する最も古典的で一般的な設計パターンが Game Manager(ゲームマネージャー) です。ゲーム全体の状態とロジックを一元的に管理する「司令塔」を1人立てる——この記事では、その役割とenum+Singletonによる実装、そして肥大化させないための注意点を解説します。
この記事でわかること
- ロジックが散らばると何が起きるか、Game Managerが引き受ける4つの役割
enumによるゲーム状態(Title / Playing / Paused / GameOver)の管理- Singleton+
DontDestroyOnLoadによる実装(コピペで使える)- 「神クラス化」を防ぐ責務分割の考え方
Game Managerの主な役割
Game Managerに持たせる機能はゲームの規模や種類によって様々ですが、一般的には以下のような役割を担います。

-
ゲーム状態の管理 (Game State Management):
enumを使って定義したゲームの状態(例:Title,Playing,Paused,GameOver)を保持し、状態の遷移を制御します。現在の状態に応じて、プ レイヤーの入力を無効にしたり、UIを表示/非表示にしたりします。 -
ゲームルールの管理: スコアの計算、残り時間のカウントダウン、勝利・敗北条件の判定など、ゲームの核となるルールを管理します。
-
グローバルなデータ管理: プレイヤーのスコア、ライフ、経験値など、複数のオブジェクトやシーンからアクセスされる必要のあるデータを保持します。
-
他オブジェクトへの参照の提供: Player、UIManager、AudioManagerなど、他の重要なマネージャークラスへの参照を保持し、他のオブジェクトがこれらのマネージャーに簡単にアクセスできる窓口を提供します。
ゲーム状態はenumで管理する
Game Managerの心臓部が「今、ゲームはどの状態か」の管理です。タイトル画面・プレイ中・ポーズ・ゲームオーバー——ゲームは常にこのうちのどれか1つにいて、決まったルートで移動します。

この「どれか1つ」を表現するのにぴったりなのがenum(列挙型)です。文字列("Playing"など)と違ってタイプミスがコンパイルエラーで見つかり、switch文との相性も抜群です。状態の種類が増え、状態ごとの処理が複雑化してきたら、Stateパターンの記事 で解説しているクラス分割方式へのステップアップを検討します。
Singletonパターンによる実装
Game Managerは、ゲーム内に常に一つだけ存在することが保証される必要があります。この「唯一のインスタンス」を保証し、どこからでも簡単にアクセスできるようにするためのデザインパターンがSingleton (シングルトン) です(仕組みの詳細は Singletonパターンの記事 を参照)。
以下は、最も基本的なSingletonパターンを使ったGame Managerの実装例です。
using UnityEngine;
using UnityEngine.SceneManagement;
public class GameManager : MonoBehaviour
{
// Singletonインスタンスを保持する静的フィールド
public static GameManager Instance { get; private set; }
// ゲームの状態を定義するenum
public enum GameState { Title, Playing, Paused, GameOver }
public GameState CurrentState { get; private set; }
// ゲームのグローバルデータ
public int Score { get; private set; }
private void Awake()
{
// Singletonパターンの実装
// インスタンスがまだ存在しない場合、このインスタンスを唯一のものとして設定
if (Instance == null)
{
Instance = this;
// シーンをまたいでこのオブジェクトを維持する
DontDestroyOnLoad(gameObject);
}
// インスタンスが既に存在する場合、この新しいインスタンスは破棄する
else
{
Destroy(gameObject);
return;
}
// 初期状態を設定
CurrentState = GameState.Title;
}
// スコアを加算するメソッド(どこからでも呼べる)
public void AddScore(int amount)
{
if (CurrentState != GameState.Playing) return;
Score += amount;
// UIManager.Instance.UpdateScoreUI(Score); // UIの更新を依頼
}
// ゲームの状態を変更するメソッド
public void ChangeState(GameState newState)
{
if (CurrentState == newState) return;
CurrentState = newState;
// 状態に応じた処理を実行
switch (newState)
{
case GameState.Title:
// タイトル画面の準備
break;
case GameState.Playing:
// ゲームプレイ開始の準備
Time.timeScale = 1f; // 時間の進行を再開
break;
case GameState.Paused:
// ポーズ処理
Time.timeScale = 0f; // 時間の進行を停止
break;
case GameState.GameOver:
// ゲームオーバー処理
break;
}
}
// 他のスクリプトからの呼び出し例
// GameManager.Instance.AddScore(100);
// GameManager.Instance.ChangeState(GameManager.GameState.GameOver);
}
補足:
AddScoreの先頭にあるif (CurrentState != GameState.Playing) return;に注目してください。「プレイ中でなければスコアは増えない」というルールが1箇所で守られています。状態管理を一元化する具体的なうまみは、こういう小さな守りが全部Game Managerに集まることです。
使い方
- 空のGameObjectを作成し、「GameManager」と名付けます。
- 上記の
GameManager.csスクリプトをアタッチします。 - このGameManagerオブジェクトをPrefab化し、ゲームの開始シーン(スプラッシュ画面やタイトル画面)に配置しておけば、ゲーム中ずっと存在し続ける唯一のインスタンスとして機能します。
DontDestroyOnLoad(gameObject);が、シーンが切り替わってもGameManagerオブジェクトが破棄されないようにするための重要な命令です(詳しくは DontDestroyOnLoadの記事 へ)。
Game Managerの注意点
Singletonパターンは非常に便利ですが、多用すると問題を引き起こす可能性もあります。特に危険なのが、何でもGameManagerに詰め込んだ結果の「神クラス」化です。

- 密結合: どのスクリプトからでも
GameManager.Instanceにアクセスできるため、様々なオブジェクトがGameManagerに強く依存してしまい、コードの再利用性やテストのしやすさが低下することがあります。 - 責務の肥大化: ゲームに関するあらゆるロジックをGameManagerに詰め込みすぎると、すぐに巨大で管理不能なクラスになってしまいます。スコア管理、UI管理、オーディオ管理など、機能ごとに
ScoreManager,UIManager,AudioManagerといった別のマネージャークラスに分割し、GameManagerはそれらを統括する役割に徹するのが良い設計です。
補足: 「GameManagerに追加しようとしているこの機能、本当にゲーム全体の関心事か?」と自問するのが有効です。特定のシーンやキャラクターだけの話なら、そのシーン・キャラクター側に置くのが正解です。
実践:60秒コインゲームを開始からリトライまで完成させる
理屈がそろったので、「1ゲーム」を丸ごと完成させます。ルールは単純—— 60秒でコインを集め、時間切れで結果画面、リトライでもう1回。この小さなゲームには、開始・残り時間・得点・終了・リトライという進行管理の要素が 全部入っています。そしてここで大事なのは、 GameManagerに「進行の判断」だけをさせる ことです。コインの生成もUIの描画も音も、GameManagerは抱え込みません。
// ファイル名: CoinGameManager.cs — 進行の判断「だけ」を持つ司令塔
using System;
using UnityEngine;
public class CoinGameManager : MonoBehaviour
{
public static CoinGameManager Instance { get; private set; }
public enum GameState { Title, Playing, Result }
public GameState CurrentState { get; private set; } = GameState.Title;
public float TimeLeft { get; private set; }
public int Score { get; private set; }
public const float PlayTime = 60f; // 動作検証中は5fに縮めてOK
// 「状態が変わった」という通知だけを出す。UIや音は購読側の仕事
public event Action<GameState> OnStateChanged;
void Awake()
{
if (Instance != null && Instance != this) { Destroy(gameObject); return; }
Instance = this;
}
void Update()
{
if (CurrentState != GameState.Playing) return; // Playing以外は時間が進まない
TimeLeft -= Time.deltaTime;
if (TimeLeft <= 0f)
{
TimeLeft = 0f;
ChangeState(GameState.Result); // 時間切れ→結果へ
}
}
// タイトルの「スタート」ボタンと、Resultの「リトライ」ボタンが同じこれを呼ぶ
public void StartGame()
{
Score = 0; // リトライのたびに必ず初期化
TimeLeft = PlayTime;
ChangeState(GameState.Playing);
}
public void AddScore(int amount)
{
if (CurrentState != GameState.Playing) return; // Playing以外では得点は動かない
Score += amount;
}
void ChangeState(GameState next)
{
if (CurrentState == next) return; // 同じ状態への二重遷移を弾く
CurrentState = next;
OnStateChanged?.Invoke(next);
}
}
コイン側は「拾われたら申告する」だけの数行です。
// ファイル名: Coin.cs
using UnityEngine;
public class Coin : MonoBehaviour
{
void OnTriggerEnter(Collider other)
{
if (!other.CompareTag("Player")) return;
CoinGameManager.Instance.AddScore(10);
Destroy(gameObject);
}
}
そして結果画面のUIは、OnStateChangedを購読して「Resultのときだけ自分を表示する」だけ。GameManagerはUIの存在すら知りません。
// ResultUI.cs の要点
void OnEnable() { CoinGameManager.Instance.OnStateChanged += HandleState; }
void OnDisable() { CoinGameManager.Instance.OnStateChanged -= HandleState; }
void HandleState(CoinGameManager.GameState state)
{
panel.SetActive(state == CoinGameManager.GameState.Result);
}
役割分担を整理すると——進行の判断(開始・時間・得点・終了)はGameManager、コインの生成はSpawner、表示はUI、音はオーディオ側。 GameManagerのコードにUIや音の単語が1つも出てこない ことを確認してください。これが「統括に徹する」の具体的な形です。

わざと壊して、直す
進行管理の定番バグを、このコインゲームで再現してみましょう。
- Playing以外でコインが拾える:
AddScoreのガードを外すと、Result画面の裏で転がってきたコインに触れて 確定したはずの得点が増えます。「プレイ中しか得点は動かない」というルールは、呼び出し側全部ではなく 入口の1箇所 で守るのが鉄則です - Resultへ2回遷移する:
ChangeStateのif (CurrentState == next) return;を外すと、時間切れのChangeStateが毎フレーム呼ばれ、OnStateChangedが連打されて結果画面のSEやアニメが多重再生されます。 遷移は「変わったときだけ」通知 ——このガード1行が守っています - Retry後に古いものが残る:
StartGameのScore = 0を消してリトライすると、2回目が前回の得点から始まります。タイマー・イベント購読も同じで、「 開始時に必ず初期化・購読は破棄前に解除 」がリトライ可能なゲームの土台です - PauseとGameOverの競合: ポーズ中に時間切れが来たら?——この設計では
Update冒頭のガードで Playing以外はタイマーが進まない ため、競合そのものが起きません。状態を増やすときは「その状態でタイマー・入力・物理はどう動くべきか」を1つずつ決めてガードに落とし込みます
仕上げは、リトライボタンの連打です。何度押しても得点0・タイマー満タンの同じスタートラインに立てるなら、進行の初期化はStartGameの1箇所にきちんと集まっています。もし2周目に前回の得点が顔を出したら、リセット漏れ——直す場所も、もう分かるはずです。
おまけ:先に知っておくと良いこと
Game Managerの骨格ができた ら、次はこのあたりが視野に入ってきます。
- 状態処理が複雑になったらStateパターン:
switchのcaseが膨らんできたら、状態をクラスに分割する Stateパターンの記事 の出番です。enum版からの移行は自然にできます。 - 「状態が変わったこと」の通知はイベントで:
ChangeStateの中でUIやサウンドを直接呼ぶと密結合が進みます。OnGameStateChangedのような イベント や イベントチャネル で通知すると、GameManagerがスリムに保てます。 Time.timeScale = 0の副作用: ポーズの定番ですが、UpdateのTime.deltaTimeが0になる一方、Time.unscaledDeltaTimeは動き続けます。「ポーズ中もメニューのアニメは動かしたい」場合はunscaled系を使う、と覚えておくと詰まりません。
まとめ
Game Managerは、複雑なゲームの構造を整理し、見通しを良くするための強力な設計パターンです。
- ゲーム全体の状態、ルール、グローバルなデータを一元管理する「司令塔」の役割を担う。
Singletonパターンを使い、ゲーム内に常に一つだけ存在するインスタンスとして実装する。DontDestroyOnLoad()を使い、シーンをまたいでオブジェクトを永続化させる。enumでゲームの状態を定義し、switch文で状態遷移時の処理を管理する。複雑化したらStateパターンへ。- 責務が大きくなりすぎないよう、機能ごとのマネージャーに分割し、GameManagerは統括に徹する。
まずはこの基本的なGame Managerの形を導入するだけでも、ゲームのコードベースは格段に整理され、機能追加やデバッグが容易になるはずです。