Jak w Unity poprawnie wdrożyć wzór singletonu?

36

Widziałem kilka filmów i samouczków dotyczących tworzenia obiektów singletonów w Unity, głównie dla takich GameManager, które wydają się wykorzystywać różne podejścia do tworzenia instancji i sprawdzania singletona.

Czy istnieje właściwe, a raczej preferowane podejście do tego?

Dwa główne przykłady, które napotkałem, to:

Pierwszy

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

druga

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

Główną różnicą między nimi jest:

Pierwsze podejście będzie polegało na przejściu przez stos obiektów gry w celu znalezienia instancji, GameManagerktóra choć zdarza się to (lub powinna się zdarzyć) tylko raz, wydaje się, że może być bardzo niezoptymalizowana, ponieważ sceny powiększają się podczas tworzenia.

Pierwsze podejście oznacza również, że obiekt nie będzie usuwany, gdy aplikacja zmieni scenę, co zapewnia zachowanie obiektu między scenami. Wydaje się, że drugie podejście się do tego nie stosuje.

Drugie podejście wydaje się dziwne, ponieważ w przypadku, gdy instancja jest pusta w module pobierającym, utworzy nowy GameObject i przypisze do niego komponent GameManger. Jednak nie można tego uruchomić bez uprzedniego podłączenia tego komponentu GameManager do obiektu w scenie, co powoduje pewne zamieszanie.

Czy są jakieś inne podejścia, które byłyby zalecane, lub hybryda dwóch powyższych? Istnieje wiele filmów i samouczków dotyczących singletonów, ale wszystkie różnią się tak bardzo, że trudno jest dokonać porównania między nimi, a tym samym stwierdzić, który z nich jest najlepszym / preferowanym podejściem.

CaptainRedmuff
źródło
Co powinien powiedzieć GameManager? Czy to musi być GameObject?
bummzack
1
Tak naprawdę nie jest to kwestia tego, co GameManagernależy zrobić, a raczej jak zapewnić, że istnieje tylko jedna instancja obiektu i najlepszy sposób, aby to wymusić.
CaptainRedmuff
w tym tutorialu bardzo ładnie wyjaśniono, jak wdrożyć singleton unitygeek.com/unity_c_singleton , mam nadzieję, że przyda się
Rahul Lalit

Odpowiedzi:

30

To zależy, ale zwykle używam trzeciej metody. Problem z zastosowanymi metodami polega na tym, że jeśli obiekt zostanie dołączony na początku, nie usunie go z drzewa, a nadal można je utworzyć, tworząc instancję zbyt wielu wywołań, co może sprawić, że wszystko będzie mylące.

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

Problemem obu twoich implementacji jest to, że nie niszczą one obiektu, który jest tworzony później. To może działać, ale można wrzucić klucz małpi do prac, co może spowodować bardzo trudny do debugowania błąd w linii. Pamiętaj, aby zaznaczyć opcję Przebudź, jeśli istnieje już instancja, a jeśli tak, zniszcz nową instancję.

PearsonArtPhoto
źródło
2
Możesz także chcieć OnDestroy() { if (this == _instance) { _instance = null; } }, jeśli chcesz mieć inną instancję w każdej scenie.
Dietrich Epp
Zamiast Destroy () w GameObject powinieneś zgłosić błąd.
Doodlemeat,
2
Możliwie. Może chcesz to zarejestrować, ale nie sądzę, że powinieneś zgłaszać błąd, chyba że próbujesz zrobić coś bardzo konkretnego. Mogę sobie wyobrazić wiele przypadków, w których zgłoszenie błędu spowodowałoby więcej problemów niż naprawienie.
PearsonArtPhoto
Możesz zauważyć, że MonoBehaviour jest pisane brytyjską pisownią Unity („MonoBehavior” nie kompiluje się - robię to cały czas); w przeciwnym razie jest to jakiś porządny kod.
Michael Eric Oberlin,
Wiem, że się spóźniam, ale chciałem tylko zaznaczyć, że singleton tej odpowiedzi nie przetrwa przeładowania edytora, ponieważ Instancewłaściwość static zostaje wyczyszczona. Przykład takiego, którego nie można znaleźć w jednej z poniższych odpowiedzi , lub wiki.unity3d.com/index.php/Singleton (co może być przestarzałe, ale wydaje się, że działa z moich eksperymentów z nim)
Jakub Arnold
24

