概要
シンプルなプロトタイプを超えて、複数のシーンや複雑なルールを持つ本格的なゲームを作り始めると、「ゲーム全体の状態をどこで、どのように管理するか?」という設計上の問題に直面します。例えば、
- 現在のゲームの状態は「タイトル画面」なのか、「プレイ中」なのか、「ポーズ中」なのか、それとも「ゲームオーバー」なのか?
- プレイヤーのスコア、残機、所持金といった、シーンをまたいで維持されるべきデータはどこに保存するか?
- ゲームの開始、リスタート、次のレベルへの移行といった、ゲーム全体の流れを誰が制御するのか?
これらの「ゲーム全体に関わる情報とロジック」を、個々のプレイヤーや敵キャラクターのスクリプト内に分散させてしまうと、コードはすぐに見通しが悪く、メンテナンスが困難な「スパゲッティコード」になってしまいます。
この問題を解決するための最も古典的で一般的な設計パターンが、Game Manager (ゲームマネージャー) です。Game Managerは、ゲーム全体の状態とロジックを一元的に管理する、いわば「司令塔」のような役割を担うオブジェクトです。
Game Managerの主な役割
Game Managerに持たせる機能はゲームの規模や種類によって様々ですが、一般的には以下のような役割を担います。
-
ゲーム状態の管理 (Game State Management):
enumを使って定義したゲームの状態(例:Title,Playing,Paused,GameOver)を保持し、状態の遷移を制御します。現在の状態に応じて、プレイヤーの入力を無効にしたり、UIを表示/非表示にしたりします。 -
ゲームルールの管理: スコアの計算、残り時間のカウントダウン、勝利・敗北条件の判定など、ゲームの核となるルールを管理します。
-
グローバルなデータ管理: プレイヤーのスコア、ライフ、経験値など、複数のオブジェクトやシーン からアクセスされる必要のあるデータを保持します。
-
他オブジェクトへの参照の提供: Player、UIManager、AudioManagerなど、他の重要なマネージャークラスへの参照を保持し、他のオブジェクトがこれらのマネージャーに簡単にアクセスできる窓口を提供します。
Singletonパターンによる実装
Game Managerは、ゲーム内に常に一つだけ存在することが保証される必要があります。この「唯一のインスタンス」を保証し、どこからでも簡単にアクセスできるようにするためのデザインパターンが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);
}
使い方
- 空のGameObjectを作成し、「GameManager」と名付けます。
- 上記の
GameManager.csスクリプトをアタッチします。 - このGameManagerオブジェクトをPrefab化し、ゲームの開始シーン(スプラッシュ画面やタイトル画面)に配置しておけば、ゲーム中ずっと存在し続ける唯一のインスタンスとして機能します。
DontDestroyOnLoad(gameObject);が、シーンが切り替わってもGameManagerオブジェクトが破棄されないようにするための重要な命令です。
Game Managerの注意点
Singletonパターンは非常に便利ですが、多用すると問題を引き起こす可能性もあります。
- 密結合: どのスクリプトからでも
GameManager.Instanceにアクセスできるため、様々なオブジェクトがGameManagerに強く依存してしまい、コードの再利用性やテストのしやすさが低下することがあります。 - 責務の肥大化: ゲームに関するあらゆるロジックをGameManagerに詰め込みすぎると、すぐに巨大で管理不能なクラスになってしまいます。スコア管理、UI管理、オーディオ管理など、機能ごとに
ScoreManager,UIManager,AudioManagerといった別のマネージャークラスに分割し、GameManagerはそれらを統括する役割に徹するのが良い設計です。
まとめ
Game Managerは、複雑なゲームの構造を整理し、見通しを良くするための強力な設計パターンです。
- ゲーム全体の状態、ルール、グローバルなデータを一元管理する「司令塔」の役割を担う。
Singletonパターンを使い、ゲーム内に常に一つだけ存在するインスタンスとして実装する。DontDestroyOnLoad()を使い、シーンをまたいでオブジェクトを永続化させる。enumでゲームの状態を定義し、switch文で状態遷移時の処理を管理する。- 責務が大きくなりすぎないよう、他のマネージャークラスへの分割も検討する。
まずはこの基本的なGame Managerの形を導入するだけでも、ゲームのコードベースは格段に整理され、機能追加やデバッグが容易になるはずです。