【Unity】UnityにおけるSingletonについて

Singletonってなに?

みなさんSingleton使ってますか?

私も勉強している最中ですが、いわゆるデザインパターンと呼ばれるプログラムの設計パターンの一つです。

デザインパターン (ソフトウェア) - Wikipedia

Singletonはアプリケーションの中で該当のクラスが1つしか存在しないことを保証するデザインパターンとなります。

なにが便利?

このSingletonパターンはUnityでも使用することができます。

クラスのインスタンスがどこからでも参照できるようになるので、多用するのもあまり良くないらしいですが、私は便利なので結構使ってしまっています。

特にGameManagerなど諸々を管理するマネージャクラスは色んなところからアクセスしたくなります。

しかし、そのたびにコンポーネントにフィールドを用意して、インスペクタから参照を設定して・・・っていうことをマネージャの参照が必要なコンポーネント全てに対して行っていくのは地味に面倒です。

そこで便利なのがSingletonです。

実装例

自分の場合はSingleton用の基底クラスを下記のように実装しています。

using UnityEngine;

/// <summary>
/// 標準的なシングルトンMonoBehaviourの基底クラス。
/// 即時初期化が必要な場合や、小規模なManagerクラスに適しています。
/// 
/// 注意: このクラスは、GameObjectにアタッチされている場合とそうでない場合で挙動が異なります。
/// - アタッチされている場合: Awakeメソッドはシーンのロード時に自動的に呼ばれます。
/// - アタッチされていない場合: 最初のInstanceアクセス時に新しいGameObjectが作成され、
///   その時点でAwakeメソッドが呼ばれます。
/// </summary>
/// <typeparam name="T">派生クラスの型</typeparam>
public abstract class SingletonMonoBehaviour<T> : MonoBehaviour where T : SingletonMonoBehaviour<T>
{
    private static T instance;
    private static bool applicationIsQuitting = false; // フラグを追加

    /// <summary>
    /// シングルトンインスタンスへのアクセサ。
    /// インスタンスが存在しない場合は新たに作成します。
    /// </summary>
    public static T Instance
    {
        get
        {
            if (applicationIsQuitting)
            {
                return null; // アプリケーション終了中は新たにインスタンスを生成しない
            }

            if (instance == null)
            {
                instance = FindObjectOfType<T>();
                if (instance == null)
                {
                    GameObject obj = new GameObject(typeof(T).Name);
                    instance = obj.AddComponent<T>();
                }
            }
            return instance;
        }
    }

    /// <summary>
    /// DontDestroyOnLoadを使用するかどうかを指定します。
    /// trueの場合、シーン遷移時もオブジェクトが破棄されません。
    /// </summary>
    [SerializeField] protected bool dontDestroyOnLoad = false;

    /// <summary>
    /// Awakeメソッド。シングルトンの設定と初期化を行います。
    /// </summary>
    protected virtual void Awake()
    {
        if (instance == null)
        {
            instance = this as T;
            if (dontDestroyOnLoad)
            {
                if (transform.parent != null)
                {
                    Debug.LogWarning($"[Singleton] {typeof(T)} is marked as DontDestroyOnLoad, but it has a parent. Detaching from parent.");
                    transform.SetParent(null);
                }
                DontDestroyOnLoad(gameObject);
            }
            OnAwake();
        }
        else if (instance != this)
        {
            Debug.LogWarning($"[Singleton] Instance already exists - destroying duplicate {gameObject.name}");
            Destroy(gameObject);
        }
    }

    /// <summary>
    /// 派生クラスで上書きして、カスタムの初期化ロジックを実装します。
    /// </summary>
    protected virtual void OnAwake() { }

    /// <summary>
    /// アプリケーション終了時の処理。シングルトンインスタンスをクリーンアップします。
    /// </summary>
    protected virtual void OnApplicationQuit()
    {
        applicationIsQuitting = true; // フラグを設定
        if (instance == this)
        {
            instance = null;
        }
    }
}

これを利用する際にはまず、これを継承したクラスを作成します。
例えば以下のような形。SingletonMonoBehaviourを継承してInputManagerを定義しています。

実装上、ここで作成したInputManagerはGameObjectにアタッチしなくてもアクセスした時点で自動的にゲームオブジェクトが作成されます。
なので、GameObjectにアタッチするかはお好みで。私はアタッチして使っています。

using System;
using UnityEngine;

public class InputManager : SingletonMonoBehaviour<InputManager>
{
    protected override void Awake()
    {
        base.Awake();

        if (Instance == this)
        {
            //初期化が必要であればここで
        }
    }

    private void Start()
    {

    }


    private void Update()
    {

    }

    public void SampleFunction()
    {
        //なにかの処理
    }

}

これを参照して使用するには下記のように「クラス名.Instance」という形でアクセスすることができます。
見ての通りフィールドを用意したりGetComponetなどを使うことなく、InputManagerの機能を利用することができます。

public class TestScript : MonoBehaviour
{
    // Awake is called when the script instance is being loaded.
    void Awake()
    {

    }

    // Start is called before the first frame update
    void Start()
    {
        
    }

    // Update is called once per frame
    void Update()
    {
        InputManager.Instance.SampleFunction();  //Instanceプロパティ経由でアクセスを行う
    }
}

GameObjectにアタッチしていない場合にはコードの何処かで最初にアクセスしたタイミングでスクリプトがGameObjectを作成します。

さいごに

説明は以上です。

使ってみると便利なので、自分が制御できる範囲で活用してみてください。

コメント

タイトルとURLをコピーしました