In Unity, come posso implementare correttamente il modello singleton?


36

Ho visto diversi video ed esercitazioni per la creazione di oggetti singleton in Unity, principalmente per a GameManager , che sembrano utilizzare approcci diversi per istanziare e convalidare un singleton.

Esiste un approccio corretto, o meglio, preferito a questo?

I due esempi principali che ho riscontrato sono:

Primo

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);
    }
}

Secondo

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 differenza principale che posso vedere tra i due è:

Il primo approccio proverà a navigare nello stack degli oggetti di gioco per trovare un'istanza della GameManagerquale, anche se ciò accade (o dovrebbe accadere solo) una volta sembra che potrebbe essere molto non ottimizzato mentre le scene aumentano di dimensioni durante lo sviluppo.

Inoltre, il primo approccio indica che l'oggetto non deve essere eliminato quando l'applicazione cambia scena, il che assicura che l'oggetto sia persistente tra le scene. Il secondo approccio non sembra aderire a questo.

Il secondo approccio sembra strano come nel caso in cui l'istanza sia nulla nel getter, creerà un nuovo GameObject e gli assegnerà un componente GameManger. Tuttavia, questo non può essere eseguito senza prima avere questo componente GameManager già collegato a un oggetto nella scena, quindi questo mi sta creando confusione.

Ci sono altri approcci che potrebbero essere raccomandati o un ibrido dei due precedenti? Ci sono molti video e tutorial riguardanti i singoli, ma differiscono tutti così tanto che è difficile trarre un confronto tra i due e quindi una conclusione su quale sia l'approccio migliore / preferito.


Cosa dovrebbe fare GameManager? Deve essere un GameObject?
Bummzack,

1
Non è davvero una questione di cosa GameManagerdovrebbe fare, piuttosto come garantire che ci sia sempre e solo un'istanza dell'oggetto e il modo migliore per applicarlo.
CaptainRedmuff,

questo tutorial ha spiegato molto bene, come implementare singleton unitygeek.com/unity_c_singleton , spero sia utile
Rahul Lalit

Risposte:


30

Dipende, ma di solito uso un terzo metodo. Il problema con i metodi che hai usato è che nel caso in cui l'oggetto sia incluso per cominciare, non li rimuoverà dall'albero e possono ancora essere creati istanziando troppe chiamate, il che potrebbe rendere le cose davvero confuse.

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;
        }
    }
}

Il problema con entrambe le implementazioni è che non distruggono un oggetto creato in seguito. Potrebbe funzionare, ma si potrebbe lanciare una chiave inglese nelle opere che potrebbe causare un errore di debug molto difficile lungo la linea. Assicurati di controllare su Attiva se esiste già un'istanza e, in tal caso, distruggere la nuova istanza.


2
Potresti anche volere OnDestroy() { if (this == _instance) { _instance = null; } }, se vuoi avere un'istanza diversa in ogni scena.
Dietrich Epp,

Invece di Destroy () ing GameObject dovresti generare un errore.
Doodlemeat,

2
Possibilmente. Potresti voler registrarlo, ma non credo che dovresti generare un errore, a meno che tu non stia provando a fare qualcosa di molto specifico. Ci sono molti casi in cui posso immaginare che sollevare un errore causerebbe effettivamente più problemi di quanti ne avrebbe risolti.
PearsonArtPhoto,

Potresti voler notare che MonoBehaviour è scritto con l'ortografia britannica di Unity ("MonoBehavior" non verrà compilato - lo faccio sempre); altrimenti, questo è un codice decente.
Michael Eric Oberlin,

So che sto arrivando tardi, ma volevo solo sottolineare che il singleton di questa risposta non sopravvive a un ricaricamento dell'editor, perché la Instanceproprietà statica viene cancellata. Un esempio di uno che non può essere trovato in una delle risposte di seguito o wiki.unity3d.com/index.php/Singleton (che potrebbe essere obsoleto, ma sembra funzionare dal mio esperimento con esso)
Jakub Arnold,

