Ricerca…


Osservazioni

Mentre ci sono scuole di pensiero che fanno argomentazioni convincenti per cui l'uso non vincolato di Singletons è una cattiva idea, ad esempio Singleton su gameprogrammingpatterns.com , ci sono occasioni in cui potresti voler mantenere un GameObject in Unity su più Scene (ad esempio per musica di sottofondo senza interruzioni) assicurando che non esista più di una istanza; un caso d'uso perfetto per un Singleton.

Aggiungendo questo script a un oggetto GameObject, una volta che è stato istanziato (ad esempio includendolo ovunque in una scena) rimarrà attivo in tutte le scene e solo un'istanza sarà mai esistita.


Le istanze ScriptableObject ( UnityDoc ) forniscono un'alternativa valida a Singletons per alcuni casi d'uso. Anche se non impongono implicitamente la regola dell'istanza singola, mantengono il loro stato tra le scene e giocano bene con il processo di serializzazione Unity. Promuovono anche Inversion of Control mentre le dipendenze vengono iniettate tramite l'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();
    }
}

Ulteriori letture

Implementazione utilizzando RuntimeInitializeOnLoadMethodAttribute

Da Unity 5.2.5 è possibile utilizzare RuntimeInitializeOnLoadMethodAttribute per eseguire la logica di inizializzazione bypassando l'ordine di esecuzione di MonoBehaviour . Fornisce un modo per creare un'implementazione più pulita e 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);
    }
}

L'ordine di esecuzione risultante:

  1. GameDirector.OnRuntimeMethodLoad() avviato ...
  2. GameDirector.Awake()
  3. GameDirector.OnRuntimeMethodLoad() completato.
  4. OtherMonoBehaviour1.Awake()
  5. OtherMonoBehaviour2.Awake() , ecc.

Un semplice Singleton MonoBehaviour in Unity C #

In questo esempio, un'istanza statica privata della classe viene dichiarata all'inizio.

Il valore di un campo statico è condiviso tra le istanze, quindi se viene creata una nuova istanza di questa classe, if troverà un riferimento al primo oggetto Singleton, distruggendo la nuova istanza (o il suo oggetto di gioco).

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

}

Advanced Unity Singleton

Questo esempio combina più varianti di singleton MonoBehaviour trovati su Internet in uno e consente di modificarne il comportamento in base ai campi statici globali.

Questo esempio è stato testato utilizzando Unity 5. Per utilizzare questo singleton, è sufficiente estenderlo come segue: public class MySingleton : Singleton<MySingleton> {} . Potrebbe anche essere necessario sostituire AwakeSingleton per usarlo al posto del solito Awake . Per ulteriori modifiche, modificare i valori predefiniti dei campi statici come descritto di seguito.


  1. Questa implementazione utilizza l'attributo DisallowMultipleComponent per mantenere un'istanza per GameObject.
  2. Questa classe è astratta e può essere estesa solo Contiene anche un metodo virtuale AwakeSingleton che deve essere sovrascritto anziché implementare il normale Awake .
  3. Questa implementazione è thread-safe.
  4. Questo singleton è ottimizzato. Utilizzando il flag instantiated invece del controllo nullo dell'istanza, evitiamo il sovraccarico derivante dall'implementazione dell'operatore == di Unity. ( Leggi di più )
  5. Questa implementazione non consente alcuna chiamata all'istanza singleton quando sta per essere distrutta da Unity.
  6. Questo singleton viene fornito con le seguenti opzioni:
  • FindInactive : se cercare altre istanze di componenti dello stesso tipo associate a GameObject inattivo.
  • Persist : se mantenere il componente vivo tra le scene.
  • DestroyOthers : se distruggere qualsiasi altro componente dello stesso tipo e mantenerne uno solo.
  • Lazy : se impostare l'istanza singleton "al volo" (in Awake ) o solo "su richiesta" (quando viene chiamato 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;
    }
}

Implementazione di Singleton attraverso la classe base

