Buscar..


Observaciones

Si bien hay escuelas de pensamiento que presentan argumentos convincentes sobre por qué el uso sin restricciones de Singletons es una mala idea, por ejemplo, Singleton en gameprogrammingpatterns.com , hay ocasiones en las que es posible que desee conservar un GameObject en Unity en varias escenas (por ejemplo, para una música de fondo perfecta) mientras se asegura que no puede existir más de una instancia; un caso de uso perfecto para un Singleton.

Al agregar este script a un GameObject, una vez que se haya creado una instancia (por ejemplo, al incluirlo en cualquier lugar de una escena) permanecerá activo en todas las escenas, y solo existirá una instancia.


Las instancias de ScriptableObject ( UnityDoc ) proporcionan una alternativa válida a Singletons para algunos casos de uso. Si bien no aplican de manera implícita la regla de instancia única, conservan su estado entre escenas y juegan bien con el proceso de serialización de Unity. También promueven la inversión de control a medida que las dependencias se inyectan a través del editor .

// MyAudioManager.cs
using UnityEngine;

[CreateAssetMenu] // Remember to create the instance in editor
public class MyAudioManager : ScriptableObject {
    public void PlaySound() {}
}
// MyGameObject.cs
using UnityEngine;

public class MyGameObject : MonoBehaviour
{
    [SerializeField]
    MyAudioManager audioManager; //Insert through Inspector

    void OnEnable()
    {
        audioManager.PlaySound();
    }
}

Otras lecturas

Implementación utilizando RuntimeInitializeOnLoadMethodAttribute

Desde Unity 5.2.5 es posible usar RuntimeInitializeOnLoadMethodAttribute para ejecutar la lógica de inicialización sin pasar por el orden de ejecución de MonoBehaviour . Proporciona una manera de crear una implementación más limpia y robusta:

using UnityEngine;

sealed class GameDirector : MonoBehaviour
{
    // Because of using RuntimeInitializeOnLoadMethod attribute to find/create and
    // initialize the instance, this property is accessible and
    // usable even in Awake() methods.
    public static GameDirector Instance
    {
        get; private set;
    }

    // Thanks to the attribute, this method is executed before any other MonoBehaviour
    // logic in the game.
    [RuntimeInitializeOnLoadMethod(RuntimeInitializeLoadType.BeforeSceneLoad)]
    static void OnRuntimeMethodLoad()
    {
        var instance = FindObjectOfType<GameDirector>();

        if (instance == null)
            instance = new GameObject("Game Director").AddComponent<GameDirector>();

        DontDestroyOnLoad(instance);

        Instance = instance;
    }

    // This Awake() will be called immediately after AddComponent() execution
    // in the OnRuntimeMethodLoad(). In other words, before any other MonoBehaviour's
    // in the scene will begin to initialize.
    private void Awake()
    {
        // Initialize non-MonoBehaviour logic, etc.
        Debug.Log("GameDirector.Awake()", this);
    }
}

El orden de ejecución resultante:

  1. GameDirector.OnRuntimeMethodLoad() comenzó ...
  2. GameDirector.Awake()
  3. GameDirector.OnRuntimeMethodLoad() completado.
  4. OtherMonoBehaviour1.Awake()
  5. OtherMonoBehaviour2.Awake() , etc.

Un sencillo Singleton MonoBehaviour en Unity C #

En este ejemplo, una instancia estática privada de la clase se declara al principio.

El valor de un campo estático se comparte entre las instancias, por lo que si se crea una nueva instancia de esta clase, if encontrará una referencia al primer objeto Singleton, destruyendo la nueva instancia (o su objeto del juego).

using UnityEngine;
        
public class SingletonExample : MonoBehaviour {

    private static SingletonExample _instance;
    
    void Awake(){

        if (_instance == null){

            _instance = this;
            DontDestroyOnLoad(this.gameObject);
    
            //Rest of your Awake code
    
        } else {
            Destroy(this);
        }
    }

    //Rest of your class code

}

Unidad avanzada de Singleton

Este ejemplo combina múltiples variantes de singletons MonoBehaviour que se encuentran en Internet en uno solo y le permite cambiar su comportamiento según los campos estáticos globales.

