En Unity, ¿cómo implemento correctamente el patrón singleton?

36

He visto varios videos y tutoriales para crear objetos singleton en Unity, principalmente para a GameManager, que parecen usar diferentes enfoques para crear instancias y validar un singleton.

¿Existe un enfoque correcto, o más bien preferido, para esto?

Los dos ejemplos principales que he encontrado son:

primero

public class GameManager
{
    private static GameManager _instance;

    public static GameManager Instance
    {
        get
        {
            if(_instance == null)
            {
                _instance = GameObject.FindObjectOfType<GameManager>();
            }

            return _instance;
        }
    }

    void Awake()
    {
        DontDestroyOnLoad(gameObject);
    }
}

Segundo

public class GameManager
{
    private static GameManager _instance;

    public static GameManager Instance
    {
        get
        {
            if(_instance == null)
            {
                instance = new GameObject("Game Manager");
                instance.AddComponent<GameManager>();
            }

            return _instance;
        }
    }

    void Awake()
    {
        _instance = this;
    }
}

La principal diferencia que puedo ver entre los dos es:

El primer enfoque intentará navegar por la pila de objetos del juego para encontrar una instancia de la GameManagercual, aunque esto solo suceda (o solo suceda), una vez parezca que podría no ser optimizada a medida que las escenas crecen en tamaño durante el desarrollo.

Además, el primer enfoque marca el objeto para que no se elimine cuando la aplicación cambia de escena, lo que garantiza que el objeto persista entre escenas. El segundo enfoque no parece adherirse a esto.

El segundo enfoque parece extraño, ya que en el caso de que la instancia sea nula en el captador, creará un nuevo GameObject y le asignará un componente GameManger. Sin embargo, esto no puede ejecutarse sin tener este componente GameManager ya conectado a un objeto en la escena, por lo que esto me está causando cierta confusión.

¿Hay otros enfoques que se recomendarían, o un híbrido de los dos anteriores? Hay muchos videos y tutoriales sobre singletons, pero todos difieren tanto que es difícil establecer comparaciones entre los dos y, por lo tanto, llegar a una conclusión sobre cuál es el enfoque mejor / preferido.

CaptainRedmuff
fuente
¿Qué se supone que debe hacer GameManager? ¿Tiene que ser un GameObject?
bummzack
1
No es realmente una cuestión de lo que GameManagerdeberían hacer, sino cómo asegurarse de que solo haya una instancia del objeto y la mejor manera de hacer cumplir eso.
CaptainRedmuff
estos tutoriales muy bien explicados, cómo implementar singleton unitygeek.com/unity_c_singleton , espero que sea útil
Rahul Lalit

Respuestas:

30

Depende, pero generalmente uso un tercer método. El problema con los métodos que usó es que, en el caso de que el objeto esté incluido, para empezar, no los eliminará del árbol, y aún pueden crearse instanciando demasiadas llamadas, lo que podría hacer que las cosas sean realmente confusas.

public class SomeClass : MonoBehaviour {
    private static SomeClass _instance;

    public static SomeClass Instance { get { return _instance; } }


    private void Awake()
    {
        if (_instance != null && _instance != this)
        {
            Destroy(this.gameObject);
        } else {
            _instance = this;
        }
    }
}

El problema con ambas implementaciones es que no destruyen un objeto que se crea más tarde. Podría funcionar, pero uno podría lanzar una llave inglesa a las obras que podría resultar en un error muy difícil de depurar en el futuro. Asegúrese de verificar en Despertar si ya hay una instancia y, de ser así, destruir la nueva instancia.

PearsonArtPhoto
fuente
2
También es posible que desee OnDestroy() { if (this == _instance) { _instance = null; } }, si desea tener una instancia diferente en cada escena.
Dietrich Epp
En lugar de Destroy () ing GameObject deberías generar un error.
Doodlemeat
2
Posiblemente. Es posible que desee iniciar sesión, pero no creo que deba generar un error, a menos que esté tratando de hacer algo muy específico. Hay muchos casos en los que me puedo imaginar que generar un error en realidad causaría más problemas de los que solucionaría.
PearsonArtPhoto
Es posible que desee tener en cuenta que MonoBehaviour está escrito con la ortografía británica por Unity ("MonoBehavior" no compilará; lo hago todo el tiempo); de lo contrario, este es un código decente.
Michael Eric Oberlin
Sé que llego tarde, pero solo quería señalar que el singleton de esta respuesta no sobrevive a una recarga del editor, porque la Instancepropiedad estática se borra. Un ejemplo de uno que no se puede encontrar en una de las respuestas a continuación , o wiki.unity3d.com/index.php/Singleton (que podría estar desactualizado, pero parece funcionar a partir de mi experimentación con él)
Jakub Arnold
24

