В 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которого, даже если это случается (или должно происходить только один раз), кажется, что это может быть очень неоптимизировано, так как сцены увеличиваются в размерах во время разработки.

Кроме того, первый подход помечает объект как не подлежащий удалению, когда приложение меняет сцену, что обеспечивает сохранение объекта между сценами. Второй подход, кажется, не придерживается этого.

Второй подход кажется странным, так как в случае, когда экземпляр является нулевым в получателе, он создаст новый GameObject и назначит ему компонент GameManger. Однако это не может быть выполнено без предварительного подключения этого компонента GameManager к объекту в сцене, поэтому это вызывает у меня некоторую путаницу.

Есть ли какие-либо другие подходы, которые будут рекомендованы, или гибрид двух вышеупомянутых? Существует множество видео и учебных пособий, касающихся синглетонов, но все они отличаются настолько, что трудно провести какие-либо сравнения между ними и, следовательно, сделать вывод о том, какой из них является наилучшим / предпочтительным подходом.


Что должен делать GameManager? Это должен быть GameObject?
bummzack

1
Вопрос не в том, что GameManagerнужно делать, а в том, как обеспечить наличие только одного экземпляра объекта и как лучше это обеспечить.
CaptainRedmuff

этот урок очень хорошо объяснил, как реализовать синглтон unitygeek.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; } }, если вы хотите иметь разные экземпляры в каждой сцене.
Дитрих Эпп

Вместо Destroy () в 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

Так что, если все, что вас волнует, это глобальный доступ, все три получают то, что вам нужно. Использование шаблона Singleton может быть немного двусмысленным в отношении того, хотим ли мы создать ленивый экземпляр, принудительную уникальность или глобальный доступ, поэтому обязательно тщательно продумайте, почему вы выбираете singleton, и выберите реализацию, которая обеспечивает правильность этих функций, а не чем использование стандарта для всех трех, когда вам нужен только один.

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

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


1
Примечание: должно быть возможно объединить все три метода для создания 4-го метода, который имеет все четыре функции.
Draco18s

3
Суть этого ответа не в том, что «вы должны искать реализацию Singleton, которая делает все это», а в том, что вы должны определить, какие функции вам действительно нужны из этого синглтона, и выбрать реализацию, которая предоставляет эти функции - даже если эта реализация ни одного сингла "
Д.М.Григорий

Это хороший момент, DMGregory. На самом деле я не собирался предлагать «разбить все вместе», но что «ничего об этих функциях не мешает им работать вместе в одном классе». т.е. «Суть этого ответа не предлагать выбрать один. »
Draco18s

17

Лучшая реализация общего Singletonшаблона для Unity, о котором я знаю, - это (конечно) моя собственная.

Он может делать все , и делает это аккуратно и эффективно :

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

     Yes                  Yes                  Yes                     Yes (optional)

Другие преимущества:

  • Это потокобезопасно .
  • Это позволяет избежать ошибок, связанных с получением (созданием) одноэлементных экземпляров при выходе из приложения, гарантируя, что синглтоны не могут быть созданы после OnApplicationQuit(). (И делает это с одним глобальным флагом, вместо каждого отдельного типа, имеющего свой собственный)
  • Он использует Mono Обновление 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? Вы, вероятно, можете себе представить, что для любого разработчика, когда синглтон применяется вне конструктора, это бровь ...
netpoetica

1
@netpoetica Простой. Unity не поддерживает конструкторы. Вот почему вы не видите конструкторов, используемых в наследовании какого-либо класса 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, который действует как контроллер для экземпляра singleton. Таким образом, у вас есть полный контроль над конструкцией синглтона, и он будет сохраняться во всех сценах. Это также позволяет вам добавлять контроллеры к любому объекту, который может нуждаться в данных синглтона, вместо того, чтобы копаться в сцене, чтобы найти определенный компонент.

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

Это очень мило!
CaptainRedmuff

1
У меня возникли проблемы с пониманием этого метода, не могли бы вы подробнее рассказать об этом? Спасибо.
шестнадцатое

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, так что вы можете использовать сопрограммы. Это основано на этом статье в Unity Wiki , и я добавлю возможность создать его из 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.