No Unity, como implemento corretamente o padrão singleton?

36

Vi vários vídeos e tutoriais para criar objetos singleton no Unity, principalmente para um GameManager, que parecem usar abordagens diferentes para instanciar e validar um singleton.

Existe uma abordagem correta, ou melhor, preferida para isso?

Os dois exemplos principais que encontrei são:

Primeiro

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

Segundo

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

A principal diferença que posso ver entre os dois é:

A primeira abordagem tentará navegar na pilha de objetos do jogo para encontrar uma instância da GameManagerqual, mesmo que isso só aconteça (ou só aconteça), uma vez parece que pode ser muito não otimizado à medida que as cenas aumentam de tamanho durante o desenvolvimento.

Além disso, a primeira abordagem marca o objeto a não ser excluído quando o aplicativo muda de cena, o que garante que o objeto persista entre as cenas. A segunda abordagem não parece aderir a isso.

A segunda abordagem parece estranha, pois no caso em que a instância é nula no getter, ela cria um novo GameObject e atribui um componente GameManger a ele. No entanto, isso não pode ser executado sem que o componente GameManager já esteja anexado a um objeto na cena, então isso está me causando alguma confusão.

Existem outras abordagens recomendadas ou um híbrido dos dois acima? Existem muitos vídeos e tutoriais sobre singletons, mas todos diferem tanto que é difícil fazer comparações entre os dois e, portanto, uma conclusão sobre qual é a melhor / preferida abordagem.

CaptainRedmuff
fonte
O que é dito que o GameManager deveria fazer? Tem que ser um GameObject?
bummzack
11
Não se trata realmente do que GameManagerdeve fazer, mas de como garantir que exista apenas uma instância do objeto e a melhor maneira de aplicá-lo.
CaptainRedmuff 02/02
este tutoriais muito bem explicado, como implementar singleton unitygeek.com/unity_c_singleton , espero que seja útil
Rahul Lalit

Respostas:

30

Depende, mas geralmente eu uso um terceiro método. O problema com os métodos que você usou é que, no início, o objeto é incluído, ele não os remove da árvore e ainda pode ser criado instanciando muitas chamadas, o que pode tornar as coisas realmente confusas.

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

O problema com as duas implementações é que elas não destroem um objeto criado posteriormente. Pode funcionar, mas é possível jogar uma chave de macaco nos trabalhos que podem resultar em um erro de depuração muito difícil na linha. Certifique-se de verificar em Despertar se já existe uma instância e, se houver, destruindo a nova instância.

PearsonArtPhoto
fonte
2
Você também pode querer OnDestroy() { if (this == _instance) { _instance = null; } }, se quiser ter uma instância diferente em cada cena.
Dietrich Epp 03/02
Em vez de Destroy () no GameObject, você deve gerar um erro.
Doodlemeat
2
Possivelmente. Convém registrá-lo, mas acho que você não deve gerar um erro, a menos que esteja tentando fazer algo muito específico. Há muitos casos em que posso imaginar que gerar um erro realmente causaria mais problemas do que consertaria.
precisa saber é o seguinte
Você pode observar que o MonoBehaviour está escrito com a ortografia britânica do Unity ("MonoBehavior" não será compilado - eu faço isso o tempo todo); caso contrário, este é um código decente.
Michael Eric Oberlin
Eu sei que estou chegando atrasado, mas só queria salientar, que o singleton dessa resposta não sobrevive a uma recarga de editor, porque a Instancepropriedade estática é apagada. Um exemplo de um que não pode ser encontrado em uma das respostas abaixo , ou wiki.unity3d.com/index.php/Singleton (que pode estar desatualizado, mas parece funcionar com minhas experiências com ele)
Jakub Arnold
24

Aqui está um resumo rápido:

                 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

Portanto, se tudo o que importa é o acesso global, todos os três terão o que você precisa. O uso do padrão Singleton pode ser um pouco ambíguo sobre se queremos instanciação lenta, exclusividade forçada ou acesso global. Portanto, pense cuidadosamente sobre o motivo pelo qual você está alcançando o singleton e escolha uma implementação que acerte esses recursos. do que usar um padrão para todos os três quando você precisar apenas de um.

(por exemplo, se meu jogo sempre tiver um GameManager, talvez eu não me importe com instâncias preguiçosas - talvez seja apenas o acesso global com existência garantida e exclusividade -, nesse caso, uma classe estática obtém exatamente esses recursos de maneira concisa, sem considerações de carregamento de cena)

... mas definitivamente não use o Método 1 como está escrito. O Find pode ser pulado mais facilmente com a abordagem Awake () do Method2 / 3, e se mantivermos o gerente entre as cenas, é provável que desejemos matar duplicados, caso possamos carregar entre duas cenas com um gerente já nelas.

DMGregory
fonte
11
Nota: deve ser possível combinar todos os três métodos para criar um quarto método que possua todos os quatro recursos.
Draco18s
3
O principal desta resposta não é "você deve procurar uma implementação Singleton que faça tudo", mas sim "deve identificar quais recursos você realmente deseja deste singleton e escolher uma implementação que ofereça esses recursos - mesmo se essa implementação for não é um singleton "
DMGregory
Esse é um bom ponto DMGregory. Não era minha intenção sugerir "esmagar tudo isso junto", mas "nada sobre esses recursos que os impedem de trabalhar juntos em uma única classe". ou seja, "O objetivo desta resposta NÃO é sugerir a escolha de uma " .
Draco18s
17

A melhor implementação de um Singletonpadrão genérico para o Unity que conheço é (é claro) o meu.

Ele pode fazer tudo , e fá-lo perfeitamente e de forma eficiente :

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

     Yes                  Yes                  Yes                     Yes (optional)

