Wie implementiere ich in Unity das Singleton-Muster richtig?

35

Ich habe mehrere Videos und Tutorials zum Erstellen von Singleton-Objekten in Unity gesehen, hauptsächlich für a GameManager, die unterschiedliche Ansätze zum Instanziieren und Validieren eines Singleton zu verwenden scheinen.

Gibt es einen richtigen oder eher bevorzugten Ansatz dafür?

Die beiden wichtigsten Beispiele, die ich angetroffen habe, sind:

Zuerst

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

Zweite

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

Der Hauptunterschied, den ich zwischen den beiden sehe, ist:

Der erste Ansatz versucht, im Spielobjektstapel zu navigieren, um eine Instanz der zu finden GameManager, obwohl dies nur einmal vorkommt (oder sollte), als ob es sehr unoptimiert erscheinen könnte, wenn Szenen während der Entwicklung an Größe zunehmen.

Außerdem markiert der erste Ansatz das Objekt, das nicht gelöscht werden soll, wenn die Anwendung die Szene ändert, wodurch sichergestellt wird, dass das Objekt zwischen den Szenen beibehalten wird. Der zweite Ansatz scheint dem nicht zu entsprechen.

Der zweite Ansatz erscheint seltsam, da in dem Fall, in dem die Instanz im Getter null ist, ein neues GameObject erstellt und ihm eine GameManger-Komponente zugewiesen wird. Dies kann jedoch nicht ausgeführt werden, ohne dass diese GameManager-Komponente zuvor bereits an ein Objekt in der Szene angehängt wurde, was mich verwirrt.

Gibt es andere Ansätze, die empfohlen würden, oder eine Mischung aus beiden? Es gibt viele Videos und Tutorials zu Singletons, aber sie unterscheiden sich alle so sehr, dass es schwierig ist, Vergleiche zwischen den beiden zu ziehen, und somit eine Schlussfolgerung darüber zu ziehen, welches der beste / bevorzugte Ansatz ist.

Captain Redmuff
quelle
Was soll GameManager tun? Muss es ein GameObject sein?
Bummzack
1
Es ist nicht wirklich eine Frage dessen, was getan werden GameManagersoll, sondern wie sichergestellt werden kann, dass es immer nur eine Instanz des Objekts gibt und wie dies am besten durchgesetzt werden kann.
Captain Redmuff
In diesen Tutorials wurde sehr gut erklärt, wie Singleton implementiert wird. unitygeek.com/unity_c_singleton , ich hoffe, es ist nützlich
Rahul Lalit

Antworten:

29

Das hängt davon ab, aber normalerweise verwende ich eine dritte Methode. Das Problem bei den von Ihnen verwendeten Methoden besteht darin, dass das Objekt in dem Fall, in dem es zunächst enthalten ist, nicht aus der Struktur entfernt wird. Sie können dennoch durch das Instanziieren von zu vielen Aufrufen erstellt werden, was zu Verwirrung führen kann.

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

Das Problem bei beiden Implementierungen ist, dass sie kein Objekt zerstören, das später erstellt wird. Es könnte funktionieren, aber man könnte einen Schraubenschlüssel in das Werk werfen, was zu einem sehr schwer zu behebenden Fehler führen könnte. Stellen Sie sicher, dass Sie in Awake prüfen, ob bereits eine Instanz vorhanden ist, und in diesem Fall die neue Instanz zerstören.

PearsonArtPhoto
quelle
2
Vielleicht möchten Sie auch OnDestroy() { if (this == _instance) { _instance = null; } }, wenn Sie in jeder Szene eine andere Instanz haben möchten.
Dietrich Epp
Anstatt das GameObject zu zerstören, solltest du einen Fehler auslösen.
Doodlemeat
2
Möglicherweise. Möglicherweise möchten Sie es protokollieren, aber ich denke nicht, dass Sie einen Fehler auslösen sollten, es sei denn, Sie versuchen, etwas ganz bestimmtes zu tun. Es gibt viele Fälle, in denen ich mir vorstellen kann, dass das Auslösen eines Fehlers mehr Probleme verursachen würde, als es beheben würde.
PearsonArtPhoto
Vielleicht möchten Sie wissen, dass MonoBehaviour von Unity mit der britischen Schreibweise geschrieben wird ("MonoBehavior" wird nicht kompiliert - das mache ich die ganze Zeit). Ansonsten ist dies ein anständiger Code.
Michael Eric Oberlin
Ich weiß, dass ich zu spät komme, wollte aber nur darauf hinweisen, dass der Singleton dieser Antwort ein Instanceerneutes Laden des Editors nicht überlebt, da die statische Eigenschaft gelöscht wird , oder wiki.unity3d.com/index.php/Singleton (was zwar veraltet sein könnte, aber von meinem Experimentieren damit zu funktionieren scheint)
Jakub Arnold
24

