Dans Unity, comment implémenter correctement le motif singleton?

36

J'ai vu plusieurs vidéos et tutoriels pour la création d'objets singleton dans Unity, principalement pour un GameManager, qui semblent utiliser différentes approches pour instancier et valider un singleton.

Existe-t-il une approche correcte, ou plutôt préférée, à cet égard?

Les deux principaux exemples que j'ai rencontrés sont:

Première

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

Seconde

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

La principale différence que je peux voir entre les deux est la suivante:

La première approche tentera de naviguer dans la pile d’objets du jeu pour trouver une instance de ce GameManagerqui, bien que cela ne se produise (ou ne devrait se produire), semble une fois que cela pourrait être très non optimisé à mesure que la taille des scènes augmente au cours du développement.

En outre, la première approche marque que l’objet ne doit pas être supprimé lorsque l’application change de scène, ce qui garantit la persistance de cet objet entre les scènes. La deuxième approche ne semble pas adhérer à cela.

La deuxième approche semble étrange car dans le cas où l'instance est nulle dans le getter, elle créera un nouveau GameObject et lui affectera un composant GameManger. Cependant, cela ne peut pas s'exécuter sans que ce composant GameManager soit déjà attaché à un objet de la scène, ce qui me confond.

Existe-t-il d'autres approches qui seraient recommandées, ou un hybride des deux ci-dessus? Il existe de nombreuses vidéos et tutoriels sur les singletons, mais ils diffèrent tous tellement qu'il est difficile d'établir des comparaisons entre les deux et donc de déterminer laquelle est la meilleure approche / la meilleure approche.

CaptainRedmuff
la source
Qu'est-ce que GameManager est censé faire? Faut-il que ce soit un GameObject?
Bummzack
1
Ce n’est pas vraiment une question de savoir quoi GameManagerfaire, mais plutôt de s’assurer qu’il n’existe qu’une seule instance de l’objet et le meilleur moyen de le faire respecter.
CaptainRedmuff
ces tutoriels très bien expliqués, comment implémenter singleton unitygeek.com/unity_c_singleton , j'espère que c'est utile
Rahul Lalit

Réponses:

30

Cela dépend, mais d'habitude j'utilise une troisième méthode. Le problème avec les méthodes que vous avez utilisées est que, dans le cas où l'objet est inclus pour commencer, il ne les supprimera pas de l'arborescence, et ils peuvent toujours être créés en instanciant trop d'appels, ce qui pourrait rendre les choses vraiment déroutantes.

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

Le problème avec vos deux implémentations est qu'elles ne détruisent pas un objet créé ultérieurement. Cela pourrait fonctionner, mais on pourrait jeter une clé à molette dans les travaux qui pourrait entraîner une erreur de débogage très difficile sur toute la ligne. Assurez-vous de vérifier dans Awake s'il existe déjà une instance et, le cas échéant, de détruire la nouvelle instance.

PearsonArtPhoto
la source
2
Vous pouvez également vouloir OnDestroy() { if (this == _instance) { _instance = null; } }, si vous voulez avoir une instance différente dans chaque scène.
Dietrich Epp
Au lieu de détruire () le GameObject, vous devriez générer une erreur.
Doodlemeat
2
Peut-être. Vous voudrez peut-être vous connecter, mais je ne pense pas que vous devriez générer une erreur, sauf si vous essayez de faire quelque chose de très spécifique. Je peux imaginer que dans de nombreux cas, le fait de générer une erreur causerait davantage de problèmes que cela ne serait corrigé.
PearsonArtPhoto
Vous voudrez peut-être noter que MonoBehaviour est orthographié avec l'orthographe britannique par Unity ("MonoBehavior" ne compilera pas - je le fais tout le temps); sinon, il s'agit d'un code décent.
Michael Eric Oberlin
Je sais que j'arrive en retard, mais je voulais juste souligner que le singleton de cette réponse ne survit pas à un rechargement d'éditeur, car la Instancepropriété static est effacée. Un exemple de ce qui ne fonctionne pas peut être trouvé non plus dans l'une des réponses ci-dessous , ou wiki.unity3d.com/index.php/Singleton (qui peut être obsolète, mais semble fonctionner à partir de mes expériences)
Jakub Arnold
24

