【Unity】派生クラスをSingletonにしたい時どうする?

SingletonMonoBehaviourの問題

先日こちらの記事を書いたんですが、ここで書いたものを使っていてあれ?っとなったので追加記事です。

ことの発端は基底クラスがあるものをSingleton化したい場合にはどうすればいいんだ?と思ったことでした。

class BassClass があり、これを継承したclass ChildClassAがあるとします。

このとき、ChildClassAをSingletonにしたい場合にはどうすりゃいいのでしょうか?

何も考えずに書くと下記のように書きたくなります。(関係性だけ書きます)

class BaseClass

class ChildClassA : BaseClass, SingletonMonoBehaviour<ChildClassA>

しかし、interfaceと違って複数のクラスを継承することはできないので上記はコンパイルエラーとなります。

じゃあ、基底クラスをSingleton化して、それを継承したらいいのでは?
ということで下記のように書きたくなるわけですが

class BaseClass : SingletonMonoBehaviour<BaseClass>

class ChildClassA : BaseClass
class ChildClassB : BaseClass

これは2つの点で問題が出てきます。

1.ChildClassA.InstanceでアクセスできるのがBaseClass型になる。
なので、ChildClassAに実装したはずのプロパティやメソッドにアクセスできなくなります。

2.BaseClassを継承した別のクラスを定義することができない。
基底クラスを用意したので、通常は複数の派生クラスが実装されることになると思いますが、Singletonの実装によりそれができません。
(仮に「class ChildClassB : BaseClass」を定義しても先にInstanceにアクセスしたものが優先され、後に参照されるものは消滅します)

じゃあどうする?

さてどうしようとなるわけですが、ここでは2つのアプローチを紹介します。

案1 BaseClassもジェネリックで定義する

今の実装を活かしBaseClass(基底クラス)もジェネリック型で定義する方法です。

/// <summary>
/// BaseClassをジェネリックにし、各派生クラスが自身を型パラメータとして渡すようにします。
/// </summary>
/// <typeparam name="T">派生クラスの型</typeparam>
public abstract class SampleBaseClass<T> : SingletonMonoBehaviour<T> where T : SampleBaseClass<T>
{
    // SampleBaseClass固有の機能やプロパティをここに追加します。

    protected override void OnAwake()
    {
        base.OnAwake();
        // BaseClassの初期化ロジック
    }

    // 他の共通メソッドやプロパティ
}

基底クラスをジェネリックで定義した上で、派生クラスを以下のようにするとSampleSingltonA,Bで別々のインスタンスを正しく参照するようになります。

public class SampleSingletonA : SampleBaseClass<SampleSingletonA>
{
    protected override void OnAwake()
    {
        base.OnAwake();

        // SampleSingletonAの初期化ロジック
    }
}

public class SampleSingletonB : SampleBaseClass<SampleSingletonB>
{
    protected override void OnAwake()
    {
        base.OnAwake();

        // SampleSingletonAの初期化ロジック
    }
}

案2 MultiInheritanceSingletonMonoBehaviourを使う

ジェネリックの実装よくわからんという人もいると思うので(私もです)、別途多段継承にに対応したSingletonMonoBehaviourを実装して使うというのが案2です。

using System.Collections.Generic;
using UnityEngine;

/// <summary>
/// 多段継承をサポートするシングルトンMonoBehaviourの基底クラス。
/// </summary>
public abstract class MultiInheritanceSingletonMonoBehaviour : MonoBehaviour
{
    private static readonly Dictionary<System.Type, MultiInheritanceSingletonMonoBehaviour> instances =
        new Dictionary<System.Type, MultiInheritanceSingletonMonoBehaviour>();

    private static bool applicationIsQuitting = false;

    /// <summary>
    /// シングルトンインスタンスへのアクセサ。
    /// 派生クラスからアクセスします。
    /// </summary>
    /// <typeparam name="T">派生クラスの型</typeparam>
    /// <returns>シングルトンインスタンス</returns>
    public static T GetInstance<T>() where T : MultiInheritanceSingletonMonoBehaviour
    {
        if (applicationIsQuitting)
        {
            return null;
        }

        System.Type type = typeof(T);

        if (!instances.ContainsKey(type) || instances[type] == null)
        {
            T instance = FindObjectOfType<T>();
            if (instance == null)
            {
                GameObject obj = new GameObject(type.Name);
                instance = obj.AddComponent<T>();
            }
            instances[type] = instance;
        }

        return instances[type] as T;
    }

    /// <summary>
    /// Awakeメソッド。シングルトンの設定と初期化を行います。
    /// </summary>
    protected virtual void Awake()
    {
        System.Type type = this.GetType();

        if (applicationIsQuitting)
        {
            Destroy(gameObject);
            return;
        }

        if (instances.ContainsKey(type))
        {
            if (instances[type] == null)
            {
                instances[type] = this;
                OnAwake();
            }
            else if (instances[type] != this)
            {
                Debug.LogWarning($"[Singleton] Instance of {type} already exists - destroying duplicate {gameObject.name}");
                Destroy(gameObject);
            }
        }
        else
        {
            instances[type] = this;
            OnAwake();
        }
    }

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

    /// <summary>
    /// アプリケーション終了時の処理。シングルトンインスタンスをクリーンアップします。
    /// </summary>
    protected virtual void OnApplicationQuit()
    {
        applicationIsQuitting = true;
        instances.Clear();
    }
}

BaseClassでこれを継承するようにします。

public class SampleBaseClass : MultiInheritanceSingletonMonoBehaviour
{
}

派生クラスでは下記のようにBaseClassを継承しInstanceのプロパティを下記のように定義します。

public class SampleSingletonA : SampleBaseClass 
{
    public static SampleSingletonA Instance => GetInstance<SampleSingletonA>();
}

public class SampleSingletonB : SampleBaseClass 
{
    public static SampleSingletonB Instance => GetInstance<SampleSingletonB>();
}

これでSampleSingletonA、Bともに独自のInstanceを持つことができます。

さいごに

以上が派生クラスをSingletonにする対応案です。
個人的には上記どっちががよいかと思っていますが、他に良い案をご存じの方いましたら教えて下さい!

コメント

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