Szukaj…


Uwagi

Chociaż istnieją szkoły myślenia, które dostarczają przekonujących argumentów, dlaczego nieskrępowane używanie Singletonów jest złym pomysłem, np. Singleton na gameprogrammingpatterns.com , zdarzają się sytuacje, w których możesz chcieć utrwalić GameObject w Unity na wielu scenach (np. Dla płynnej muzyki w tle) zapewniając jednocześnie, że nie może istnieć więcej niż jedna instancja; idealny przypadek użycia dla Singletona.

Dodając ten skrypt do GameObject, po utworzeniu instancji (np. Poprzez umieszczenie go w dowolnym miejscu w Scenie) pozostanie on aktywny we wszystkich Scenach i tylko jedna instancja będzie istnieć.


Instancje ScriptableObject ( UnityDoc ) stanowią poprawną alternatywę dla Singletonów w niektórych przypadkach użycia. Chociaż nie wymuszają domyślnie reguły pojedynczej instancji, zachowują swój stan między scenami i ładnie grają w proces serializacji Unity. Promują także odwracanie kontroli, ponieważ zależności są wprowadzane przez edytor .

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

Dalsza lektura

Implementacja za pomocą RuntimeInitializeOnLoadMethodAttribute

Od Unity 5.2.5 można użyć RuntimeInitializeOnLoadMethodAttribute do wykonania logiki inicjalizacji z pominięciem kolejności wykonywania MonoBehaviour . Zapewnia sposób na stworzenie bardziej przejrzystej i niezawodnej implementacji:

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

Wynikająca kolejność wykonania:

  1. GameDirector.OnRuntimeMethodLoad() rozpoczęło się ...
  2. GameDirector.Awake()
  3. GameDirector.OnRuntimeMethodLoad() zakończone.
  4. OtherMonoBehaviour1.Awake()
  5. OtherMonoBehaviour2.Awake() itp.

Prosty Singleton MonoBehaviour w Unity C #

W tym przykładzie na początku deklarowana jest prywatna instancja statyczna klasy.

Wartość pola statycznego jest współdzielona między instancjami, więc jeśli nowa instancja tej klasy zostanie utworzona, if znajdzie odniesienie do pierwszego obiektu Singleton, niszcząc nową instancję (lub jej obiekt gry).

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

Ten przykład łączy wiele wariantów singletonów MonoBehaviour znalezionych w Internecie w jeden i pozwala zmienić jego zachowanie w zależności od globalnych pól statycznych.

Ten przykład został przetestowany przy użyciu Unity 5. Aby użyć tego singletona, wystarczy go rozszerzyć w następujący sposób: public class MySingleton : Singleton<MySingleton> {} . Może być także konieczne zastąpienie funkcji AwakeSingleton aby użyć jej zamiast zwykłej funkcji Awake . W celu dalszego ulepszenia zmień domyślne wartości pól statycznych, jak opisano poniżej.


  1. Ta implementacja wykorzystuje atrybut DisallowMultipleComponent, aby zachować jedną instancję na GameObject.
  2. Ta klasa jest abstrakcyjna i można ją tylko rozszerzyć. Zawiera również jeden wirtualny metoda AwakeSingleton który musi być zastąpiona zamiast wdrażania normalne Awake .
  3. Ta implementacja jest bezpieczna dla wątków.
  4. Ten singleton jest zoptymalizowany. Używając flagi instantiated zamiast sprawdzania zerowego instancji, unikamy narzutu związanego z implementacją operatora == przez Unity. ( Czytaj więcej )
  5. Ta implementacja nie zezwala na żadne wywołania instancji singletonu, gdy ma zostać zniszczona przez Unity.
  6. Ten singleton zawiera następujące opcje:
  • FindInactive : czy szukać innych wystąpień komponentów tego samego typu dołączonych do nieaktywnego GameObject.
  • Persist : czy zachować komponent przy życiu między scenami.
  • DestroyOthers : czy zniszczyć inne komponenty tego samego typu i zachować tylko jeden.
  • Lazy : czy ustawić instancję singletona „w locie” (w Awake ) czy tylko „na żądanie” (gdy wywoływany jest 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;
    }
}

Implementacja singletonu poprzez klasę podstawową

W projektach obejmujących kilka klas singletonów (jak to często bywa), może być czyste i wygodne wyodrębnienie zachowania singletonów do klasy podstawowej:

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