Este ejemplo se probó con Unity 5. Para usar este singleton, todo lo que necesita hacer es extenderlo de la siguiente manera: public class MySingleton : Singleton<MySingleton> {} . También es posible que deba anular AwakeSingleton para usarlo en lugar de Awake habitual. Para realizar más ajustes, cambie los valores predeterminados de los campos estáticos como se describe a continuación.


  1. Esta aplicación hace uso de DisallowMultipleComponent atributo para mantener una instancia por GameObject.
  2. Esta clase es abstracta y solo se puede ampliar. También contiene un método virtual, AwakeSingleton que debe ser anulado en lugar de implementar Awake normal.
  3. Esta implementación es segura para subprocesos.
  4. Este singleton está optimizado. Al utilizar la instantiated lugar de la verificación nula de la instancia, evitamos la sobrecarga que conlleva la implementación de Unity del operador == . ( Leer más )
  5. Esta implementación no permite ninguna llamada a la instancia de singleton cuando está a punto de ser destruida por Unity.
  6. Este singleton viene con las siguientes opciones:
  • FindInactive : si hay que buscar otras instancias de componentes del mismo tipo adjuntos a GameObject inactivo.
  • Persist : mantener el componente vivo entre escenas.
  • DestroyOthers : si destruir cualquier otro componente del mismo tipo y conservar solo uno.
  • Lazy : si se debe establecer la instancia de singleton "sobre la marcha" (en Awake ) o solo "a pedido" (cuando se llama getter).
using UnityEngine;

[DisallowMultipleComponent]
public abstract class Singleton<T> : MonoBehaviour where T : Singleton<T>
{
    private static volatile T instance;
    // thread safety
    private static object _lock = new object();
    public static bool FindInactive = true;
    // Whether or not this object should persist when loading new scenes. Should be set in Init().
    public static bool Persist;
    // Whether or not destory other singleton instances if any. Should be set in Init().
    public static bool DestroyOthers = true;
    // instead of heavy comparision (instance != null)
    // http://blogs.unity3d.com/2014/05/16/custom-operator-should-we-keep-it/
    private static bool instantiated;

    private static bool applicationIsQuitting;

    public static bool Lazy;

    public static T Instance
    {
        get
        {
            if (applicationIsQuitting)
            {
                Debug.LogWarningFormat("[Singleton] Instance '{0}' already destroyed on application quit. Won't create again - returning null.", typeof(T));
                return null;
            }
            lock (_lock)
            {
                if (!instantiated)
                {
                    Object[] objects;
                    if (FindInactive) { objects = Resources.FindObjectsOfTypeAll(typeof(T)); }
                    else { objects = FindObjectsOfType(typeof(T)); }
                    if (objects == null || objects.Length < 1)
                    {
                        GameObject singleton = new GameObject();
                        singleton.name = string.Format("{0} [Singleton]", typeof(T));
                        Instance = singleton.AddComponent<T>();
                        Debug.LogWarningFormat("[Singleton] An Instance of '{0}' is needed in the scene, so '{1}' was created{2}", typeof(T), singleton.name, Persist ? " with DontDestoryOnLoad." : ".");
                    }
                    else if (objects.Length >= 1)
                    {
                        Instance = objects[0] as T;
                        if (objects.Length > 1)
                        {
                            Debug.LogWarningFormat("[Singleton] {0} instances of '{1}'!", objects.Length, typeof(T));
                            if (DestroyOthers)
                            {
                                for (int i = 1; i < objects.Length; i++)
                                {
                                    Debug.LogWarningFormat("[Singleton] Deleting extra '{0}' instance attached to '{1}'", typeof(T), objects[i].name);
                                    Destroy(objects[i]);
                                }
                            }
                        }
                        return instance;
                    }
                }
                return instance;
            }
        }
        protected set
        {
            instance = value;
            instantiated = true;
            instance.AwakeSingleton();
            if (Persist) { DontDestroyOnLoad(instance.gameObject); }
        }
    }

    // if Lazy = false and gameObject is active this will set instance
    // unless instance was called by another Awake method
    private void Awake()
    {
        if (Lazy) { return; }
        lock (_lock)
        {
            if (!instantiated)
            {
                Instance = this as T;
            }
            else if (DestroyOthers && Instance.GetInstanceID() != GetInstanceID())
            {
                Debug.LogWarningFormat("[Singleton] Deleting extra '{0}' instance attached to '{1}'", typeof(T), name);
                Destroy(this);
            }
        }
    }
    
    // this might be called for inactive singletons before Awake if FindInactive = true
    protected virtual void AwakeSingleton() {}

    protected virtual void OnDestroy()
    {
        applicationIsQuitting = true;
        instantiated = false;
    }
}

