Як я можу правильно реалізувати одиночну схему в Unity?


36

Я переглянув декілька відео та навчальних посібників для створення одиночних об'єктів в Unity, головним чином для a GameManager, які, як видається, використовують різні підходи до інстанціювання та перевірки сингтона.

Чи є правильний, а точніше, кращий підхід до цього?

Два основні приклади, з якими я стикався:

Спочатку

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

Друге

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

Основна відмінність, яку я бачу між собою, це:

Перший підхід спробує орієнтуватися у стеці ігрових об'єктів, щоб знайти примірник того, GameManagerщо, хоча це тільки відбувається (або повинно відбуватися лише один раз), здається, що це може бути дуже неоптимізовано, оскільки сцени збільшуються в розмірах під час розвитку.

Також перший підхід позначає, що об'єкт не повинен бути видалений, коли програма змінює сцену, що забезпечує збереження об'єкта між сценами. Другий підхід, схоже, цього не дотримується.

Другий підхід здається дивним, як і у випадку, коли екземпляр у getter є нульовим, він створить новий GameObject і призначить йому компонент GameManger. Однак, це не може запуститися без того, щоб цей компонент GameManager вже був приєднаний до об'єкта в сцені, тому це викликає у мене певну плутанину.

Чи є інші підходи, які були б рекомендовані, або гібрид із двох вище? Відео та навчальні посібники щодо синглів багато, але всі вони настільки різняться, що важко провести будь-яке порівняння між ними, і, таким чином, зробити висновок, який із них є найкращим / бажаним підходом.


Що сказано, що GameManager повинен робити? Чи повинен це бути GameObject?
bummzack

1
Це насправді не питання про те, що GameManagerслід робити, а про те, як забезпечити існування лише одного примірника об'єкта та найкращого способу його виконання.
Капітан Редмуфф

в цьому навчальному посібнику дуже добре пояснено, як реалізувати одиночну одиницю edgeek.com/unity_c_singleton , я сподіваюся, що це буде корисно
Рахул Лаліт

Відповіді:


30

Це залежить, але зазвичай я використовую третій метод. Проблема з методами, які ви використовували, полягає в тому, що якщо об'єкт включений для початку, він не видалить їх з дерева, і вони все одно можуть бути створені, ініціюючи занадто багато викликів, які можуть зробити речі дійсно заплутаними.

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

Проблема обох ваших реалізацій полягає в тому, що вони не знищують об’єкт, який створюється пізніше. Це може спрацювати, але можна закинути мавповий ключ у твори, що може призвести до дуже важкої помилки налагодження вниз по лінії. Не забудьте перевірити в "Awake", чи вже є примірник, і якщо так, знищивши новий екземпляр.


2
Ви також можете захотіти OnDestroy() { if (this == _instance) { _instance = null; } }, якщо ви хочете мати інший екземпляр у кожній сцені.
Дітріх Епп

Замість того, щоб знищити () ing GameObject, ви повинні створити помилку.
Doodlemeat

2
Можливо. Ви можете ввійти в систему, але я не думаю, що ви повинні викликати помилку, якщо ви не намагаєтесь зробити щось дуже конкретно. Є багато випадків, я можу собі уявити, що підвищення помилки насправді спричинить більше проблем, ніж це виправить.
PearsonArtPhoto

Ви можете зауважити, що MonoBehaviour написано британським правописом Unity ("MonoBehavior" не збирається - я це роблю постійно); інакше це якийсь гідний код.
Майкл Ерік Оберлін

Я знаю, що я запізнююсь, але просто хотів зазначити, що Instanceсингтон цієї відповіді не пережив перезавантаження редактора, оскільки статична властивість стирається. Приклад такого, який не можна знайти ні в одній з відповідей нижче або wiki.unity3d.com/index.php/Singleton (який, можливо, застарів, але, здається, працює від мого експерименту з ним)
Якуб Арнольд,

24