Aquí hay un resumen rápido:

                 Create object   Removes scene   Global    Keep across
               if not in scene?   duplicates?    access?   Scene loads?

Method 1              No              No           Yes        Yes

Method 2              Yes             No           Yes        No

PearsonArtPhoto       No              Yes          Yes        No
Method 3

Entonces, si todo lo que le importa es el acceso global, los tres le dan lo que necesita. El uso del patrón Singleton puede ser un poco ambiguo sobre si queremos una instanciación perezosa, unicidad forzada o acceso global, así que asegúrese de pensar cuidadosamente por qué está buscando el singleton y elija una implementación que obtenga esas características correctas, en lugar de que usar un estándar para los tres cuando solo necesitas uno.

(por ejemplo, si mi juego siempre tendrá un GameManager, tal vez no me preocupe la instanciación perezosa, tal vez sea solo el acceso global con existencia garantizada y singularidad lo que me importa), en cuyo caso una clase estática me ofrece exactamente esas características de manera muy concisa, sin consideraciones de carga de escena)

... pero definitivamente no use el Método 1 como está escrito. El Find se puede omitir más fácilmente con el enfoque Awake () del Método 2/3, y si mantenemos al administrador en varias escenas, es muy probable que deseemos matar duplicados, en caso de que alguna vez carguemos entre dos escenas con un administrador ya en ellas.

DMGregory
fuente
1
Nota: debería ser posible combinar los tres métodos para crear un cuarto método que tenga las cuatro características.
Draco18s
3
El objetivo de esta respuesta no es "debe buscar una implementación Singleton que lo haga todo", sino más bien "debe identificar qué características realmente desea de este singleton y elegir una implementación que ofrezca esas características, incluso si esa implementación es no es un singleton en absoluto "
DMGregory
Ese es un buen punto DMGregory. No era realmente mi intención sugerir "aplastarlo todo", sino que "nada de estas características les impide trabajar juntos en una sola clase". es decir, "La idea central de esta respuesta no es sugerir elegir uno. "
Draco18s
17

La mejor implementación de un Singletonpatrón genérico para Unity que conozco es (por supuesto) el mío.

Puede hacer todo , y lo hace de manera ordenada y eficiente :

Create object        Removes scene        Global access?               Keep across
if not in scene?     duplicates?                                       Scene loads?

     Yes                  Yes                  Yes                     Yes (optional)