Nei progetti che presentano più classi di singleton (come spesso accade), può essere pulito e comodo astrarre il comportamento di singleton in una classe di 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 può quindi implementare il modello singleton estendendo MonoBehaviourSingleton. Questo approccio consente di utilizzare il modello con un ingombro minimo sul Singleton stesso:

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

Si noti che uno dei vantaggi del modello singleton è che si può accedere staticamente a un riferimento all'istanza:

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

Tenete a mente però, questa pratica dovrebbe essere minimizzata al fine di ridurre l'accoppiamento. Questo approccio ha anche un leggero costo di prestazioni dovuto all'uso del dizionario, ma poiché questa raccolta può contenere solo un'istanza di ogni classe di singleton, il compromesso in termini di principio di DRY (non ripetersi), leggibilità e la convenienza è piccola.

Singleton Pattern che utilizza il sistema Entity-Component di Unitys

L'idea di base è usare GameObjects per rappresentare singleton, che ha molteplici vantaggi:

  • Mantiene la complessità al minimo ma supporta concetti come l'iniezione di dipendenza
  • Singletons hanno un normale ciclo di vita di Unity come parte del sistema Entity-Component
  • Singletons può essere pigro caricato e memorizzato nella cache dove necessario (ad esempio nei cicli di aggiornamento)
  • Nessun campo statico necessario
  • Non è necessario modificare MonoBehaviours / Componenti esistenti per usarli come Singletons
  • Facile da resettare (basta distruggere il Singletons GameObject), sarà pigro nuovamente caricato al prossimo utilizzo
  • Facile da iniettare mock (basta inizializzarlo con la simulazione prima di usarlo)
  • Ispezione e configurazione usando l'editor di Unity normale e possono accadere già nel momento dell'editor ( Schermata di un Singleton accessibile nell'editor di Unity )

Test.cs (che utilizza l'esempio 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 (che contiene un esempio e la classe Singleton effettiva):

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

I due metodi di estensione per GameObject sono utili anche in altre situazioni, se non ne hai bisogno spostali all'interno della classe Singleton e rendili privati.

Classe Singleton basata su MonoBehaviour & ScriptableObject

La maggior parte degli esempi Singleton usa MonoBehaviour come classe base. Il principale svantaggio è che questa classe Singleton vive solo durante il periodo di esecuzione. Questo ha alcuni inconvenienti:

  • Non c'è modo di modificare direttamente i campi Singleton oltre a modificare il codice.
  • Nessun modo per memorizzare un riferimento ad altre risorse su Singleton.
  • Impossibile impostare il singleton come destinazione di un evento UI Unity. Finisco per utilizzare ciò che chiamo "Componenti proxy" che la sua unica proposta è di avere 1 metodi di linea che chiamano "GameManager.Instance.SomeGlobalMethod ()".

Come notato nelle osservazioni, esistono implementazioni che tentano di risolvere questo problema utilizzando ScriptableObjects come classe base, ma perdono i benefici del tempo di esecuzione del MonoBehaviour. Questa implementazione risolve questi problemi utilizzando un oggetto ScriptableObject come classe base e un MonoBehavior associato durante il runtime:

  • È un bene quindi le sue proprietà possono essere aggiornate sull'editor come qualsiasi altra risorsa Unity.
  • Funziona bene con il processo di serializzazione Unity.
  • È possibile assegnare riferimenti al singleton ad altre risorse dall'editor (le dipendenze vengono iniettate tramite l'editor).
  • Gli eventi Unity possono chiamare direttamente i metodi su Singleton.
  • Puoi chiamarlo da qualsiasi posizione nella base di codice usando "SingletonClassName.Instance"
  • Ha accesso agli eventi e ai metodi del tempo di esecuzione di MonoBehaviour come: Aggiornamento, Sveglia, Inizio, FixedUpdate, StartCoroutine, ecc.
/************************************************************
 * 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() { }
    }
}

Qui c'è un esempio di classe Singleton di GameManager che utilizza SingletonScriptableObject (con molti commenti):

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
Autorizzato sotto CC BY-SA 3.0
Non affiliato con Stack Overflow