Sök…


Anmärkningar

Även om det finns tankekurser som gör tvingande argument varför en obegränsad användning av Singletons är en dålig idé, t.ex. Singleton på gameprogrammingpatterns.com , finns det tillfällen då du kanske vill fortsätta ett GameObject i Unity över flera scener (t.ex. för sömlös bakgrundsmusik) samtidigt som man säkerställer att inte mer än ett exempel kan existera; ett perfekt fodral för en Singleton.

Genom att lägga till detta skript till ett GameObject, så snart det har instansierats (t.ex. genom att inkludera det var som helst i en scen) kommer det att förbli aktivt över hela scener, och endast en instans kommer någonsin att existera.


ScriptableObject- instanser ( UnityDoc ) ger ett giltigt alternativ till Singletons för vissa användningsfall. Även om de inte implicit upprätthåller regeln med enstaka instanser behåller de sina tillstånd mellan scener och spelar fint med Unity-serien. De främjar också Inversion of Control eftersom beroenden injiceras genom redaktören .

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

Vidare läsning

Implementering med RuntimeInitializeOnLoadMethodAttribute

Sedan Unity 5.2.5 är det möjligt att använda RuntimeInitializeOnLoadMethodAttribute för att utföra initieringslogiken genom att kringgå MonoBehaviour exekveringsordning . Det ger ett sätt att skapa mer ren och robust implementering:

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

Den resulterande ordningsföljden:

  1. GameDirector.OnRuntimeMethodLoad() startade ...
  2. GameDirector.Awake()
  3. GameDirector.OnRuntimeMethodLoad() slutförd.
  4. OtherMonoBehaviour1.Awake()
  5. OtherMonoBehaviour2.Awake() , etc.

En enkel Singleton MonoBehaviour i Unity C #

I detta exempel förklaras en privat statisk instans av klassen i början.

Värdet på ett statiskt fält delas mellan instanser, så om en ny instans av den här klassen skapas kommer if att hitta en referens till det första Singleton-objektet och förstöra den nya instansen (eller dess spelobjekt).

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

Detta exempel kombinerar flera varianter av MonoBehaviour singletons som finns på internet till en och låter dig ändra dess beteende beroende på globala statiska fält.

Detta exempel testades med hjälp av Unity 5. För att använda detta singleton, behöver du bara förlänga det enligt följande: public class MySingleton : Singleton<MySingleton> {} . Du kan också behöva åsidosätta AwakeSingleton att använda det istället för vanligt Awake . För ytterligare justeringar, ändra standardvärden för statiska fält som beskrivs nedan.


  1. Denna implementering använder DisallowMultipleComponent- attributet för att behålla en instans per GameObject.
  2. Denna klass är abstrakt och kan bara förlängas. Den innehåller också en virtuell metod AwakeSingleton som måste åsidosättas istället för att implementera normal Awake .
  3. Denna implementering är trådsäker.
  4. Denna singleton är optimerad. Genom att använda instantiated flagga istället för instans nullkontroll undviker vi det overhead som följer med Unitys implementering av == operatör. ( Läs mer )
  5. Denna implementering tillåter inte några samtal till singleton-instansen när den håller på att bli förstörd av Unity.
  6. Denna singleton har följande alternativ:
  • FindInactive : om man ska leta efter andra instanser av komponenter av samma typ som är kopplade till inaktivt GameObject.
  • Persist : huruvida komponenten ska leva mellan scenerna.
  • DestroyOthers : om du ska förstöra andra komponenter av samma typ och bara behålla en.
  • Lazy : om man ställer in singleton-instansen "on the fly" (i Awake ) eller bara "on demand" (när getter kallas).
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;
    }
}

Singleton Implementation genom basklass

I projekt som innehåller flera singletonklasser (som ofta är fallet) kan det vara rent och bekvämt att abstrahera singletonbeteendet till en basklass:

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

En MonoBehaviour kan sedan implementera singletonmönstret genom att utöka MonoBehaviourSingleton. Denna strategi gör det möjligt att använda mönstret med ett minimalt fotavtryck på Singleton själv:

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

Observera att en av fördelarna med singletonmönstret är att en referens till instansen kan komma åt statiskt:

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

Tänk på att denna praxis bör minimeras för att minska kopplingen. Detta tillvägagångssätt kommer också till en liten prestandakostnad på grund av användningen av ordlista, men eftersom denna samling endast kan innehålla en instans av varje singleton-klass, är avvägningen i form av DRY-principen (Don't Repeat Yourself), läsbarhet och bekvämligheten är liten.

Singleton-mönster som använder Unitys Entity-Component-system

Grundidén är att använda GameObjects för att representera singletons, vilket har flera fördelar:

  • Håller komplexiteten till ett minimum men stöder begrepp som beroendeinjektion
  • Singletons har en normal Unity-livscykel som en del av Entity-Component-systemet
  • Singletons kan vara lata och laddas lokalt där det behövs regelbundet (t.ex. i uppdateringsslingor)
  • Inga statiska fält behövs
  • Inget behov av att modifiera befintliga MonoBehaviours / Components för att använda dem som Singletons
  • Lätt att återställa (förstöra Singletons GameObject), kommer att vara laddad igen vid nästa användning
  • Lätt att injicera håravfall (initiera bara det med håna innan du använder det)
  • Inspektion och konfiguration med normal Unity-redigerare och kan hända redan på redaktörens tid ( Skärmdump av en Singleton tillgänglig i Unity-redigeraren )

Test.cs (som använder exemplet 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);
    }
}

ExempelSingleton.cs (som innehåller ett exempel och den faktiska Singleton-klassen):

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

De två förlängningsmetoderna för GameObject är också användbara i andra situationer, om du inte behöver dem flytta dem in i Singleton-klassen och göra dem privata.

MonoBehaviour & ScriptableObject-baserad Singleton Class

De flesta Singleton-exempel använder MonoBehaviour som basklass. Den största nackdelen är att denna Singleton-klass bara lever under körtid. Detta har några nackdelar:

  • Det finns inget sätt att direkt redigera singleton-fälten annat än att ändra koden.
  • Inget sätt att lagra en referens till andra tillgångar på Singleton.
  • Inget sätt att ställa singleton som destination för en Unity UI-händelse. Jag slutar använda vad jag kallar "Proxy Components" som dess enda förslag är att ha 1 radmetoder som kallar "GameManager.Instance.SomeGlobalMethod ()".

Som nämnts i anmärkningarna finns det implementationer som försöker lösa detta med hjälp av ScriptableObjects som basklass men förlorar fördelarna med körtid för MonoBehaviour. Denna implementering löser problemen genom att använda ett ScriptableObject som en basklass och en tillhörande MonoBehavior under körning:

  • Det är en tillgång så dess egenskaper kan uppdateras på redigeraren som alla andra Unity-tillgångar.
  • Det spelar bra med Unity-serieringsprocessen.
  • Är det möjligt att tilldela referenser på singleton till andra tillgångar från redaktören (beroenden injiceras genom redaktören).
  • Enhetshändelser kan direkt ringa metoder på Singleton.
  • Kan kalla det var som helst i kodbasen med "SingletonClassName.Instance"
  • Har tillgång till körtid MonoBehaviour-händelser och metoder som: Uppdatera, Vakna, Start, 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() { }
    }
}

Här finns ett exempel på GameManager-singleton-klassen med SingletonScriptableObject (med många kommentarer):

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
Licensierat under CC BY-SA 3.0
Inte anslutet till Stack Overflow