Oto krótkie podsumowanie:

                 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

Jeśli więc zależy Ci tylko na globalnym dostępie, wszystkie trzy dostaną to, czego potrzebujesz. Użycie wzorca Singleton może być nieco dwuznaczne, czy chcemy leniwej instancji, wymuszonej wyjątkowości czy globalnego dostępu, więc dobrze się zastanów, dlaczego sięgasz po singleton, i wybierz implementację, która odpowiednio dostosowuje te funkcje. niż stosowanie standardu dla wszystkich trzech, gdy potrzebujesz tylko jednego.

(np. jeśli moja gra zawsze będzie miała GameManagera, może nie dbam o leniwe tworzenie instancji - może to tylko globalny dostęp z gwarantowanym istnieniem i wyjątkowością, na której mi zależy - w takim przypadku klasa statyczna uzyskuje dokładnie te cechy bardzo zwięźle, bez uwag na temat ładowania sceny)

... ale zdecydowanie nie używaj Metody 1 jak napisano. Znalezienie można łatwiej pominąć dzięki metodzie Awake () metody Method2 / 3, a jeśli utrzymujemy menedżera między scenami, bardzo prawdopodobne jest, że chcemy zabić duplikaty, na wypadek, gdybyśmy kiedykolwiek załadowali się między dwiema scenami z menedżerem już w nich zawartym.

DMGregory
źródło
1
Uwaga: powinno być możliwe połączenie wszystkich trzech metod w celu utworzenia czwartej metody, która ma wszystkie cztery cechy.
Draco18s,
3
Istota tej odpowiedzi nie brzmi: „powinieneś poszukać implementacji Singleton, która to wszystko robi”, ale raczej „powinieneś określić, które funkcje naprawdę chcesz od tego singletonu, i wybrać implementację, która zapewnia te funkcje - nawet jeśli ta implementacja jest wcale nie singleton ”
DMGregory
To dobry punkt DMGregory. Naprawdę nie miałem zamiaru sugerować „zniszczyć wszystko razem”, ale „nic z tych funkcji, które uniemożliwiają im wspólną pracę w jednej klasie”. tzn .
Istota
17

Najlepsze wdrożenie ogólnego Singletonwzorca dla Unity, o którym wiem, to (oczywiście) moje własne.

Potrafi zrobić wszystko i robi to schludnie i skutecznie :

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

     Yes                  Yes                  Yes                     Yes (optional)

Inne zalety:

  • Jest bezpieczny dla wątków .
  • Pozwala to uniknąć błędów związanych z pozyskiwaniem (tworzeniem) instancji singletonu, gdy aplikacja kończy pracę, zapewniając, że singletonów nie będzie można później utworzyć OnApplicationQuit(). (I robi to z pojedynczą flagą globalną, zamiast każdego typu singletonu posiadającego własne)
  • Wykorzystuje aktualizację mono Unity 2017 (mniej więcej odpowiednik C # 6). (Ale można go łatwo dostosować do starożytnej wersji)
  • Pochodzi z darmowymi cukierkami!

A ponieważ dzielenie się jest ważne, oto:

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
źródło
To jest całkiem solidne. Opierając się na tle programowania i tle niejednorodności, możesz wyjaśnić, dlaczego singletonem nie zarządza się w konstruktorze, a nie w metodzie Przebudźcie? Prawdopodobnie możesz sobie wyobrazić, że dla każdego dewelopera, zobaczenie Singletona egzekwowanego poza konstruktorem jest
uniesieniem
1
@netpoetica Simple. Unity nie obsługuje konstruktorów. Dlatego nie widzisz konstruktorów używanych w dziedziczeniu klas MonoBehaviouri uważam, że każda klasa używana przez Unity ogólnie.
XenoRo
Nie jestem pewien, czy podążam za tym, jak to wykorzystać. Czy to ma być po prostu rodzicem danej klasy? Po zadeklarowaniu SampleSingletonClass : Singleton, SampleSingletonClass.Instancewraca z SampleSingletonClass does not contain a definition for Instance.
Ben I.
@BenI. Musisz użyć Singleton<>klasy ogólnej . Dlatego generic jest dzieckiem Singletonklasy podstawowej .
XenoRo
Och, oczywiście! To całkiem oczywiste. Nie jestem pewien, dlaczego tego nie widziałem. = /
Ben I.
6

Chciałbym tylko dodać, że przydatne może być zadzwonienie, DontDestroyOnLoadjeśli chcesz, aby singleton trwał w różnych scenach.

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
źródło
To bardzo przydatne. Właśnie miałem opublikować komentarz do odpowiedzi @ PearsonArtPhoto, aby zadać dokładnie to pytanie:]
CaptainRedmuff
5