24

Ecco un breve riassunto:

                 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

Quindi, se tutto ciò che ti interessa è l'accesso globale, tutti e tre ti danno ciò di cui hai bisogno. L'uso del modello Singleton può essere un po 'ambiguo sul fatto che desideriamo un'istanza pigra, unicità forzata o accesso globale, quindi assicurati di pensare attentamente al motivo per cui stai raggiungendo il singleton e scegli un'implementazione che renda tali funzionalità giuste, piuttosto che usare uno standard per tutti e tre quando ne hai solo bisogno.

(ad es. se il mio gioco avrà sempre un GameManager, forse non mi interessa l'istanza pigra - forse è solo l'accesso globale con esistenza e unicità garantite a cui tengo - nel qual caso una classe statica mi dà esattamente quelle caratteristiche in modo molto conciso, senza considerazioni sul caricamento delle scene)

... ma sicuramente non usare il Metodo 1 come scritto. La ricerca può essere saltata più facilmente con l'approccio Awake () di Method2 / 3, e se stiamo mantenendo il manager attraverso le scene, molto probabilmente vorremmo uccidere i duplicati, nel caso in cui caricassimo mai tra due scene con un manager già in esse.


1
Nota: dovrebbe essere possibile combinare tutti e tre i metodi per creare un quarto metodo con tutte e quattro le funzionalità.
Draco18s,

3
Il filo conduttore di questa risposta non è "dovresti cercare un'implementazione di Singleton che faccia tutto", ma piuttosto "dovresti identificare quali funzionalità vuoi effettivamente da questo singleton, e scegliere un'implementazione che offra quelle caratteristiche - anche se l'implementazione è non è un singleton "
DMGregory

È un buon punto DMGregory. Non era proprio il mio intento di suggerire di "distruggere tutto insieme", ma quel "nulla di queste caratteristiche che impedisce loro di lavorare insieme in una singola classe". vale a dire "La spinta di questa risposta NON è quella di suggerire di sceglierne una. "
Draco18s,

17

La migliore implementazione di un Singletonmodello generico per Unity che conosco è (ovviamente) la mia.

Può fare tutto e lo fa in modo ordinato ed efficiente :

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

     Yes                  Yes                  Yes                     Yes (optional)

Altri vantaggi:

  • È sicuro per i thread .
  • Evita i bug relativi all'acquisizione (creazione) di istanze singleton quando l'applicazione viene chiusa assicurando che dopo non si possano creare singleton OnApplicationQuit(). (E lo fa con una singola bandiera globale, invece che ogni singolo tipo abbia il proprio)
  • Utilizza l'aggiornamento Mono di Unity 2017 (approssimativamente equivalente a C # 6). (Ma può essere facilmente adattato per la versione antica)
  • Viene fornito con alcune caramelle gratis!

E poiché la condivisione è premurosa , eccola:

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!

Questo è abbastanza solido. Provenendo da un background di programmazione e non di Unity, puoi spiegare perché il singleton non è gestito nel costruttore piuttosto che nel metodo Awake? Probabilmente puoi immaginare che per qualsiasi sviluppatore là fuori, vedere un Singleton imposto all'esterno di un costruttore è un
innalzatore di

1
@netpoetica Semplice. Unity non supporta i costruttori. Ecco perché non vedi costruttori utilizzati in alcuna classe ereditaria MonoBehavioure credo che qualsiasi classe utilizzata da Unity direttamente in generale.
XenoRo

Non sono sicuro di seguire come utilizzare questo. Questo vuole essere semplicemente il genitore della classe in questione? Dopo aver dichiarato SampleSingletonClass : Singleton, SampleSingletonClass.Instancetorna con SampleSingletonClass does not contain a definition for Instance.
Ben I.

@BenI. Devi usare la Singleton<>classe generica . Questo è il motivo per cui il generico è un figlio della Singletonclasse base .
XenoRo il