Otras ventajas:

  • Es a prueba de hilos .
  • Evita errores relacionados con la adquisición (creación) de instancias singleton cuando la aplicación se cierra al garantizar que no se puedan crear singletons después OnApplicationQuit(). (Y lo hace con una sola bandera global, en lugar de que cada tipo de singleton tenga la suya propia)
  • Utiliza la Actualización Mono de Unity 2017 (aproximadamente equivalente a C # 6). (Pero se puede adaptar fácilmente para la versión antigua)
  • ¡Viene con algunos dulces gratis!

Y porque compartir es cariñoso , aquí está:

public abstract class Singleton<T> : Singleton where T : MonoBehaviour
{
    #region  Fields
    [CanBeNull]
    private static T _instance;

    [NotNull]
    // ReSharper disable once StaticMemberInGenericType
    private static readonly object Lock = new object();

    [SerializeField]
    private bool _persistent = true;
    #endregion

    #region  Properties
    [NotNull]
    public static T Instance
    {
        get
        {
            if (Quitting)
            {
                Debug.LogWarning($"[{nameof(Singleton)}<{typeof(T)}>] Instance will not be returned because the application is quitting.");
                // ReSharper disable once AssignNullToNotNullAttribute
                return null;
            }
            lock (Lock)
            {
                if (_instance != null)
                    return _instance;
                var instances = FindObjectsOfType<T>();
                var count = instances.Length;
                if (count > 0)
                {
                    if (count == 1)
                        return _instance = instances[0];
                    Debug.LogWarning($"[{nameof(Singleton)}<{typeof(T)}>] There should never be more than one {nameof(Singleton)} of type {typeof(T)} in the scene, but {count} were found. The first instance found will be used, and all others will be destroyed.");
                    for (var i = 1; i < instances.Length; i++)
                        Destroy(instances[i]);
                    return _instance = instances[0];
                }

                Debug.Log($"[{nameof(Singleton)}<{typeof(T)}>] An instance is needed in the scene and no existing instances were found, so a new instance will be created.");
                return _instance = new GameObject($"({nameof(Singleton)}){typeof(T)}")
                           .AddComponent<T>();
            }
        }
    }
    #endregion

    #region  Methods
    private void Awake()
    {
        if (_persistent)
            DontDestroyOnLoad(gameObject);
        OnAwake();
    }

    protected virtual void OnAwake() { }
    #endregion
}

public abstract class Singleton : MonoBehaviour
{
    #region  Properties
    public static bool Quitting { get; private set; }
    #endregion

    #region  Methods
    private void OnApplicationQuit()
    {
        Quitting = true;
    }
    #endregion
}
//Free candy!
XenoRo
fuente
Esto es bastante sólido. Viniendo de un fondo de programación y un fondo que no es de Unity, ¿puede explicar por qué el singleton no se administra en el constructor en lugar del método Despertar? Probablemente se pueda imaginar que para cualquier desarrollador por ahí, ver un Singleton forzado fuera de un constructor es un levantador de cejas ...
netpoetica
1
@netpoetica simple. La unidad no admite constructores. Es por eso que no ves que los constructores se usen en ninguna clase heredada MonoBehaviour, y creo que cualquier clase utilizada por Unity directamente en general.
XenoRo
No estoy seguro de seguir cómo utilizar esto. ¿Se supone que esto sea simplemente el padre de la clase en cuestión? Después de declarar SampleSingletonClass : Singleton, SampleSingletonClass.Instancevuelve con SampleSingletonClass does not contain a definition for Instance.
Ben I.
@BenI. Tienes que usar la Singleton<>clase genérica . Es por eso que el genérico es un hijo de la Singletonclase base .
XenoRo
¡Oh por supuesto! Es bastante obvio. No estoy seguro de por qué no vi eso. = /
Ben I.
6

Solo me gustaría agregar que puede ser útil llamar DontDestroyOnLoadsi desea que su singleton persista en las escenas.

public class Singleton : MonoBehaviour
{ 
    private static Singleton _instance;

    public static Singleton Instance 
    { 
        get { return _instance; } 
    } 

    private void Awake() 
    { 
        if (_instance != null && _instance != this) 
        { 
            Destroy(this.gameObject);
            return;
        }

        _instance = this;
        DontDestroyOnLoad(this.gameObject);
    } 
}
zcabjro
fuente
Eso es muy útil. Estaba a punto de publicar un comentario en la respuesta de @ PearsonArtPhoto para hacer esta pregunta exacta:]
CaptainRedmuff
5

Otra opción podría ser dividir la clase en dos partes: una clase estática regular para el componente Singleton y un MonoBehaviour que actúa como controlador para la instancia de singleton. De esta manera, tiene control total sobre la construcción del singleton, y persistirá en todas las escenas. Esto también le permite agregar controladores a cualquier objeto que pueda necesitar los datos del singleton, en lugar de tener que buscar en la escena para encontrar un componente en particular.

public class Singleton{
    private Singleton(){
        //Class initialization goes here.
    }

    public void someSingletonMethod(){
        //Some method that acts on the Singleton.
    }

    private static Singleton _instance;
    public static Singleton Instance 
    { 
        get { 
            if (_instance == null)
                _instance = new Singleton();
            return _instance; 
        }
    } 
}

public class SingletonController: MonoBehaviour{
   //Create a local reference so that the editor can read it.
   public Singleton instance;
   void Awake(){
       instance = Singleton.Instance;
   }
   //You can reference the singleton instance directly, but it might be better to just reflect its methods in the controller.
   public void someMethod(){
       instance.someSingletonMethod();
   }
} 
Mr.Underhill89
fuente
¡Esto esta muy bien!
CaptainRedmuff
1
Tengo problemas para comprender este método, ¿puede ampliar un poco más sobre este tema? Gracias.
hex
3

Aquí está mi implementación de una clase abstracta singleton a continuación. Así es como se compara con los 4 criterios

             Create object   Removes scene   Global    Keep across
           if not in scene?   duplicates?    access?   Scene loads?

             No (but why         Yes           Yes        Yes
             should it?)