Implementación Singleton a través de clase base

En proyectos que cuentan con varias clases singleton (como suele ser el caso), puede ser limpio y conveniente abstraer el comportamiento singleton a una clase base:

using UnityEngine;
using System.Collections.Generic;
using System;

public abstract class MonoBehaviourSingleton<T> : MonoBehaviour {
    
    private static Dictionary<Type, object> _singletons
        = new Dictionary<Type, object>();

    public static T Instance {
        get {
            return (T)_singletons[typeof(T)];
        }
    }

    void OnEnable() {
        if (_singletons.ContainsKey(GetType())) {
            Destroy(this);
        } else {
            _singletons.Add(GetType(), this);
            DontDestroyOnLoad(this);
        }
    }
}

Un MonoBehaviour puede entonces implementar el patrón de singleton al extender MonoBehaviourSingleton. Este enfoque permite utilizar el patrón con una huella mínima en el propio Singleton:

using UnityEngine;
using System.Collections;

public class SingletonImplementation : MonoBehaviourSingleton<SingletonImplementation> {

    public string Text= "String Instance";

    // Use this for initialisation
    IEnumerator Start () {
        var demonstration = "SingletonImplementation.Start()\n" +
                            "Note that the this text logs only once and\n"
                            "only one class instance is allowed to exist.";
        Debug.Log(demonstration);
        yield return new WaitForSeconds(2f);
        var secondInstance = new GameObject();
        secondInstance.AddComponent<SingletonImplementation>();
    }
   
}

Tenga en cuenta que uno de los beneficios del patrón singleton es que se puede acceder a una referencia a la instancia de forma estática:

// Logs: String Instance
Debug.Log(SingletonImplementation.Instance.Text);

Sin embargo, tenga en cuenta que esta práctica debe minimizarse para reducir el acoplamiento. Este enfoque también tiene un ligero costo de rendimiento debido al uso del Diccionario, pero como esta colección puede contener solo una instancia de cada clase de singleton, la compensación en términos del principio DRY (No se repita), legibilidad y la conveniencia es pequeña

Patrón Singleton utilizando el sistema Entity-Component de Unitys

La idea central es usar GameObjects para representar singletons, que tiene múltiples ventajas:

  • Mantiene la complejidad al mínimo, pero admite conceptos como la inyección de dependencia.
  • Los Singletons tienen un ciclo de vida normal de Unity como parte del sistema Entity-Component
  • Los Singletons pueden cargarse y almacenarse de manera local en lugares donde se necesitan regularmente (por ejemplo, en ciclos de actualización)
  • No se necesitan campos estáticos
  • No es necesario modificar los MonoBehaviours / Componentes existentes para usarlos como Singletons
  • Fácil de restablecer (solo destruye el Singletons GameObject), se cargará de nuevo en el próximo uso
  • Fácil de inyectar simulacros (solo inicialízalo con el simulacro antes de usarlo)
  • Inspección y configuración usando el editor normal de Unity y puede ocurrir ya en el tiempo del editor ( Captura de pantalla de un Singleton accesible en el editor de Unity )

Test.cs (que usa el ejemplo singleton):

using UnityEngine;
using UnityEngine.Assertions;

public class Test : MonoBehaviour {
    void Start() {
        ExampleSingleton singleton = ExampleSingleton.instance;
        Assert.IsNotNull(singleton); // automatic initialization on first usage
        Assert.AreEqual("abc", singleton.myVar1);
        singleton.myVar1 = "123";
        // multiple calls to instance() return the same object:
        Assert.AreEqual(singleton, ExampleSingleton.instance); 
        Assert.AreEqual("123", ExampleSingleton.instance.myVar1);
    }
}

ExampleSingleton.cs (que contiene un ejemplo y la clase Singleton real):

using UnityEngine;
using UnityEngine.Assertions;

public class ExampleSingleton : MonoBehaviour {
    public static ExampleSingleton instance { get { return Singleton.get<ExampleSingleton>(); } }
    public string myVar1 = "abc";
    public void Start() { Assert.AreEqual(this, instance, "Singleton more than once in scene"); } 
}

/// <summary> Helper that turns any MonBehaviour or other Component into a Singleton </summary>
public static class Singleton {
    public static T get<T>() where T : Component {
        return GetOrAddGo("Singletons").GetOrAddChild("" + typeof(T)).GetOrAddComponent<T>();
    }
    private static GameObject GetOrAddGo(string goName) {
        var go = GameObject.Find(goName);
        if (go == null) { return new GameObject(goName); }
        return go;
    }
}