Hier ist eine kurze Zusammenfassung:

                 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

Wenn Sie sich also nur für den globalen Zugriff interessieren, erhalten Sie mit allen drei Funktionen das, was Sie benötigen. Die Verwendung des Singleton-Musters kann ein bisschen zweideutig sein, ob es sich um eine verzögerte Instanziierung, eine erzwungene Eindeutigkeit oder einen globalen Zugriff handelt. Überlegen Sie sich also genau, warum Sie nach dem Singleton greifen , und wählen Sie eine Implementierung, die diese Funktionen richtig ausführt als einen Standard für alle drei zu verwenden, wenn Sie nur einen benötigen.

(zB wenn mein Spiel immer einen GameManager haben wird, interessiert mich die verzögerte Instanziierung vielleicht nicht - vielleicht ist es nur ein globaler Zugriff mit garantierter Existenz und Einzigartigkeit, die mir wichtig sind - in diesem Fall bringt mir eine statische Klasse genau diese Funktionen sehr präzise, ohne Überlegungen zum Laden der Szene)

... aber auf keinen Fall Methode 1 wie geschrieben anwenden. Die Suche kann mit dem Awake () - Ansatz von Methode 2/3 einfacher übersprungen werden. Wenn wir den Manager über Szenen hinweg beibehalten, möchten wir höchstwahrscheinlich das Duplizieren, falls wir jemals zwischen zwei Szenen laden, in denen bereits ein Manager vorhanden ist.

DMGregory
quelle
1
Hinweis: Es sollte möglich sein, alle drei Methoden zu kombinieren, um eine vierte Methode mit allen vier Funktionen zu erstellen.
Draco18s
3
Der Kern dieser Antwort ist nicht "Sie sollten nach einer Singleton-Implementierung suchen, die alles kann", sondern "Sie sollten herausfinden, welche Funktionen Sie tatsächlich von diesem Singleton erwarten, und eine Implementierung auswählen, die diese Funktionen bereitstellt - auch wenn dies die Implementierung ist überhaupt kein Singleton "
DMGregory
Das ist ein guter Punkt, DMGregory. War nicht wirklich meine Absicht, "alles zusammenzubringen" vorzuschlagen, sondern "nichts an diesen Features, das sie daran hindert, in einer einzigen Klasse zusammenzuarbeiten". Das heißt: "Die Antwort lautet NICHT, einen auszuwählen. "
Draco18s
17

Die beste Implementierung eines generischen SingletonMusters für Unity, von dem ich weiß, ist (natürlich) meine eigene.

Es kann alles und das ordentlich und effizient :

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

     Yes                  Yes                  Yes                     Yes (optional)