Ось короткий підсумок:

                 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

Тож якщо все, що вам важливо, це глобальний доступ, усі троє отримують те, що вам потрібно. Використання шаблону Сінглтона може бути дещо неоднозначним щодо того, чи хочемо ми лінивої інстанції, примусової унікальності чи глобального доступу, тож не забудьте добре подумати про те, чому ви добираєтесь до синглтона, і вибирайте реалізацію, яка найкраще відповідає цим можливостям. ніж використання стандарту для всіх трьох, коли вам потрібен лише один.

(наприклад, якщо в моїй грі завжди буде GameManager, можливо, мені не байдуже ліниве уявлення - можливо, це лише глобальний доступ із гарантованим існуванням та унікальністю, про які я дбаю - в такому випадку статичний клас отримує мені саме ці функції дуже стисло, без врахування завантаження сцени)

... але точно не використовуйте метод 1 як написано. Знахідку можна пропустити легше за допомогою Awake () підходу Method2 / 3, і якщо ми тримаємо менеджера на сценах, то, швидше за все, ми хочемо дублювати вбивство, якщо ми коли-небудь завантажимось між двома сценами з менеджером, який уже знаходиться в них.


1
Примітка: слід створити можливість поєднання всіх трьох методів для створення четвертого методу, який має всі чотири функції.
Draco18s

3
Спроба цієї відповіді полягає не в тому, що "ви повинні шукати реалізацію Singleton, яка все це робить", а скоріше "ви повинні визначити, які функції ви дійсно хочете від цього сингтона, і вибрати програму, яка забезпечує ці функції, навіть якщо ця реалізація є зовсім не
сингтон

Це хороший момент DMGregory. Насправді не було наміру запропонувати "розбити все це разом", але "нічого про ці функції, що не заважає їм працювати разом в одному класі". тобто "
Потік

17

Найкраща реалізація загальної Singletonмоделі для Єдності, про яку я знаю, - це, звичайно, моя власна.

Він може робити все , і це робить акуратно та ефективно :

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

     Yes                  Yes                  Yes                     Yes (optional)

Інші переваги:

  • Це безпечно для ниток .
  • Це дозволяє уникнути помилок, пов’язаних із придбанням (створенням) одиночних екземплярів, коли програма закриває, гарантуючи, що одиночні кнопки не можуть бути створені після OnApplicationQuit(). (І це робиться з одним глобальним прапором, замість того, щоб кожен тип одиночного типу мав свій власний)
  • Він використовує Mono Update Unity 2017 (приблизно еквівалентний C # 6). (Але його можна легко адаптувати до стародавньої версії)
  • У комплекті є кілька безкоштовних цукерок!

А оскільки обмін є дбайливим , ось це:

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!

Це досить солідно. Виходячи з фону програмування та не-Unity, ви можете пояснити, чому синглтон не управляється в конструкторі, а не в методі Awake? Ви, напевно, можете собі уявити, що будь-який розробник там, побачивши, що синглтон примусовий поза конструктором, - це
підтяжка

1
@netpoetica Простий. Єдність не підтримує конструкторів. Ось чому ви не бачите конструкторів, які використовуються в будь-якому класі, який успадковується MonoBehaviour, і я вважаю, що будь-який клас, що використовується Unity безпосередньо в цілому.
XenoRo

Я не впевнений, що я слідую, як це використовувати. Це означає просто бути батьком відповідного класу? Після оголошення SampleSingletonClass : Singleton, SampleSingletonClass.Instanceповертається с SampleSingletonClass does not contain a definition for Instance.
Бен І.

@BenI. Ви повинні використовувати загальний Singleton<>клас. Ось чому генерик - це дитина базового Singletonкласу.
XenoRo

О, звичайно! Це цілком очевидно. Я не впевнений, чому я цього не бачив. = /
Бен І.

6

Я хотів би лише додати, що вам може бути корисно зателефонувати, DontDestroyOnLoadякщо ви хочете, щоб ваш сингл зберігався на сценах.

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

Це дуже зручно. Я збирався опублікувати коментар до відповіді @ PearsonArtPhoto, щоб задати точне запитання:]
CaptainRedmuff