public static class GameObjectExtensionMethods { 
    public static GameObject GetOrAddChild(this GameObject parentGo, string childName) {
        var childGo = parentGo.transform.FindChild(childName);
        if (childGo != null) { return childGo.gameObject; } // child found, return it
        var newChild = new GameObject(childName);        // no child found, create it
        newChild.transform.SetParent(parentGo.transform, false); // add it to parent
        return newChild;
    }

    public static T GetOrAddComponent<T>(this GameObject parentGo) where T : Component {
        var comp = parentGo.GetComponent<T>();
        if (comp == null) { return parentGo.AddComponent<T>(); }
        return comp;
    }
}

Los dos métodos de extensión para GameObject también son útiles en otras situaciones, si no los necesitas, muévelos dentro de la clase Singleton y hazlos privados.

Clase de Singleton basada en MonoBehaviour y ScriptableObject

La mayoría de los ejemplos de Singleton usan MonoBehaviour como la clase base. La principal desventaja es que esta clase Singleton solo vive durante el tiempo de ejecución. Esto tiene algunos inconvenientes:

  • No hay forma de editar directamente los campos singleton que no sea cambiar el código.
  • No hay forma de almacenar una referencia a otros activos en el Singleton.
  • No hay forma de configurar el singleton como el destino de un evento de UI de Unity. Termino usando lo que llamo "Componentes Proxy", su única propuesta es tener métodos de 1 línea que llamen "GameManager.Instance.SomeGlobalMethod ()".

Como se señaló en los comentarios, hay implementaciones que intentan resolver esto utilizando ScriptableObjects como clase base pero pierden los beneficios de tiempo de ejecución de MonoBehaviour. Esta implementación resuelve estos problemas utilizando un objeto ScriptableObject como clase base y un MonoBehavior asociado durante el tiempo de ejecución:

  • Es un activo por lo que sus propiedades pueden actualizarse en el editor como cualquier otro activo de Unity.
  • Juega muy bien con el proceso de serialización de Unity.
  • Es posible asignar referencias en el singleton a otros activos desde el editor (las dependencias se inyectan a través del editor).
  • Los eventos de Unity pueden llamar directamente a los métodos en Singleton.
  • Puede llamarlo desde cualquier lugar en la base de código usando "SingletonClassName.Instance"
  • Tiene acceso para ejecutar eventos MonoBehaviour de tiempo y métodos como: Actualizar, Despertar, Iniciar, FixedUpdate, StartCoroutine, etc.
/************************************************************
 * Better Singleton by David Darias
 * Use as you like - credit where due would be appreciated :D
 * Licence: WTFPL V2, Dec 2014
 * Tested on Unity v5.6.0 (should work on earlier versions)
 * 03/02/2017 - v1.1 
 * **********************************************************/

using System;
using UnityEngine;
using SingletonScriptableObjectNamespace;

public class SingletonScriptableObject<T> : SingletonScriptableObjectNamespace.BehaviourScriptableObject where T : SingletonScriptableObjectNamespace.BehaviourScriptableObject
{
    //Private reference to the scriptable object
    private static T _instance;
    private static bool _instantiated;
    public static T Instance
    {
        get
        {
            if (_instantiated) return _instance;
            var singletonName = typeof(T).Name;
            //Look for the singleton on the resources folder
            var assets = Resources.LoadAll<T>("");
            if (assets.Length > 1) Debug.LogError("Found multiple " + singletonName + "s on the resources folder. It is a Singleton ScriptableObject, there should only be one.");
            if (assets.Length == 0)
            {
                _instance = CreateInstance<T>();
                Debug.LogError("Could not find a " + singletonName + " on the resources folder. It was created at runtime, therefore it will not be visible on the assets folder and it will not persist.");
            }
            else _instance = assets[0];
            _instantiated = true;
            //Create a new game object to use as proxy for all the MonoBehaviour methods
            var baseObject = new GameObject(singletonName);
            //Deactivate it before adding the proxy component. This avoids the execution of the Awake method when the the proxy component is added.
            baseObject.SetActive(false);
            //Add the proxy, set the instance as the parent and move to DontDestroyOnLoad scene
            SingletonScriptableObjectNamespace.BehaviourProxy proxy = baseObject.AddComponent<SingletonScriptableObjectNamespace.BehaviourProxy>();
            proxy.Parent = _instance;
            Behaviour = proxy;
            DontDestroyOnLoad(Behaviour.gameObject);
            //Activate the proxy. This will trigger the MonoBehaviourAwake. 
            proxy.gameObject.SetActive(true);
            return _instance;
        }
    }
    //Use this reference to call MonoBehaviour specific methods (for example StartCoroutine)
    protected static MonoBehaviour Behaviour;
    public static void BuildSingletonInstance() { SingletonScriptableObjectNamespace.BehaviourScriptableObject i = Instance; }
    private void OnDestroy(){ _instantiated = false; }
}