Andere Vorteile:

  • Es ist threadsicher .
  • Sie vermeidet Fehler beim Erfassen (Erstellen) von Singleton-Instanzen beim Beenden der Anwendung, indem sichergestellt wird, dass Singleton-Instanzen nach dem Beenden nicht mehr erstellt werden können OnApplicationQuit(). (Und das mit einem einzigen globalen Flag, anstatt dass jeder Singleton-Typ seinen eigenen hat.)
  • Es wird das Mono-Update von Unity 2017 verwendet (entspricht in etwa C # 6). (Aber es kann leicht für die alte Version angepasst werden)
  • Es kommt mit einigen kostenlosen Süßigkeiten!

Und weil Teilen wichtig ist , ist es hier:

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
quelle
Das ist ziemlich solide. Können Sie ausgehend von einem Programmier- und einem Nicht-Unity-Hintergrund erklären, warum der Singleton nicht im Konstruktor, sondern in der Awake-Methode verwaltet wird? Sie können sich wahrscheinlich vorstellen, dass für jeden Entwickler da draußen das
Erzwingen
1
@netpoetica Einfach. Unity unterstützt keine Konstruktoren. Das ist der Grund, warum Konstruktoren in keiner Klasse verwendet werden, die erbt MonoBehaviour, und ich glaube, dass jede Klasse von Unity direkt im Allgemeinen verwendet wird.
XenoRo
Ich bin mir nicht sicher, wie ich das nutzen soll. Soll dies einfach das Elternteil der betreffenden Klasse sein? Kommt nach der Deklaration SampleSingletonClass : Singletonmit SampleSingletonClass.Instancezurück SampleSingletonClass does not contain a definition for Instance.
Ben I.
@BenI. Sie müssen die generische Singleton<>Klasse verwenden. Deshalb ist das Generikum ein Kind der Basisklasse Singleton.
XenoRo
Ja natürlich! Es ist ganz offensichtlich. Ich bin mir nicht sicher, warum ich das nicht gesehen habe. = /
Ben I.
6

Ich möchte nur hinzufügen, dass es nützlich sein kann, anzurufen, DontDestroyOnLoadwenn Ihr Singleton über Szenen hinweg bestehen bleiben soll.

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
quelle
Das ist sehr praktisch. Ich wollte gerade einen Kommentar zu @ PearsonArtPhoto's Antwort abgeben, um genau diese Frage zu stellen:]
CaptainRedmuff
5

Eine weitere Möglichkeit besteht darin, die Klasse in zwei Teile zu unterteilen: eine reguläre statische Klasse für die Singleton-Komponente und ein MonoBehaviour, das als Controller für die Singleton-Instanz fungiert. Auf diese Weise haben Sie die volle Kontrolle über die Konstruktion des Singletons und diese bleibt über Szenen hinweg erhalten. Auf diese Weise können Sie auch jedem Objekt Controller hinzufügen, für das möglicherweise die Daten des Singletons erforderlich sind, anstatt die Szene durchsuchen zu müssen, um eine bestimmte Komponente zu finden.

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
quelle
Das ist sehr nett!
Captain Redmuff
1
Ich habe Probleme, diese Methode zu verstehen. Können Sie das Thema etwas näher erläutern? Vielen Dank.
Hex
3

Hier ist meine Implementierung einer abstrakten Singleton-Klasse. Hier ist, wie es gegen die 4 Kriterien stapelt

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

             No (but why         Yes           Yes        Yes
             should it?)

Es hat ein paar andere Vorteile im Vergleich zu einigen anderen Methoden:

  • Es wird nicht verwendet, FindObjectsOfTypewas ein Leistungskiller ist
  • Es ist flexibel, da es während des Spiels kein neues leeres Spielobjekt erstellen muss. Sie fügen es einfach im Editor (oder während des Spiels) zu einem Spielobjekt Ihrer Wahl hinzu.
  • Es ist threadsicher

    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
quelle
Könnte eine dumme Frage, aber warum hast du das machen OnApplicationQuitCallbackund OnEnableCallbackwie abstractstatt nur leere virtualMethoden? Zumindest in meinem Fall habe ich keine Quit / Enable-Logik und ein leerer Override fühlt sich schmutzig an. Aber mir könnte etwas fehlen.
Jakub Arnold
@ JakubArnold Ich habe mir das schon eine Weile nicht mehr angesehen, aber auf den ersten Blick sieht es so aus, als ob du recht hättest. Besser als virtuelle Methoden
aBertrand
@JakubArnold Eigentlich denke ich, dass ich mich an meine Gedanken von damals erinnere: Ich wollte diejenigen, die dies als eine Komponente benutzten, darauf aufmerksam machen, OnApplicationQuitCallbackund OnEnableCallback: es als virtuelle Methode zu haben, macht es weniger offensichtlich. Vielleicht ein bisschen seltsam, aber soweit ich mich erinnere, war das meine Vernunft.
aBertrand
2

Es gibt tatsächlich eine pseudo-offizielle Möglichkeit, Singleton in Unity zu verwenden. Hier ist die Erklärung: Erstellen Sie im Grunde genommen eine Singleton-Klasse und lassen Sie Ihre Skripte von dieser Klasse erben.

Alakanu
quelle
Vermeiden Sie Antworten nur über Links, indem Sie mindestens eine Zusammenfassung der Informationen in Ihre Antwort aufnehmen, von denen Sie hoffen, dass die Leser sie über den Link erhalten. Auf diese Weise bleibt die Antwort nützlich, falls der Link jemals nicht mehr verfügbar ist.
DMGregory
2

Ich werde meine Implementierung auch für zukünftige Generationen versuchen.

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

Für mich ist diese Zeile Destroy(gameObject.GetComponent(instance.GetType()));sehr wichtig, da ich einmal ein Singleton-Skript für ein anderes gameObject in einer Szene hinterlassen hatte und das gesamte Spielobjekt gelöscht wurde. Dadurch wird die Komponente nur dann zerstört, wenn sie bereits vorhanden ist.

Uri Popov
quelle
1

Ich habe eine Singleton-Klasse geschrieben, mit der Singleton-Objekte einfach erstellt werden können. Es ist ein MonoBehaviour-Skript, sodass Sie die Coroutinen verwenden können. Es basiert auf diesem Unity-Wiki-Artikel und ich werde die Option hinzufügen, es später aus Prefab zu erstellen.

Sie müssen also nicht die Singleton-Codes schreiben. Laden Sie einfach diese Singleton.cs-Basisklasse herunter , fügen Sie sie Ihrem Projekt hinzu und erstellen Sie eine Singleton-Erweiterung:

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

  public string globalVar;

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

Jetzt ist Ihre MySingleton-Klasse ein Singleton und Sie können sie nach Instanz aufrufen:

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

Hier ist ein vollständiges Tutorial: http://www.bivis.com.br/2016/05/04/unity-reusable-singleton-tutorial/

Bivis
quelle