MonoBehaviour może następnie wdrożyć wzór singletonu poprzez rozszerzenie MonoBehaviourSingleton. Takie podejście pozwala na wykorzystanie wzorca przy minimalnej powierzchni samego Singletona:

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

Zauważ, że jedną z zalet wzorca singletonu jest to, że dostęp do odwołania do instancji można uzyskać statycznie:

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

Należy jednak pamiętać, że tę praktykę należy zminimalizować w celu zmniejszenia sprzężenia. Takie podejście wiąże się również z niewielkim kosztem wydajności wynikającym z użycia słownika, ale ponieważ ta kolekcja może zawierać tylko jedno wystąpienie każdej klasy singleton, kompromis pod względem zasady DRY (Don't Repeat Yourself), czytelności i wygoda jest niewielka.

Singleton Pattern wykorzystujący system Unitys Entity-Component

Podstawową ideą jest użycie GameObjects do reprezentowania singletonów, co ma wiele zalet:

  • Ogranicza złożoność do minimum, ale obsługuje pojęcia takie jak wstrzykiwanie zależności
  • Singletony mają normalny cykl życia Jedności jako część systemu Entity-Component
  • Singletony mogą być leniwie ładowane i buforowane lokalnie tam, gdzie jest to normalnie potrzebne (np. W pętlach aktualizacji)
  • Nie są wymagane pola statyczne
  • Nie trzeba modyfikować istniejących MonoBehaviours / Components, aby używać ich jako singletonów
  • Łatwy do zresetowania (wystarczy zniszczyć Singletons GameObject), będzie leniwie ładowany ponownie przy następnym użyciu
  • Łatwe wstrzykiwanie makiet (po prostu zainicjuj je za pomocą makiety przed użyciem)
  • Kontrola i konfiguracja przy użyciu normalnego edytora Unity i może się zdarzyć już w czasie edytora ( zrzut ekranu singletonu dostępny w edytorze Unity )

Test.cs (który wykorzystuje przykładowy 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 (który zawiera przykład i faktyczną klasę Singleton):

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

Dwie metody rozszerzenia GameObject są również pomocne w innych sytuacjach, jeśli nie potrzebujesz ich, przenieś je do klasy Singleton i ustaw jako prywatne.

Klasa Singleton oparta na MonoBehaviour i ScriptableObject

Większość przykładów Singleton używa MonoBehaviour jako klasy podstawowej. Główną wadą jest to, że ta klasa Singleton żyje tylko w czasie wykonywania. Ma to pewne wady:

  • Nie ma innej możliwości bezpośredniej edycji pól singletonów niż zmiana kodu.
  • Nie ma możliwości przechowywania odniesienia do innych zasobów w Singleton.
  • Nie ma możliwości ustawienia singletonu jako miejsca docelowego zdarzenia interfejsu Unity. Skończyło się na tym, co nazywam „komponentami proxy”, których jedyną propozycją jest posiadanie 1-liniowych metod, które nazywają „GameManager.Instance.SomeGlobalMethod ()”.

Jak zauważono w uwagach, istnieją implementacje, które próbują rozwiązać ten problem za pomocą ScriptableObjects jako klasy podstawowej, ale tracą korzyści płynące z MonoBehaviour w czasie wykonywania. Ta implementacja rozwiązuje te problemy za pomocą ScriptableObject jako klasy bazowej i powiązanego MonoBehavior w czasie wykonywania:

  • Jest to zasób, więc jego właściwości można aktualizować w edytorze, jak każdy inny zasób Unity.
  • Świetnie współgra z procesem serializacji Unity.
  • Możliwe jest przypisanie referencji na singletonie do innych zasobów z edytora (zależności są wprowadzane przez edytor).
  • Zdarzenia Unity mogą bezpośrednio wywoływać metody w Singleton.
  • Można go wywołać z dowolnego miejsca w bazie kodu przy użyciu „SingletonClassName.Instance”
  • Ma dostęp do zdarzeń i metod MonoBehaviour w czasie wykonywania, takich jak: Aktualizacja, Przebudź, Start, FixedUpdate, StartCoroutine itp.
/************************************************************
 * 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() { }
    }
}

Oto przykładowa klasa singleton GameManager wykorzystująca SingletonScriptableObject (z dużą ilością komentarzy):

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
Licencjonowany na podstawie CC BY-SA 3.0
Nie związany z Stack Overflow