概要
Unityでゲーム開発を進 めていると、「どこからでもアクセスできる、ただ一つの管理役」が必要になる場面に必ず遭遇します。例えば、ゲーム全体のスコアを管理するクラス、BGMや効果音の再生を制御するクラス、あるいはゲームの進行状態(ポーズ中、ゲームオーバーなど)を保持するクラスなどです。
これらの管理クラスを、毎回GameObject.Find()で探したり、インスペクターから手動で参照を設定したりするのは非常に手間がかかります。また、誤って同じ管理クラスのオブジェクトを複数シーンに配置してしまうと、予期せぬバグの原因にもなりかねません。
この記事では、Singleton(シングルトン)パターンの基本概念と、Unityでの安全な実装方法を解説します。
Singletonパターンの基本概念
Singletonパターンは、クラスのインスタンスがアプリケーション全体でただ一つであることを保証し、そのインスタンスへのグローバルなアクセスポイントを提供する仕組みです。
通常のC#クラスでこれを実現するには、主に以下の要素 が必要です。
- 静的なインスタンス変数: クラス自身を保持するための
staticな変数。 - プライベートなコンストラクタ: 外部からの
newによるインスタンス生成を防ぐため。 - 静的なアクセスプロパティ: 唯一のインスタンスを返すための
staticなプロパティ。
UnityにおけるMonoBehaviour Singletonの実装
UnityでSingletonパターンを実装する場合、多くの管理クラスはMonoBehaviourを継承する必要があります。これは、AwakeやUpdateといったUnityのライフサイクルメソッドを利用したり、インスペクターから設定を行ったりするためです。
ここでは、ジェネリック(Generic)を用いて、どんなクラスでもSingleton化できる汎用的な基底クラスを作成する方法を紹介します。この方法が、UnityでのSingleton実装のベストプラクティスの一つとされています。
汎用的なMonoSingletonクラス
以下のコードをMonoSingleton.csとして保存してください。
using UnityEngine;
// TはMonoBehaviourを継承したクラスである必要があるという制約
public abstract class MonoSingleton<T> : MonoBehaviour where T : MonoSingleton<T>
{
// 唯一のインスタンスを保持する静的変数
private static T instance;
// 外部からアクセスするための静的プロパティ
public static T Instance
{
get
{
// インスタンスがまだ存在しない場合
if (instance == null)
{
// シーン内からT型のオブジェクトを探す
instance = (T)FindObjectOfType(typeof(T));
// それでも見つからない場合は、新しいGameObjectを作成してアタッチする
if (instance == null)
{
GameObject singletonObject = new GameObject();
instance = singletonObject.AddComponent<T>();
singletonObject.name = typeof(T).ToString() + " (Singleton)";
}
}
return instance;
}
}
// インスタンスが生成された直後に呼ばれる
protected virtual void Awake()
{
// 既にインスタンスが存在する場合(重複生成の防止)
if (instance != null && instance != this)
{
// 自身を破棄して、重複を防ぐ
Debug.LogWarning($"[Singleton] 既にインスタンスが存在するため、{typeof(T).Name}を破棄します。");
Destroy(gameObject);
return;
}
// 自身を唯一のインスタンスとして設定
instance = (T)this;
// シーンを跨いでオブジェクトを永続化させる
// ただし、エディタ上での挙動に注意が必要なため、実行時のみ適用
if (Application.isPlaying)
{
DontDestroyOnLoad(gameObject);
}
// 初期化処理を子クラスに任せるためのメソッド
OnInitialize();
}
// 破棄時に静的参照をクリアする
protected virtual void OnDestroy()
{
if (instance == this)
{
instance = null;
}
}
// 子クラスで初期化処理をオーバーライドするためのメソッド
protected virtual void OnInitialize() { }
}
使い方(例:サウンドマネージャー)
次に、この基底クラスを継承して、実際の管理クラスを作成します。
using UnityEngine;
// MonoSingleton<SoundManager>を継承する
public class SoundManager : MonoSingleton<SoundManager>
{
// 外部からアクセスするための公開メソッド
public void PlayBGM(AudioClip clip)
{
Debug.Log($"BGMを再生しました: {clip.name}");
// 実際のBGM再生処理...
}
public void PlaySE(AudioClip clip)
{
Debug.Log($"効果音を再生しました: {clip.name}");
// 実際SE再生処理...
}
// 初期化処理をオーバーライド
protected override void OnInitialize()
{
// サウンド設定のロードなど、初期化時に一度だけ実行したい処理を記述
Debug.Log("SoundManagerの初期化が完了しました。");
}
}
アクセス方法
これで、ゲーム内のどこからでも、シーンを気にすることなく簡単にアクセスできます。
// どこかの別のスクリプトから
public class PlayerController : MonoBehaviour
{
public AudioClip jumpSound;
void Jump()
{
// 唯一のインスタンスにアクセスし、メソッドを呼び出す
SoundManager.Instance.PlaySE(jumpSound);
}
}
ポイント: MonoSingleton<T>クラスのInstanceプロパティは、インスタンスが存在しない場合に自動的にシーン内を検索し、それでも見つからなければ新しいGameObjectを作成してアタッチします。これにより、手動でシーンに配置し忘れてもエラーにならず、安全性が高まります。
初心者が陥りやすい3つの落とし穴と対策
Singletonパターンは便利ですが、Unity特有のライフサイクルが絡むため、初心者が陥りやすい問題がいくつかあります。
1. シーン遷移時の重複インスタンス生成
最もよくある問題は、DontDestroyOnLoadを使っているSingletonクラスが、シーンを移動した際に重複して生成されてしまうことです。
- 間違いの例: シーンAで
SoundManagerが生成され、DontDestroyOnLoadで永続化されます。次にシーンBをロードした際、シーンBにもSoundManagerのプレハブが配置されていると、2つ目のインスタンスが生成されてしまいます。 - 対策: 上記の
MonoSingleton<T>の実装にあるように、Awakeメソッド内で既にinstanceが存在するかどうかをチェックし、存在する場合はDestroy(gameObject);で自身を即座に破棄 します。これにより、常に一つだけのインスタンスが保証されます。
2. 参照順序の問題(Script Execution Order)
Unityでは、スクリプトの実行順序(AwakeやStartが呼ばれる順番)は基本的に不定です。あるスクリプトのAwakeでSingletonのInstanceにアクセスしようとしたとき、