Dans Unity, comment implémenter correctement le modèle singleton?

J'ai vu plusieurs vidéos et tutoriels pour créer des 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?

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

Premier

public class GameManager
{
    private static GameManager _instance;

    public static GameManager Instance
    {
        get
        {
            if(_instance == null)
            {
                _instance = GameObject.FindObjectOfType();
            }

            return _instance;
        }
    }

    void Awake()
    {
        DontDestroyOnLoad(gameObject);
    }
}

Deuxième

public class GameManager
{
    private static GameManager _instance;

    public static GameManager Instance
    {
        get
        {
            if(_instance == null)
            {
                instance = new GameObject("Game Manager");
                instance.AddComponent();
            }

            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 parcourir la pile d'objets du jeu pour trouver une instance du GameManager qui, même si cela ne se produit que (ou ne devrait se produire), semble une fois que cela pourrait être très non optimisé à mesure que la taille des scènes augmente. pendant le 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 assignera 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 didacticiels concernant les singletons, mais ils diffèrent tous tellement qu'il est difficile d'établir une comparaison entre les deux et donc de déterminer laquelle est la meilleure approche/la meilleure approche.

23
ajouté édité
Vues: 1
Qu'est-ce que GameManager est censé faire? Faut-il que ce soit un GameObject?
ajouté l'auteur Tom, source
Ce n’est pas vraiment une question de ce que GameManager doit faire, mais plutôt de s’assurer qu’il n’ya qu’une seule instance de l’objet et le meilleur moyen de l’appliquer.
ajouté l'auteur nealmcb, source
ces tutoriels très bien expliqués, comment implémenter singleton unitygeek.com/unity_c_singleton , j'espère que c'est utile
ajouté l'auteur Ashok K Thakur, source

10 Réponses

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 risquerait de rendre les choses confuses.

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 lancer une clé à molette dans le logiciel, ce qui pourrait entraîner une erreur de débogage très difficile. 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.

22
ajouté
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é.
ajouté l'auteur m104, source
Au lieu de détruire() le GameObject, vous devriez générer une erreur.
ajouté l'auteur 7hi4g0, source
Vous voudrez peut-être aussi OnDestroy() {if (this == _instance) {_instance = null; }} , si vous voulez avoir une instance différente dans chaque scène.
ajouté l'auteur andykais, source
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.
ajouté l'auteur David, source

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 à la question de savoir si nous voulons une instanciation paresseuse, une unicité imposée ou un accès global . Examinez attentivement les raisons pour lesquelles vous recherchez le singleton et choisissez une implémentation qui intègre correctement ces fonctionnalités, plutôt que l’utilisation d’une norme pour les trois solutions lorsque vous n’en avez besoin que d’une seule.

(eg. if my game will always have a GameManager, maybe I don't care about lazy instantiation - maybe it's only global access with guaranteed existence & uniqueness I care about - in which case a static class gets me exactly those features very concisely, with no scene loading considerations)

... 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 maintenons le manager entre scènes, nous voulons très probablement supprimer les doublons, au cas où nous chargerions entre deux scènes avec un manager déjà présent.

20
ajouté
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 "
ajouté l'auteur DMGregory, source
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.
ajouté l'auteur Draco18s, source
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». "Le but de cette réponse n'est PAS de suggérer en choisir un. "
ajouté l'auteur Draco18s, source

J'aimerais juste ajouter qu'il peut être utile d'appeler DontDestroyOnLoad si vous voulez que votre singleton persiste dans toutes les scènes.

public class Singleton : MonoBehavior 
{ 
    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);
    } 
}
7
ajouté
C'est très pratique. J'étais sur le point de poster un commentaire sur la réponse de @ PearsonArtPhoto pour poser cette question précise:]
ajouté l'auteur nealmcb, source

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

Il peut tout tout et 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:

  • It's thread-safe.
  • It avoids bugs related to acquiring (creating) singleton instances when the application is quitting by ensuring that singletons cannot be created after OnApplicationQuit(). (And it does so with a single global flag, instead of each singleton type having their own)
  • It uses Unity 2017's Mono Update (roughly equivalent to C# 6). (But it can easily be adapted for the ancient version)
  • It comes with some free candy!

Et parce que partager, c'est bien aimer , la voici:

public abstract class Singleton : 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();
                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();
            }
        }
    }
    #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!
6
ajouté

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, qui 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 rechercher 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();
   }
} 
4
ajouté
C'est très gentil!
ajouté l'auteur nealmcb, source
J'ai du mal à comprendre cette méthode, pouvez-vous développer un peu plus à ce sujet. Je vous remercie.
ajouté l'auteur Gerold Broser, source

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 j’avais laissé un script singleton sur un autre gameObject dans une scène et que l’objet du jeu entier était supprimé. . Cela ne détruit le composant que s'il existe déjà.

2
ajouté

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:

  • It doesn't use FindObjectsOfType which is a performance killer
  • It's flexible in that it doesn't need to create a new empty gameobject during the game. You simply add it in the editor (or during the game) to a gameobject of your choosing.
  • It's thread safe

    using System.Collections.Generic;
    using System.Linq;
    using UnityEngine;
    
    public abstract class Singleton : MonoBehaviour where T : Singleton
    {
        #region  Variables
        protected static bool Quitting { get; private set; }
    
        private static readonly object Lock = new object();
        private static Dictionary> _instances;
    
        public static T Instance
        {
            get
            {
                if (Quitting)
                {
                    return null;
                }
                lock (Lock)
                {
                    if (_instances == null)
                        _instances = new Dictionary>();
    
                    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>();
    
                    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
    }
    
2
ajouté

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. C’est basé sur cet article d'Unity Wiki , et j'ajouterai une option pour le créer à partir de Prefab ultérieurement .

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

public class MySingleton : Singleton {
  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);

Here is a complete tutorial: http://www.bivis.com.br/2016/05/04/unity-reusable-singleton-tutorial/

1
ajouté

Il existe en fait un pseudo moyen officiel d'utiliser Singleton dans Unity. Voici l'explication, créez une classe Singleton et faites en sorte que vos scripts héritent de cette classe.

1
ajouté
É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.
ajouté l'auteur DMGregory, source

Nous avons effectué une bonne implémentation de Singleton sur Unity Community. Veuillez vérifier, je suis sûr que vous le trouverez mieux.

J'espère que cela aide, merci!

0
ajouté