5

Іншим варіантом може бути поділ класу на дві частини: звичайний статичний клас для компонента Singleton та MonoBehaviour, який виступає контролером для одиночного екземпляра. Таким чином, ви маєте повний контроль над будівництвом сингла, і він буде зберігатися на сценах. Це також дозволяє додавати контролери до будь-якого об'єкта, який може потребувати даних одиночного, замість того, щоб викопати сцену, щоб знайти певний компонент.

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

Це дуже приємно!
Капітан Редмуфф

1
У мене виникають проблеми з розумінням цього методу, чи можете ви трохи розширити цю тему. Дякую.
hex

3

Ось моя реалізація однотонного абстрактного класу нижче. Ось як вона відповідає рівню 4 критеріїв

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

             No (but why         Yes           Yes        Yes
             should it?)

У порівнянні з деякими з інших методів тут є кілька інших переваг:

  • Він не використовує, FindObjectsOfTypeщо є вбивцею продуктивності
  • Він гнучкий тим, що йому не потрібно створювати новий порожній ігровий об’єкт під час гри. Ви просто додаєте його в редакторі (або під час гри) до ігрового об'єкта на ваш вибір.
  • Це безпечно для ниток

    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
    }

Це може бути дурним питанням, але чому ви створили OnApplicationQuitCallbackі OnEnableCallbackяк abstractзамість просто порожніх virtualметодів? Принаймні, в моєму випадку я не маю жодної логіки виходу / ввімкнення, і з порожнім переходом відчувається брудність. Але я можу чогось бракувати.
Якуб Арнольд

@JakubArnold Я не дивився на це деякий час, але на перший погляд схоже, що ти маєш рацію, було б краще як віртуальні методи
aBertrand

@JakubArnold Насправді я думаю, що я пам’ятаю своє мислення ще з того часу: я хотів би усвідомити тих, хто використовував це як компонент, який вони могли використовувати, OnApplicationQuitCallbackі OnEnableCallback: наявність цього типу віртуальних методів робить його менш очевидним. Можливо, трохи дивне мислення, але наскільки я пам’ятаю, це було моїм раціональним.
aBertrand

2

Насправді існує псевдо офіційний спосіб використання Singleton в Unity. Ось пояснення: в основному створіть клас Singleton і зробіть свої сценарії успадкованими від цього класу.


Будь ласка, уникайте відповідей, що стосуються лише посилань, включивши у свою відповідь принаймні резюме інформації, на яку ви сподіваєтесь, що читачі отримають посилання. Таким чином, якщо посилання коли-небудь стане недоступним, відповідь залишається корисною.
DMGregory

2

Я буду кидати свою реалізацію також і для майбутніх поколінь.

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

Для мене цей рядок Destroy(gameObject.GetComponent(instance.GetType()));дуже важливий, оскільки одного разу я залишив на сцені однотонний сценарій на іншій грі «Об’єкт» і весь ігровий об’єкт видалявся. Це знищить компонент лише тоді, коли він уже є.


1

Я написав сингл-клас, який спрощує створення одиночних об'єктів. Це сценарій MonoBehaviour, тож ви можете використовувати Coroutines. Він заснований на цій статті Wiki Unity , і я додам варіант створити його з Prefab пізніше.

Тому вам не потрібно писати коди Singleton. Просто завантажте цей базовий клас Singleton.cs , додайте його до свого проекту та створіть свій однотонний розширення:

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

  public string globalVar;

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

Тепер ваш клас MySingleton є однотонним, і ви можете назвати його інстанцією:

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

Ось повний підручник: http://www.bivis.com.br/2016/05/04/unity-reusable-singleton-tutorial/

Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.