Outras vantagens:

  • É seguro para threads .
  • Evita erros relacionados à aquisição (criação) de instâncias singleton quando o aplicativo é encerrado, garantindo que os singletons não possam ser criados depois OnApplicationQuit(). (E o faz com uma única bandeira global, em vez de cada tipo de singleton ter o seu próprio)
  • Ele usa o Mono Update do Unity 2017 (aproximadamente equivalente ao C # 6). (Mas pode ser facilmente adaptado para a versão antiga)
  • Ele vem com alguns doces grátis!

E porque compartilhar é cuidar , aqui está:

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
fonte
Isso é bastante sólido. Vindo de um background de programação e de um background que não seja da Unity, você pode explicar por que o singleton não é gerenciado no construtor e não no método Awake? Você provavelmente pode imaginar que, para qualquer desenvolvedor lá fora, ver um Singleton forçado fora de um construtor é um levantador de sobrancelhas ...
netpoetica
11
@netpoetica Simple. Unity não suporta construtores. É por isso que você não vê construtores sendo usados ​​em nenhuma classe herdada MonoBehaviour, e acredito que qualquer classe usada pelo Unity diretamente em geral.
XenoRo 19/04
Não sei se segui como utilizar isso. Isso significa simplesmente ser o pai da classe em questão? Depois de declarar SampleSingletonClass : Singleton, SampleSingletonClass.Instancevolta com SampleSingletonClass does not contain a definition for Instance.
Ben I.
@BenI. Você precisa usar a Singleton<>classe genérica . É por isso que o genérico é filho da Singletonclasse base .
XenoRo 23/08
Ah, claro! É bastante óbvio. Não sei por que não vi isso. = /
Ben I.
6

Gostaria apenas de acrescentar que pode ser útil ligar DontDestroyOnLoadse você quiser que o seu singleton persista nas cenas.

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
fonte
Isso é muito útil. Eu estava prestes a postar um comentário na resposta de @ PearsonArtPhoto para fazer esta pergunta exata:]
CaptainRedmuff 02/02
5

Outra opção pode ser dividir a classe em duas partes: uma classe estática regular para o componente Singleton e um MonoBehaviour que atua como um controlador para a instância singleton. Dessa forma, você tem total controle sobre a construção do singleton, e ele persistirá nas cenas. Isso também permite adicionar controladores a qualquer objeto que possa precisar dos dados do singleton, em vez de precisar percorrer a cena para encontrar um componente específico.

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
fonte
Isso é muito legal!
CaptainRedmuff
11
Estou tendo problemas para entender esse método. Você pode expandir um pouco mais sobre esse assunto. Obrigado.
Hex2020:
3

Aqui está minha implementação de uma classe abstrata singleton abaixo. Aqui está como ele se compara aos 4 critérios

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

             No (but why         Yes           Yes        Yes
             should it?)

Ele tem algumas outras vantagens em comparação com alguns dos outros métodos aqui:

  • Não usa o FindObjectsOfTypeque é um assassino de desempenho
  • É flexível, pois não precisa criar um novo objeto de jogo vazio durante o jogo. Você simplesmente o adiciona no editor (ou durante o jogo) a um objeto de jogo de sua escolha.
  • É seguro para discussão

    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
fonte
Pode ser uma pergunta boba, mas por que você fez o OnApplicationQuitCallbacke OnEnableCallbackcomo em abstractvez de apenas virtualmétodos vazios ? Pelo menos no meu caso, eu não tenho nenhuma lógica de encerrar / ativar e ter uma substituição vazia parece suja. Mas eu posso estar perdendo alguma coisa.
Jakub Arnold
@JakubArnold Eu não olho para isso há um tempo, mas à primeira vista parece que você está certo, seria melhor como métodos virtuais
aBertrand
@JakubArnold Na verdade, acho que me lembro do que pensava naquela época: queria conscientizar aqueles que usavam isso como um componente que eles poderiam usar OnApplicationQuitCallbacke OnEnableCallback: tê-lo como métodos virtuais meio que o torna menos óbvio. Talvez um pensamento um pouco estranho, mas pelo que me lembro, esse era o meu racional.
aBertrand 29/04
2

Na verdade, existe uma maneira pseudo-oficial de usar o Singleton no Unity. Aqui está a explicação: basicamente crie uma classe Singleton e faça com que seus scripts sejam herdados dessa classe.

Alakanu
fonte
Evite respostas apenas para links, incluindo na sua resposta pelo menos um resumo das informações que você espera que os leitores colhem no link. Dessa forma, se o link ficar indisponível, a resposta permanece útil.
DMGregory
2

Vou seguir minha implementação também para as gerações futuras.

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

Para mim, essa linha Destroy(gameObject.GetComponent(instance.GetType()));é muito importante porque uma vez deixei um script singleton em outro gameObject em uma cena e todo o objeto do jogo estava sendo excluído. Isso destruirá o componente apenas se ele já existir.

Uri Popov
fonte
1

Eu escrevi uma classe singleton que facilita a criação de objetos singleton. É um script MonoBehaviour, para que você possa usar as Coroutines. É baseado neste artigo do Unity Wiki , e adicionarei a opção de criá-lo no Prefab posteriormente.

Portanto, você não precisa escrever os códigos Singleton. Basta baixar esta classe base Singleton.cs , adicioná-la ao seu projeto e criar seu singleton estendendo-o:

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

  public string globalVar;

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

Agora sua classe MySingleton é um singleton e você pode chamá-lo por Instância:

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

Aqui está um tutorial completo: http://www.bivis.com.br/2016/05/04/unity-reusable-singleton-tutorial/

Bivis
fonte