Oh, certo! È abbastanza ovvio. Non sono sicuro del perché non l'ho visto. = /
Ben I.

6

Vorrei solo aggiungere che può essere utile chiamare DontDestroyOnLoadse vuoi che il tuo singleton persista attraverso le scene.

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);
    } 
}

È molto utile. Stavo per pubblicare un commento sulla risposta di @ PearsonArtPhoto per porre questa domanda esatta:]
CaptainRedmuff,

5

Un'altra opzione potrebbe essere quella di dividere la classe in due parti: una normale classe statica per il componente Singleton e un MonoBehaviour che funge da controller per l'istanza singleton. In questo modo hai il pieno controllo sulla costruzione del singleton e persisterà in tutte le scene. Ciò consente anche di aggiungere controller a qualsiasi oggetto che potrebbe richiedere i dati del singleton, invece di dover scavare attraverso la scena per trovare un particolare componente.

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();
   }
} 

Questo è molto carino!
CaptainRedmuff,

1
Ho difficoltà a comprendere questo metodo, puoi approfondire un po 'di più su questo argomento. Grazie.
hex

3

Ecco la mia implementazione di una classe astratta singleton di seguito. Ecco come si confronta con i 4 criteri

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

             No (but why         Yes           Yes        Yes
             should it?)

Ha un paio di altri vantaggi rispetto ad alcuni degli altri metodi qui:

  • Non usa FindObjectsOfTypequale è un killer delle prestazioni
  • È flessibile in quanto non è necessario creare un nuovo oggetto di gioco vuoto durante il gioco. Lo aggiungi semplicemente nell'editor (o durante il gioco) a un oggetto di gioco di tua scelta.
  • È thread-safe

    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
    }

Potrebbe essere una domanda sciocca, ma perché hai fatto i metodi OnApplicationQuitCallbacke OnEnableCallbackcome abstractinvece di solo vuoti virtual? Almeno nel mio caso non ho alcuna logica di chiusura / abilitazione e avere un override vuoto mi sembra sporco. Ma potrei mancare qualcosa.
Jakub Arnold,

@JakubArnold Non lo guardo da un po 'ma a prima vista sembra che tu abbia ragione, sarebbe meglio come metodi virtuali
aBertrand

@JakubArnold In realtà penso di ricordare il mio pensiero di allora: volevo rendere consapevoli coloro che lo usavano come componente che potevano usare OnApplicationQuitCallbacke OnEnableCallback: averlo come metodo virtuale in qualche modo lo rende meno ovvio. Forse un pensiero un po 'strano ma per quanto mi ricordo era il mio razionale.
Bertrand,

2

In realtà esiste un modo pseudo ufficiale di usare Singleton in Unity. Ecco la spiegazione, fondamentalmente crea una classe Singleton e fai ereditare i tuoi script da quella classe.


Evita le risposte solo al link, includendo nella tua risposta almeno un riepilogo delle informazioni che speri che i lettori trarranno dal link. In questo modo se il collegamento non diventa mai disponibile, la risposta rimane utile.
DMGregory

2

Intenderò la mia implementazione anche per le generazioni future.

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

Per me questa riga Destroy(gameObject.GetComponent(instance.GetType()));è molto importante perché una volta avevo lasciato uno script singleton su un altro giocoOggetto in una scena e l'intero oggetto del gioco veniva eliminato. Questo distruggerà il componente solo se esiste già.


1

Ho scritto una classe singleton che semplifica la creazione di oggetti singleton. È uno script MonoBehaviour, quindi puoi usare le Coroutine. È basato su questo articolo di Unity Wiki e aggiungerò l'opzione per crearlo da Prefab più tardi.

Quindi non è necessario scrivere i codici Singleton. Basta scaricare questa classe base Singleton.cs , aggiungerla al progetto e creare il tuo singleton estendendolo:

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

  public string globalVar;

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

Ora la tua classe MySingleton è una singleton e puoi chiamarla per istanza:

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

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

Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.