Inną opcją może być podzielenie klasy na dwie części: zwykłą klasę statyczną dla komponentu Singleton i MonoBehaviour, który działa jako kontroler dla instancji singleton. W ten sposób masz pełną kontrolę nad konstrukcją singletona i będzie on trwał w różnych scenach. Pozwala to również dodawać kontrolery do dowolnego obiektu, który może potrzebować danych singletona, zamiast konieczności przekopywania sceny w celu znalezienia określonego komponentu.

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
źródło
To jest bardzo miłe!
CaptainRedmuff
1
Mam problem ze zrozumieniem tej metody. Czy mógłbyś bardziej rozwinąć ten temat. Dziękuję Ci.
hex
3

Oto moja implementacja singletonowej klasy abstrakcyjnej poniżej. Oto jak wypada w porównaniu z 4 kryteriami

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

             No (but why         Yes           Yes        Yes
             should it?)

Ma kilka innych zalet w porównaniu do niektórych innych metod tutaj:

  • Nie używa, FindObjectsOfTypektóry jest zabójcą wydajności
  • Jest elastyczny, ponieważ nie musi tworzyć nowego pustego obiektu podczas gry. Po prostu dodajesz go w edytorze (lub podczas gry) do wybranego obiektu.
  • Jest bezpieczny dla wątków

    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
źródło
To może być głupie pytanie, ale dlaczego to zrobiłeś OnApplicationQuitCallback i OnEnableCallbackjak abstractzamiast tylko pustymi virtualmetodami? Przynajmniej w moim przypadku nie mam logiki wyjścia / włączenia, a puste zastąpienie jest brudne. Ale coś może mi umknąć.
Jakub Arnold
@JakubArnold Nie patrzyłem na to od dłuższego czasu, ale na pierwszy rzut oka wygląda na to, że masz rację, byłoby lepiej jako metody wirtualne
aBertrand
@JububArnold Właściwie myślę, że pamiętam moje myślenie z tamtych czasów: chciałem uświadomić tym, którzy używali tego jako komponentu, którego mogliby użyć, OnApplicationQuitCallbacka OnEnableCallback: posiadanie go jako metod wirtualnych sprawia, że ​​jest to mniej oczywiste. Może nieco dziwne myślenie, ale o ile pamiętam, było to moje racjonalne podejście.
aBertrand
2

W rzeczywistości istnieje pseudo oficjalny sposób korzystania z Singleton w Unity. Oto wyjaśnienie, w zasadzie stwórz klasę Singleton i spraw, aby twoje skrypty dziedziczyły po tej klasie.

Alakanu
źródło
Unikaj odpowiedzi tylko za pomocą linku, umieszczając w odpowiedzi przynajmniej podsumowanie informacji, które, jak masz nadzieję, czytelnicy uzyskają z linku. W ten sposób, jeśli link kiedykolwiek stanie się niedostępny, odpowiedź pozostanie przydatna.
DMGregory
2

Zaimplementuję też moje wdrożenie dla przyszłych pokoleń.

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

Dla mnie ta linia Destroy(gameObject.GetComponent(instance.GetType()));jest bardzo ważna, ponieważ po pozostawieniu skryptu singletonowego w innej grze Object w scenie cały obiekt gry był usuwany. Spowoduje to zniszczenie komponentu, jeśli już istnieje.

Uri Popow
źródło
1

Napisałem klasę singletonów, która ułatwia tworzenie obiektów singletonów. Jest to skrypt MonoBehaviour, więc możesz używać Coroutines. Opiera się na tym artykule Unity Wiki , a później dodam opcję utworzenia go z prefabrykatu.

Więc nie musisz pisać kodów Singleton. Wystarczy pobrać tę klasę podstawową Singleton.cs , dodać ją do projektu i utworzyć singleton, rozszerzając ją:

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

  public string globalVar;

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

Teraz twoja klasa MySingleton jest singletonem i możesz ją nazwać instancją:

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

Oto kompletny samouczek: http://www.bivis.com.br/2016/05/04/unity-reusable-singleton-tutorial/

Bivis
źródło