Tiene un par de otras ventajas en comparación con algunos de los otros métodos aquí:

  • No usa FindObjectsOfTypecuál es un asesino de rendimiento
  • Es flexible porque no necesita crear un nuevo objeto de juego vacío durante el juego. Simplemente agréguelo en el editor (o durante el juego) al objeto de juego que elija.
  • Es hilo seguro

    using System.Collections.Generic;
    using System.Linq;
    using UnityEngine;
    
    public abstract class Singleton<T> : MonoBehaviour where T : Singleton<T>
    {
        #region  Variables
        protected static bool Quitting { get; private set; }
    
        private static readonly object Lock = new object();
        private static Dictionary<System.Type, Singleton<T>> _instances;
    
        public static T Instance
        {
            get
            {
                if (Quitting)
                {
                    return null;
                }
                lock (Lock)
                {
                    if (_instances == null)
                        _instances = new Dictionary<System.Type, Singleton<T>>();
    
                    if (_instances.ContainsKey(typeof(T)))
                        return (T)_instances[typeof(T)];
                    else
                        return null;
                }
            }
        }
    
        #endregion
    
        #region  Methods
        private void OnEnable()
        {
            if (!Quitting)
            {
                bool iAmSingleton = false;
    
                lock (Lock)
                {
                    if (_instances == null)
                        _instances = new Dictionary<System.Type, Singleton<T>>();
    
                    if (_instances.ContainsKey(this.GetType()))
                        Destroy(this.gameObject);
                    else
                    {
                        iAmSingleton = true;
    
                        _instances.Add(this.GetType(), this);
    
                        DontDestroyOnLoad(gameObject);
                    }
                }
    
                if(iAmSingleton)
                    OnEnableCallback();
            }
        }
    
        private void OnApplicationQuit()
        {
            Quitting = true;
    
            OnApplicationQuitCallback();
        }
    
        protected abstract void OnApplicationQuitCallback();
    
        protected abstract void OnEnableCallback();
        #endregion
    }
aBertrand
fuente
Puede ser una pregunta tonta, pero ¿por qué hiciste el OnApplicationQuitCallbacky OnEnableCallbackcomo en abstractlugar de solo virtualmétodos vacíos ? Al menos en mi caso, no tengo ninguna lógica para salir / habilitar y tener una anulación vacía se siente sucio. Pero podría estar perdiendo algo.
Jakub Arnold
@JakubArnold No he visto esto en mucho tiempo, pero a primera vista parece que tienes razón, sería mejor como métodos virtuales
aBertrand
@JakubArnold En realidad, creo recordar lo que pensé en aquel entonces: quería darme cuenta de quienes usaron esto como un componente que podrían usar OnApplicationQuitCallbacky OnEnableCallback: tenerlo como métodos virtuales lo hace menos obvio. Tal vez un pensamiento un poco extraño, pero por lo que recuerdo, ese era mi racional.
aBertrand
2

En realidad, hay una forma pseudo oficial de usar Singleton en Unity. Aquí está la explicación, básicamente crea una clase Singleton y haz que tus scripts hereden de esa clase.

Alakanu
fuente
Evite las respuestas de solo enlace, al incluir en su respuesta al menos un resumen de la información que espera que los lectores obtengan del enlace. De esa manera, si el enlace no está disponible, la respuesta sigue siendo útil.
DMGregory
2

Voy a cambiar mi implementación también para las generaciones futuras.

void Awake()
    {
        if (instance == null)
            instance = this;
        else if (instance != this)
            Destroy(gameObject.GetComponent(instance.GetType()));
        DontDestroyOnLoad(gameObject);
    }

Para mí, esta línea Destroy(gameObject.GetComponent(instance.GetType()));es muy importante porque una vez que había dejado un script singleton en otro GameObject en una escena y todo el objeto del juego estaba siendo eliminado. Esto solo destruirá el componente si ya existe.

Uri Popov
fuente
1

Escribí una clase singleton que facilita la creación de objetos singleton. Es un script MonoBehaviour, por lo que puede usar las Coroutines. Se basa en esto artículo de Unity Wiki , y agregaré la opción para crearlo desde Prefab más adelante.

Por lo tanto, no necesita escribir los códigos Singleton. Simplemente descargue esta clase base Singleton.cs , agréguela a su proyecto y cree su singleton extendiéndola:

public class MySingleton : Singleton<MySingleton> {
  protected MySingleton () {} // Protect the constructor!

  public string globalVar;

  void Awake () {
      Debug.Log("Awoke Singleton Instance: " + gameObject.GetInstanceID());
  }
}

Ahora su clase MySingleton es singleton, y puede llamarla por Instancia:

MySingleton.Instance.globalVar = "A";
Debug.Log ("globalVar: " + MySingleton.Instance.globalVar);

Aquí hay un tutorial completo: http://www.bivis.com.br/2016/05/04/unity-reusable-singleton-tutorial/

Bivis
fuente