Unityの「GameManager」を“役割”で細くする:最小コードで回したメモ
Unityで小さめのゲームを作ってると、「GameManager.cs」に何でも入れたくなる。
私もそうで、UIもスコアも生成もぜんぶ突っ込んで、あとから触るのが怖くなった。
この記事は、わたしが「Unity GameManager 役割」を削っていったときのメモ。
断定はしないけど、同じ悩みの人の足場にはなるかもしれない。
最初の具体例:全部入りからの“ちぎり出し”
最初の失敗版は、Updateで状態を見て、UIのTextを書き換えて、敵のスポーンまでしていた。
正直、動くけど伸びない。
そこで「GameManagerは“入口”だけにする」方向へ寄せた。
まずは、タイトル→プレイ中→ポーズ→ゲームオーバーの遷移だけ面倒を見る“細い根本”を置く。
// GameRoot.cs(最小)
// 状態遷移+イベント通知だけ。UIやスコアは触らない。
using System;
using UnityEngine;
public enum GameState { Title, Playing, Paused, GameOver }
public sealed class GameRoot : MonoBehaviour
{
public static GameRoot I { get; private set; }
public GameState State { get; private set; } = GameState.Title;
public event Action OnGameStart;
public event Action OnGameOver;
public event Action<bool> OnPause;
void Awake()
{
if (I != null && I != this) { Destroy(gameObject); return; }
I = this;
DontDestroyOnLoad(gameObject); // シーンまたいで生存
}
public void StartGame()
{
if (State == GameState.Playing) return;
Set(GameState.Playing);
OnGameStart?.Invoke();
}
public void GameOver()
{
if (State == GameState.GameOver) return;
Set(GameState.GameOver);
OnGameOver?.Invoke();
}
public void TogglePause()
{
bool toPause = State != GameState.Paused;
Set(toPause ? GameState.Paused : GameState.Playing);
Time.timeScale = toPause ? 0f : 1f;
OnPause?.Invoke(toPause);
}
void Set(GameState s) { State = s; }
}
このくらい“薄い入口”にしたら、他の責務(スコアやUI)が勝手に太れなくなる。
結果、ユニット単位で触りやすくなった。
気づき:GameManagerの“最小役割”は4つで足りた
GameManager(またはGameRoot)に残したのは次の4つだけ。
やってることは少ないけど、全体の見通しは一番効いてきた。
- 状態遷移:Title/Playing/Paused/GameOver を決める。
- イベント配信:OnGameStart / OnGameOver / OnPause を投げる。
- 生存管理:DontDestroyOnLoad で“根本”を保つ。
- ハブ:他のサービス(Score、Audio など)への入り口を置く(でも直接は触らない)。
逆に外したのは、UI更新、スコア加算、敵スポーン、入力解釈。
この線引きで「unity gamemanager 歯車にならない」感覚が出てきた。
UIは触らない:イベントを“聞く側”にする最小コード
UIはGameManagerから直接いじらず、イベントを購読するだけにした。
Zennっぽくミニマムで。
// HUDPresenter.cs(最小)
// GameRootのイベントだけを購読。テキストの見た目はUI側で完結。
using UnityEngine;
using UnityEngine.UI;
public class HUDPresenter : MonoBehaviour
{
[SerializeField] Text scoreText; // TextMeshProなら型を置き換え
[SerializeField] Text stateText;
void OnEnable()
{
var r = GameRoot.I; if (r == null) return;
r.OnGameStart += HandleStart;
r.OnGameOver += HandleOver;
r.OnPause += HandlePause;
UpdateState(r.State);
}
void OnDisable()
{
var r = GameRoot.I; if (r == null) return;
r.OnGameStart -= HandleStart;
r.OnGameOver -= HandleOver;
r.OnPause -= HandlePause;
}
void HandleStart() => UpdateState(GameState.Playing);
void HandleOver() => UpdateState(GameState.GameOver);
void HandlePause(bool paused) => UpdateState(paused ? GameState.Paused : GameState.Playing);
void UpdateState(GameState s) { stateText.text = $"State: {s}"; }
public void RenderScore(int v) { scoreText.text = $"Score: {v}"; }
}
「GameManagerがUIの参照を握って更新」だとすぐ重くなる。
UIは“聞く側”、Managerは“言うだけ”に寄せたら、変更が局所化してだいぶ気が楽に。
スコアは別サービスに逃す(取得も軽く)
Scoreは「足し算・最大値・保存」などが増えがちなので、早めに分離した。
取得パターンは最初は素直にシーン参照→必要になったら差し替える感じ。
// ScoreService.cs(最小)
using UnityEngine;
public class ScoreService : MonoBehaviour
{
public int Current { get; private set; }
public int High { get; private set; }
public void ResetAll() { Current = 0; }
public void Add(int value)
{
Current = Mathf.Max(0, Current + value);
High = Mathf.Max(High, Current);
}
}
// どこかの敵やコインから:スコアを足すだけ
using UnityEngine;
public class Coin : MonoBehaviour
{
void OnTriggerEnter(Collider other)
{
var score = FindObjectOfType<ScoreService>(); // 最初はこれで十分
score?.Add(10);
var hud = FindObjectOfType<HUDPresenter>();
hud?.RenderScore(score?.Current ?? 0);
Destroy(gameObject);
}
}
“取得どうする問題(Unity GameManager 取得)”は、プロジェクト規模で変える前提にすると気が楽。
小さいうちは Find でも壊れない。
混んできたら静的参照や Service Locator に寄せる。
取得パターンのメモ:小→中の順で切り替える
いまのところ、こんな感じでスライドさせている。
- A. シーン内参照(SerializeField / FindObjectOfType):最小・一番読みやすい。Prefab検証が楽。
- B. シングルトン(static Instance):呼ぶのは楽。ただ依存の見えづらさがあるので“入口1か所”(GameRootとか)に限定したい。
- C. 薄いService Locator:辞書に登録して Get するだけ。差し替えやすい。
// ServiceHub.cs(必要になってからでOK)
using System;
using System.Collections.Generic;
using UnityEngine;
public class ServiceHub : MonoBehaviour
{
static readonly Dictionary<Type, object> map = new();
public static void Register<T>(T svc) where T : class => map[typeof(T)] = svc;
public static T Get<T>() where T : class => map.TryGetValue(typeof(T), out var o) ? (T)o : null;
void Awake()
{
Register(FindObjectOfType<ScoreService>());
// Register(FindObjectOfType<AudioService>()); など
}
}
// 使う側(例)
var score = ServiceHub.Get<ScoreService>();
score?.Add(100);
「unity ゲームマネージャー シングルトン」の沼にはまった時期もあったけど、最小から始めて必要になったら段階的に、がいちばん壊れなかった。
壊れポイントの観察ログ(対処メモ付き)
- Pauseで止まらない:UIアニメはUnscaledTimeで動く設定かもしれない。OnPauseで UI 側の Animator.updateMode を切り替えるか、アニメを手動に。
- シーンまたぎで参照切れ:DontDestroyOnLoad で根本だけ生かし、シーン依存の参照(UI、カメラ)は sceneLoaded で取り直すと安定。
- SerializeFieldが増殖:10本超えたら“役割を外に出す合図”。ScoreServiceやSpawnDirectorに逃がす。
- Updateが重い:状態監視で回し続けがち。イベント駆動に寄せるとフレーム依存が減る。
// Scene再ロードでUIを取り直す例
using UnityEngine;
using UnityEngine.SceneManagement;
public class UILinker : MonoBehaviour
{
HUDPresenter hud;
void OnEnable() => SceneManager.sceneLoaded += OnLoaded;
void OnDisable() => SceneManager.sceneLoaded -= OnLoaded;
void OnLoaded(Scene s, LoadSceneMode m)
{
hud = FindObjectOfType<HUDPresenter>();
// 必要なら GameRoot.I.OnPause など再購読
}
}
小さく回す手順(今日はここまで、明日ここから)
具体例→気づき→次に試す、の順で自分に言い聞かせる感じで。
- 具体例:GameRoot(状態とイベントだけ)+ScoreService(足し算だけ)+HUD(イベント購読だけ)を置く。
- 気づき:GameManagerは「入口」に徹したほうが、他の責務が勝手に太らない。UI直参照は“やめどき”サイン。
- 次に試せること:ServiceHubを薄く入れて取得経路を一本化、もしくは ScriptableObject で設定共有。テストしやすくなったら、シーンロードやBGMを別担当(SceneFlow/BgmController)に切る。
まとめ
「Unity GameManager 役割」を削っていったら、状態遷移・イベント配信・生存管理・ハブの4つに落ち着いた。
やらないこと(UI更新、スコア計算、スポーン、入力)は早めに外へ。
取得は小規模なら Find / SerializeField でOK、混んだら静的 or ServiceHubへ。
私はこの順で“歯車にならない”感じが出て、触り続けられるようになった。
もし同じ壁に当たっていたら、まずは「UI直参照をゼロにする」から試してみるのが楽かも。
学習素材を整理したいときは、道具の棚卸しついでにUnity入門の森ショップも眺めておくと、次の分割アイデアが湧きやすかった。
Discussion