読者です 読者をやめる 読者になる 読者になる

しゅみぷろ

プログラミングとか

UnityでのSingletonについて

C# Unity

UnityのMonoBehaviourをSingletonにしたい時って結構ありますよね。 調べれば、シンプルなSingletonMonoBehaviourの実装とかそんな感じですごいたくさん出てきます。

しかし、公開されているものの中には結構用法によっては不便に思えるものが多かったりします。 基本的に、公開されているものが満たす要件は以下の通りです。

機能的には十分だと思います。しかし、このSingletonMonoBehaviourコンポーネントを持つゲームオブジェクトがまだ生成されてい無い場合には、問題になる場合があります。シーンが複数ある場合、一番最初のシーンでゲームオブジェクトを生成しDontDestroyOnLoadを用いてシーンが遷移しても削除されないようにすればこの問題を回避できるのですが、面倒です。

結論から言えば、SingletonMonoBehaviour.Instanceプロパティを叩いた段階で、Nullが返ってきてほしく無いわけです。 SingletonMonoBehaviour.Instanceプロパティをgetした際、インスタンスの実体が入るフィールドがnullであれば動的に生成し、その値をSingletonとして使いたい、といった要望に沿わないため、運用ルール(Singletonが利用されるより前のシーンで必ずゲームオブジェクトを生成する)を無視した場合に初期化されておらずエラーが出る原因になります。

この要件を満たすSingletonMonoBehaviourの実装が紹介されているサイトがありましたので、リンクを貼っておきます。

kleber-swf.com

SingletonMonoBehaviour.Instanceが空だった場合、Prefab属性を参照してResourceからPrefabをロードし、インスタンス化しています。SingletonMonoBehaviourを継承するクラスでこのPrefab属性をつけてやれば、Instanceがnullの時、動的に生成してくれるようになるわけです。

私の場合、このSingletonMonoBehaviourを以下のように少し変えて使っています。

using System;
using UnityEngine;

/// <summary>
/// 常に単一のインスタンスしか存在しないことを保証する
/// </summary>
public abstract class SingletonMonoBehaviour<T> : MonoBehaviour where T : MonoBehaviour
{
  private static T instance;
  private static bool instantiated;

  private void Awake()
  {
    if(FindObjectsOfType<T>().Length > 1)
      DestroyImmediate(this);
  }

  /// <summary>
  /// インスタンスへのアクセスを提供する
  /// </summary>
  public static T Instance
  {
    get
    {
      if(instantiated)
        return instance;

      var type = typeof(T);
      var objects = FindObjectsOfType<T>();

      if(objects.Length > 0)
      {
        Instance = objects[0];
        if(objects.Length > 1)
        {
          Debug.LogWarning("複数の\"" + type + "\"が存在したため、削除されました");
          for(var i = 1; i < objects.Length; i++)
            DestroyImmediate(objects[i].gameObject);
        }
        instantiated = true;
        return instance;
      }

      Debug.LogWarning("ゲーム内に" + type + "型インスタンスが存在しないためResources内のPrefabからの作成処理を行います");

      var attribute = Attribute.GetCustomAttribute(type, typeof(PrefabAttribute)) as PrefabAttribute;
      if(attribute == null)
        Debug.LogError("PrefabAttributeが付加されていない型\"" + type + "\"が参照されました");
      else
      {
        var prefabName = attribute.Path;
        if(String.IsNullOrEmpty(prefabName))
        {
          Debug.LogError("PrefabAttributeのnameが空です \"" + type + "\"");
          return null;
        }

        var gameObject = Instantiate(Resources.Load<GameObject>(prefabName)) as GameObject;
        if(gameObject == null)
        {
          Debug.LogError("\"type\"" + "型のPrefab\"" + prefabName + "\"を生成できませんでした");
          return null;
        }
        gameObject.name = prefabName;
        Instance = gameObject.GetComponent<T>();
        if(!instantiated)
        {
          Debug.LogWarning("\"" + type + "\"型のComponentが\"" + prefabName + "\"に存在しなかったため追加されました");
          Instance = gameObject.AddComponent<T>();
        }
        if(attribute.Persistent)
          DontDestroyOnLoad(instance.gameObject);
        return instance;
      }

      Debug.LogWarning("Prefabからの生成に失敗したため、強制的にオブジェクトを生成します");
      var createObject = new GameObject("SingletonCreateObject", type);
      Instance = createObject.GetComponent<T>();
      return instance;
    }

    private set
    {
      instance = value;
      instantiated = value != null;
    }
  }

  private void OnDestroy()
  {
    instantiated = false;
  }
}
using System;

/// <summary>
/// Resourcesフォルダ配下のPrefabに付加される情報を表す
/// </summary>
[AttributeUsage(AttributeTargets.Class, Inherited = true)]
public class PrefabAttribute : Attribute
{
  /// <summary>
  /// Resourcesフォルダ以降のPrefabのパス
  /// </summary>
  public readonly string Path;

  /// <summary>
  /// シーンロード時に消失しないオブジェクトであるべきかどうか
  /// </summary>
  public readonly bool Persistent;

  /// <summary>
  /// プレハブ情報を表すデータを生成する
  /// </summary>
  /// <param name="path">Resources.Loadで呼び出せるプレハブのへのパス</param>
  /// <param name="persistent">シーン変更時に消失しないオブジェクトにするかどうか</param>
  public PrefabAttribute(string path, bool persistent = false)
  {
    Path = path;
    Persistent = persistent;
  }
}

ゲーム上でのSingletonとシーン内でのSingletonを実現するため、PrefabAttributeに情報を追加しています。 また、PrefabAttributeが付加されていない場合でも強制的にゲームオブジェクトとコンポーネントを生成します。