Voici un résumé rapide:

                 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

Donc, si tout ce qui vous intéresse, c’est l’accès mondial, vous obtenez tous les trois ce dont vous avez besoin. L'utilisation du modèle Singleton peut être un peu ambiguë quant à savoir si nous voulons une instanciation paresseuse, une unicité imposée ou un accès global . Veillez donc à bien réfléchir à la raison pour laquelle vous recherchez le singleton et à choisir une implémentation qui optimise ces fonctionnalités, que d'utiliser un standard pour les trois quand vous n'en avez besoin que d'un.

(par exemple, si mon jeu aura toujours un GameManager, peut-être que je ne me soucie pas de l'instanciation paresseuse - c'est peut-être seulement un accès global avec une existence garantie et le caractère unique qui me tient à cœur - dans ce cas, une classe statique m'offre exactement ces fonctionnalités de manière très concise, sans considérations de chargement de scène)

... mais n'utilisez certainement pas la Méthode 1 telle qu'écrite. La recherche peut être ignorée plus facilement avec l’approche Awake () de Method2 / 3, et si nous gardons le responsable d’une scène à l’autre, nous voulons très probablement supprimer les doublons, au cas où nous chargerions entre deux scènes avec un responsable déjà présent.

DMGregory
la source
1
Remarque: il devrait être possible de combiner les trois méthodes pour créer une 4ème méthode présentant les quatre fonctionnalités.
Draco18s
3
Cette réponse ne vise pas essentiellement "vous devez rechercher une implémentation Singleton qui fait tout", mais plutôt "vous devez identifier les fonctionnalités que vous souhaitez réellement de ce singleton, et choisir une implémentation offrant ces fonctionnalités - même si cette implémentation est pas un singleton du tout "
DMGregory
C'est un bon point DMGregory. Je n’avais pas vraiment l’intention de suggérer «écraser tout cela ensemble» mais «rien au sujet de ces fonctionnalités qui les empêche de travailler ensemble dans une seule classe». C'est-à-dire que "le but de cette réponse n'est PAS de suggérer d'en choisir un. "
Draco 18s
17

La meilleure implémentation d'un Singletonmodèle générique pour Unity que je connaisse est (bien sûr) la mienne.

Il peut tout faire , et il le fait proprement et efficacement :

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

     Yes                  Yes                  Yes                     Yes (optional)

Autres avantages:

  • C'est thread-safe .
  • Cela évite les bugs liés à l'acquisition (création) d'instances singleton lorsque l'application est abandonnée en s'assurant que les singletons ne peuvent pas être créés après OnApplicationQuit(). (Et cela se fait avec un seul drapeau global, au lieu que chaque type de singleton ait son propre)
  • Il utilise la mise à jour Mono d'Unity 2017 (à peu près équivalente à C # 6). (Mais il peut facilement être adapté à la version ancienne)
  • Il vient avec des bonbons gratuits!

Et parce que partager c'est aimer , le voici:

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
la source
C'est assez solide. Pouvez-vous expliquer pourquoi le singleton n’est pas géré dans le constructeur plutôt que dans la méthode Awake? Vous pouvez probablement imaginer que pour tout développeur, voir un Singleton forcé à l'extérieur d'un constructeur est un lever de sourcil ...
netpoetica
1
@netpoetica Simple. L'unité ne supporte pas les constructeurs. C'est pourquoi vous ne voyez pas les constructeurs utilisés dans aucune classe hériter MonoBehaviour, et je crois toute classe utilisée directement par Unity en général.
XenoRo
Je ne suis pas sûr de savoir comment utiliser cela. Est-ce que cela signifie simplement être le parent de la classe en question? Après avoir déclaré SampleSingletonClass : Singleton, SampleSingletonClass.Instancerevient avec SampleSingletonClass does not contain a definition for Instance.
Ben I.
@BenI. Vous devez utiliser la Singleton<>classe générique . C'est pourquoi le générique est un enfant de la Singletonclasse de base .
XenoRo
Oh bien sûr! C'est assez évident. Je ne sais pas pourquoi je n'ai pas vu ça. = /
Ben I.
6

J'aimerais juste ajouter qu'il peut être utile d'appeler DontDestroyOnLoadsi vous voulez que votre singleton persiste d'une scène à l'autre.

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
la source
C'est très pratique. J'étais sur le point de poster un commentaire sur la réponse de @ PearsonArtPhoto pour poser cette question exacte:]
CaptainRedmuff
5

