Unityでゲーム開発を進める中で、ゲーム全体で一つだけ存在すれば良いクラス(例:ゲームマネージャー、サウンドマネージャー)に出会うことがよくあります。そんなときに使われる代表的なデザインパターンが「シングルトン」です。
シングルトンを実装すると、そのクラスが二つ以上存在できないことが保証され、メソッドへのアクセスが超簡単になります。
今回は、シングルトンの基本からUnityでの実装方法、さらにシンプル・汎用的なコード例、そして「アンチシングルトン」という逆の考え方まで紹介していきます。
デザインパターンとは?
これはUnityの機能の話ではありません。「デザインパターン」とは、ソフトウェア開発における再利用可能な設計のテンプレートのことです。頻出する設計上の問題に対して、洗練された解決策をパターン化したもので、「GoFデザインパターン」と呼ばれる23種類が有名です。(KuroMikanはその領域まで達していませんが…)
その中でも、インスタンスが1つだけ必要な状況で使われるのが「シングルトンパターン(Singleton Pattern)」です。
Unityにおけるシンプルなシングルトンの書き方
Unityでは以下のように簡単にシングルトンを実装できます。
GameManager.cs
public class GameManager : MonoBehaviour
{
public static GameManager Instance { get; private set; }
void Awake()
{
if (Instance != null && Instance != this)
{
Destroy(gameObject);
return;
}
Instance = this;
DontDestroyOnLoad(gameObject);
}
}
このGameManager.csをシーン上に配置したオブジェクトにアタッチさせます。
特徴
Instance
にアクセスすればどのスクリプトからでもGameManagerを参照可能。- GameManager.Instance.GetScore(); ←こんな感じで使う
DontDestroyOnLoad()
により、シーンを跨いでも保持されます。- ただし、このままでは他の型には使い回せません(毎回コピペしては必要な部分を書き換える作業が発生する)
汎用的に使えるシングルトンのテンプレート
複数のクラスでシングルトンを使いたい場合は、ジェネリッククラスとして抽象化するのが便利です。
Singleton.cs
public abstract class Singleton<T> : MonoBehaviour where T : MonoBehaviour
{
private static T _instance;
private static bool _shuttingDown = false;
public static T Instance
{
get
{
if (_shuttingDown) return null;
if (_instance == null)
{
_instance = (T)FindObjectOfType(typeof(T));
if (_instance == null)
{
var singletonObj = new GameObject(typeof(T).Name);
_instance = singletonObj.AddComponent<T>();
DontDestroyOnLoad(singletonObj);
}
}
return _instance;
}
}
protected virtual void OnApplicationQuit()
{
_shuttingDown = true;
}
protected virtual void OnDestroy()
{
_shuttingDown = true;
}
}
利用方法:
SoundManager.cs
public class SoundManager : Singleton<SoundManager>
{
public void PlaySE(string name)
{
// サウンド再生処理
}
}
メリット
- 簡単に複数のシングルトンを使い回せる。
- 自動生成対応(Find→なければ生成)。
OnDestroy()
とOnApplicationQuit()
でシャットダウン時の安全対策も含む。
アンチシングルトンという考え方
シングルトンは便利な一方で、過剰に使うとテストしづらく、密結合を招きやすいというデメリットもあります。こうした問題点を指摘し、シングルトンの使用を控える設計方針を「アンチシングルトン」と呼ぶこともあります。
アンチシングルトン派の主張
- 依存性注入(DI)の方がテスト可能性や柔軟性が高い。
- グローバルな状態が多くなるとバグの温床になる。
- 複数シーンでの競合や生成タイミングの問題が発生しやすい。
Unityでの代替手段
ScriptableObject
を使ったグローバルデータの管理。- ZenjectなどのDIフレームワークの導入。
- もちろんKuroMikanはその領域まで達していない!
おわりに
FindやGetComponentなどを気にせず、どこからでもすぐにアクセスできる「シングルトン」は便利なデザインパターンですが、「必要な場面にだけ使う」ことが大切です。
GameManagerやSoundManagerなど、1つしか存在しない前提であるものに対してのみ慎重に適用しましょうね。約束だよ。
参考
旧ブログでも同じテーマでエントリーを公開しています。(こちらではタグによる判別で実装してました)