// Helper classes for the SingletonScriptableObject
namespace SingletonScriptableObjectNamespace
{
    #if UNITY_EDITOR
    //Empty custom editor to have cleaner UI on the editor.
    using UnityEditor;
    [CustomEditor(typeof(BehaviourProxy))]
    public class BehaviourProxyEditor : Editor
    {
        public override void OnInspectorGUI(){}
    }
    
    #endif
    
    public class BehaviourProxy : MonoBehaviour
    {
        public IBehaviour Parent;

        public void Awake() { if (Parent != null) Parent.MonoBehaviourAwake(); }
        public void Start() { if (Parent != null) Parent.Start(); }
        public void Update() { if (Parent != null) Parent.Update(); }
        public void FixedUpdate() { if (Parent != null) Parent.FixedUpdate(); }
    }

    public interface IBehaviour
    {
        void MonoBehaviourAwake();
        void Start();
        void Update();
        void FixedUpdate();
    }

    public class BehaviourScriptableObject : ScriptableObject, IBehaviour
    {
        public void Awake() { ScriptableObjectAwake(); }
        public virtual void ScriptableObjectAwake() { }
        public virtual void MonoBehaviourAwake() { }
        public virtual void Start() { }
        public virtual void Update() { }
        public virtual void FixedUpdate() { }
    }
}

Aquí hay un ejemplo de clase Singleton de GameManager usando SingletonScriptableObject (con muchos comentarios):

using System;
using System.Collections;
using System.Collections.Generic;
using UnityEngine;

//this attribute is optional but recommended. It will allow the creation of the singleton via the asset menu.
//the singleton asset should be on the Resources folder.
[CreateAssetMenu(fileName = "GameManager", menuName = "Game Manager", order = 0)]
public class GameManager : SingletonScriptableObject<GameManager> {

    //any properties as usual
    public int Lives;
    public int Points;

    //optional (but recommended)
    //this method will run before the first scene is loaded. Initializing the singleton here
    //will allow it to be ready before any other GameObjects on every scene and will
    //will prevent the "initialization on first usage". 
    [RuntimeInitializeOnLoadMethod(RuntimeInitializeLoadType.BeforeSceneLoad)]
    public static void BeforeSceneLoad() { BuildSingletonInstance(); }

    //optional,
    //will run when the Singleton Scriptable Object is first created on the assets. 
    //Usually this happens on edit mode, not runtime. (the override keyword is mandatory for this to work)
    public override void ScriptableObjectAwake(){
        Debug.Log(GetType().Name + " created." );
    }

    //optional,
    //will run when the associated MonoBehavioir awakes. (the override keyword is mandatory for this to work)
    public override void MonoBehaviourAwake(){
        Debug.Log(GetType().Name + " behaviour awake." );

        //A coroutine example:
        //Singleton Objects do not have coroutines.
        //if you need to use coroutines use the atached MonoBehaviour
        Behaviour.StartCoroutine(SimpleCoroutine());
    }

    //any methods as usual
    private IEnumerator SimpleCoroutine(){
        while(true){
            Debug.Log(GetType().Name + " coroutine step." );
            yield return new WaitForSeconds(3);
        }
    }

    //optional,
    //Classic runtime Update method (the override keyword is mandatory for this to work).
    public override void Update(){

    }

    //optional,
    //Classic runtime FixedUpdate method (the override keyword is mandatory for this to work).
    public override void FixedUpdate(){

    }
}

/*
*  Notes:
*  - Remember that you have to create the singleton asset on edit mode before using it. You have to put it on the Resources folder and of course it should be only one. 
*  - Like other Unity Singleton this one is accessible anywhere in your code using the "Instance" property i.e: GameManager.Instance
*/


Modified text is an extract of the original Stack Overflow Documentation
Licenciado bajo CC BY-SA 3.0
No afiliado a Stack Overflow