Une autre option pourrait être de scinder la classe en deux parties: une classe statique régulière pour le composant Singleton et un MonoBehaviour qui agit en tant que contrôleur pour l'instance singleton. De cette façon, vous avez un contrôle total sur la construction du singleton, et cela persistera à travers les scènes. Cela vous permet également d'ajouter des contrôleurs à tout objet pouvant nécessiter les données du singleton, sans avoir à fouiller dans la scène pour trouver un composant particulier.

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
la source
C'est très gentil!
CaptainRedmuff
1
J'ai du mal à comprendre cette méthode, pouvez-vous développer un peu plus à ce sujet. Merci.
hex
3

Voici mon implémentation d'une classe abstraite singleton ci-dessous. Voici comment cela se compare aux 4 critères

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

             No (but why         Yes           Yes        Yes
             should it?)

Il présente quelques autres avantages par rapport à certaines des autres méthodes présentées ici:

  • Il n'utilise pas FindObjectsOfTypequi est un tueur de performance
  • Il est flexible en ce sens qu'il n'est pas nécessaire de créer un nouvel objet de jeu vide pendant le jeu. Vous l'ajoutez simplement dans l'éditeur (ou pendant le jeu) à un objet de votre choix.
  • C'est fil-safe

    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
la source
Peut-être une question idiote, mais pourquoi avez-vous créé les méthodes OnApplicationQuitCallbackand en OnEnableCallbacktant abstractque virtualméthodes vides ? Au moins dans mon cas, je n'ai pas de logique Quitter / Activer et avoir un remplacement vide me semble sale. Mais il se peut que je manque quelque chose.
Jakub Arnold
@JakubArnold Je n'ai pas examiné cela depuis un moment, mais à première vue, on dirait que vous avez raison, serait mieux comme méthode virtuelle
aBertrand
@JakubArnold En fait, je pense me souvenir de ma pensée de l'époque: je voulais informer ceux qui l'utilisaient comme composant qu'ils pourraient utiliser OnApplicationQuitCallbacket OnEnableCallback: le fait de l' utiliser comme méthode virtuelle le rend moins évident. Peut-être un peu bizarre une pensée, mais pour autant que je m'en souvienne, c'était ma raison.
aBertrand le
2

Il existe en fait une manière pseudo officielle d'utiliser Singleton dans Unity. Voici l'explication: créez en gros une classe Singleton et faites en sorte que vos scripts héritent de cette classe.

Alakanu
la source
Évitez les réponses avec lien uniquement, en incluant dans votre réponse au moins un résumé des informations que vous espérez voir extraites du lien. Ainsi, si le lien devient indisponible, la réponse reste utile.
DMGregory
2

Je vais aussi ma mise en œuvre pour les générations futures.

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

Pour moi, cette ligne Destroy(gameObject.GetComponent(instance.GetType()));est très importante car jadis j’avais laissé un script singleton sur un autre gameObject d’une scène et que tout l’objet du jeu était en train d’être supprimé. Cela ne détruit le composant que s'il existe déjà.

Uri Popov
la source
1

J'ai écrit une classe singleton qui facilite la création d'objets singleton. C'est un script MonoBehaviour, vous pouvez donc utiliser les Coroutines. Son basé sur cet article de wiki de Unity , et j'ajouterai l'option pour le créer à partir de Prefab plus tard.

Donc, vous n'avez pas besoin d'écrire les codes Singleton. Il suffit de télécharger cette classe de base Singleton.cs , de l'ajouter à votre projet et de créer votre singleton en l'étendant:

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

  public string globalVar;

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

Maintenant, votre classe MySingleton est un singleton, et vous pouvez l'appeler par instance:

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

Voici un tutoriel complet: http://www.bivis.com.br/2016/05/04/unity-reusable-singleton-tutorial/

Bivis
la source