Recherche…


Remarques

Bien qu'il existe des écoles de pensée qui expliquent pourquoi l'utilisation non restreinte de Singletons est une mauvaise idée, par exemple Singleton sur gameprogrammingpatterns.com , il peut arriver que vous souhaitiez conserver un objet GameObject dans Unity sur plusieurs scènes (par exemple pour une musique de fond transparente). tout en veillant à ce qu’il n’existe pas plus d’une instance; un cas d'utilisation parfait pour un Singleton.

En ajoutant ce script à un objet GameObject, une fois instancié (par exemple en l'incluant n'importe où dans une scène), il restera actif dans les scènes et une seule instance existera jamais.


Les instances ScriptableObject ( UnityDoc ) fournissent une alternative valide à Singletons pour certains cas d'utilisation. Bien qu'ils n'appliquent pas implicitement la règle d'instance unique, ils conservent leur état entre les scènes et fonctionnent bien avec le processus de sérialisation Unity. Ils favorisent également l' inversion du contrôle au fur et à mesure que les dépendances sont injectées via l'éditeur .

// MyAudioManager.cs
using UnityEngine;

[CreateAssetMenu] // Remember to create the instance in editor
public class MyAudioManager : ScriptableObject {
    public void PlaySound() {}
}
// MyGameObject.cs
using UnityEngine;

public class MyGameObject : MonoBehaviour
{
    [SerializeField]
    MyAudioManager audioManager; //Insert through Inspector

    void OnEnable()
    {
        audioManager.PlaySound();
    }
}

Lectures complémentaires

Implémentation à l'aide de RuntimeInitializeOnLoadMethodAttribute

Depuis Unity 5.2.5, il est possible d'utiliser RuntimeInitializeOnLoadMethodAttribute pour exécuter la logique d'initialisation en contournant l' ordre d'exécution MonoBehaviour . Cela permet de créer une implémentation plus propre et plus robuste:

using UnityEngine;

sealed class GameDirector : MonoBehaviour
{
    // Because of using RuntimeInitializeOnLoadMethod attribute to find/create and
    // initialize the instance, this property is accessible and
    // usable even in Awake() methods.
    public static GameDirector Instance
    {
        get; private set;
    }

    // Thanks to the attribute, this method is executed before any other MonoBehaviour
    // logic in the game.
    [RuntimeInitializeOnLoadMethod(RuntimeInitializeLoadType.BeforeSceneLoad)]
    static void OnRuntimeMethodLoad()
    {
        var instance = FindObjectOfType<GameDirector>();

        if (instance == null)
            instance = new GameObject("Game Director").AddComponent<GameDirector>();

        DontDestroyOnLoad(instance);

        Instance = instance;
    }

    // This Awake() will be called immediately after AddComponent() execution
    // in the OnRuntimeMethodLoad(). In other words, before any other MonoBehaviour's
    // in the scene will begin to initialize.
    private void Awake()
    {
        // Initialize non-MonoBehaviour logic, etc.
        Debug.Log("GameDirector.Awake()", this);
    }
}

L'ordre d'exécution résultant:

  1. GameDirector.OnRuntimeMethodLoad() démarré ...
  2. GameDirector.Awake()
  3. GameDirector.OnRuntimeMethodLoad() terminé.
  4. OtherMonoBehaviour1.Awake()
  5. OtherMonoBehaviour2.Awake() , etc.

Un simple MonoBehaviour Singleton dans Unity C #

Dans cet exemple, une instance statique privée de la classe est déclarée au début.

La valeur d'un champ statique est partagée entre les instances, donc si une nouvelle instance de cette classe est créée, le if trouvera une référence au premier objet Singleton, détruisant la nouvelle instance (ou son objet de jeu).

using UnityEngine;
        
public class SingletonExample : MonoBehaviour {

    private static SingletonExample _instance;
    
    void Awake(){

        if (_instance == null){

            _instance = this;
            DontDestroyOnLoad(this.gameObject);
    
            //Rest of your Awake code
    
        } else {
            Destroy(this);
        }
    }

    //Rest of your class code

}

Advanced Unity Singleton

Cet exemple combine plusieurs variantes de MonoBehaviour singletons trouvées sur Internet en un seul et vous permet de modifier son comportement en fonction de champs statiques globaux.

Cet exemple a été testé avec Unity 5. Pour utiliser ce singleton, il suffit de l'étendre comme suit: public class MySingleton : Singleton<MySingleton> {} . Vous devrez peut-être également remplacer AwakeSingleton pour l'utiliser au lieu de Awake habituel. Pour plus de réglages, changez les valeurs par défaut des champs statiques comme décrit ci-dessous.


  1. Cette implémentation utilise l'attribut DisallowMultipleComponent pour conserver une instance par GameObject.
  2. Cette classe est abstraite et ne peut être étendue. Il contient également une méthode virtuelle AwakeSingleton qui doit être remplacée au lieu d’implémenter un Awake normal.
  3. Cette implémentation est thread-safe.
  4. Ce singleton est optimisé. En utilisant un drapeau instantiated au lieu d'une instance null, nous évitons la surcharge liée à l'implémentation de l'opérateur == par Unity. ( Lire la suite )
  5. Cette implémentation n'autorise aucun appel à l'instance singleton lorsqu'elle est sur le point d'être détruite par Unity.
  6. Ce singleton est livré avec les options suivantes:
  • FindInactive : recherche d'autres instances de composants du même type attachées à GameObject inactif.
  • Persist : Persist si le composant doit rester actif entre les scènes.
  • DestroyOthers : DestroyOthers -il détruire tout autre composant du même type et n'en conserver qu'un seul?
  • Lazy : s'il faut définir l'instance singleton "à la volée" (dans Awake ) ou seulement "à la demande" (quand getter est appelé).
using UnityEngine;

[DisallowMultipleComponent]
public abstract class Singleton<T> : MonoBehaviour where T : Singleton<T>
{
    private static volatile T instance;
    // thread safety
    private static object _lock = new object();
    public static bool FindInactive = true;
    // Whether or not this object should persist when loading new scenes. Should be set in Init().
    public static bool Persist;
    // Whether or not destory other singleton instances if any. Should be set in Init().
    public static bool DestroyOthers = true;
    // instead of heavy comparision (instance != null)
    // http://blogs.unity3d.com/2014/05/16/custom-operator-should-we-keep-it/
    private static bool instantiated;

    private static bool applicationIsQuitting;

    public static bool Lazy;

    public static T Instance
    {
        get
        {
            if (applicationIsQuitting)
            {
                Debug.LogWarningFormat("[Singleton] Instance '{0}' already destroyed on application quit. Won't create again - returning null.", typeof(T));
                return null;
            }
            lock (_lock)
            {
                if (!instantiated)
                {
                    Object[] objects;
                    if (FindInactive) { objects = Resources.FindObjectsOfTypeAll(typeof(T)); }
                    else { objects = FindObjectsOfType(typeof(T)); }
                    if (objects == null || objects.Length < 1)
                    {
                        GameObject singleton = new GameObject();
                        singleton.name = string.Format("{0} [Singleton]", typeof(T));
                        Instance = singleton.AddComponent<T>();
                        Debug.LogWarningFormat("[Singleton] An Instance of '{0}' is needed in the scene, so '{1}' was created{2}", typeof(T), singleton.name, Persist ? " with DontDestoryOnLoad." : ".");
                    }
                    else if (objects.Length >= 1)
                    {
                        Instance = objects[0] as T;
                        if (objects.Length > 1)
                        {
                            Debug.LogWarningFormat("[Singleton] {0} instances of '{1}'!", objects.Length, typeof(T));
                            if (DestroyOthers)
                            {
                                for (int i = 1; i < objects.Length; i++)
                                {
                                    Debug.LogWarningFormat("[Singleton] Deleting extra '{0}' instance attached to '{1}'", typeof(T), objects[i].name);
                                    Destroy(objects[i]);
                                }
                            }
                        }
                        return instance;
                    }
                }
                return instance;
            }
        }
        protected set
        {
            instance = value;
            instantiated = true;
            instance.AwakeSingleton();
            if (Persist) { DontDestroyOnLoad(instance.gameObject); }
        }
    }

    // if Lazy = false and gameObject is active this will set instance
    // unless instance was called by another Awake method
    private void Awake()
    {
        if (Lazy) { return; }
        lock (_lock)
        {
            if (!instantiated)
            {
                Instance = this as T;
            }
            else if (DestroyOthers && Instance.GetInstanceID() != GetInstanceID())
            {
                Debug.LogWarningFormat("[Singleton] Deleting extra '{0}' instance attached to '{1}'", typeof(T), name);
                Destroy(this);
            }
        }
    }
    
    // this might be called for inactive singletons before Awake if FindInactive = true
    protected virtual void AwakeSingleton() {}

    protected virtual void OnDestroy()
    {
        applicationIsQuitting = true;
        instantiated = false;
    }
}

Mise en œuvre de singleton via la classe de base

Dans les projets comportant plusieurs classes singleton (comme c'est souvent le cas), il peut être propre et pratique d'abstraire le comportement singleton dans une classe de base:

using UnityEngine;
using System.Collections.Generic;
using System;

public abstract class MonoBehaviourSingleton<T> : MonoBehaviour {
    
    private static Dictionary<Type, object> _singletons
        = new Dictionary<Type, object>();

    public static T Instance {
        get {
            return (T)_singletons[typeof(T)];
        }
    }

    void OnEnable() {
        if (_singletons.ContainsKey(GetType())) {
            Destroy(this);
        } else {
            _singletons.Add(GetType(), this);
            DontDestroyOnLoad(this);
        }
    }
}

Un MonoBehaviour peut alors implémenter le modèle singleton en étendant MonoBehaviourSingleton. Cette approche permet d'utiliser le modèle avec une empreinte minimale sur le Singleton lui-même:

using UnityEngine;
using System.Collections;

public class SingletonImplementation : MonoBehaviourSingleton<SingletonImplementation> {

    public string Text= "String Instance";

    // Use this for initialisation
    IEnumerator Start () {
        var demonstration = "SingletonImplementation.Start()\n" +
                            "Note that the this text logs only once and\n"
                            "only one class instance is allowed to exist.";
        Debug.Log(demonstration);
        yield return new WaitForSeconds(2f);
        var secondInstance = new GameObject();
        secondInstance.AddComponent<SingletonImplementation>();
    }
   
}

Notez que l'un des avantages du modèle singleton est qu'il est possible d'accéder à une référence à l'instance de manière statique:

// Logs: String Instance
Debug.Log(SingletonImplementation.Instance.Text);

Gardez à l'esprit que cette pratique doit être minimisée afin de réduire le couplage. Cette approche a également un faible coût de performance en raison de l'utilisation de Dictionary, mais comme cette collection ne peut contenir qu'une seule instance de chaque classe de singleton, le compromis en termes de principe DRY (Don't Repeat Yourself), de lisibilité et la commodité est petite.

Motif Singleton utilisant le système Unitys Entity-Component

L'idée principale est d'utiliser GameObjects pour représenter les singletons, ce qui présente de nombreux avantages:

  • Réduit au minimum la complexité mais supporte des concepts comme l'injection de dépendance
  • Les singletons ont un cycle de vie Unity normal dans le cadre du système Entity-Component
  • Les singletons peuvent être chargés et mis en cache localement lorsque cela est nécessaire (par exemple dans les boucles de mise à jour)
  • Pas de champs statiques nécessaires
  • Pas besoin de modifier les MonoBehaviours / Composants existants pour les utiliser comme Singletons
  • Facile à réinitialiser (il suffit de détruire le Singletons GameObject), sera à nouveau chargé lors de la prochaine utilisation
  • Mock facile à injecter (juste l'initialiser avec la maquette avant de l'utiliser)
  • Inspection et configuration avec l'éditeur Unity normal et peut déjà se produire à l'heure de l'éditeur ( Capture d'écran d'un Singleton accessible dans l'éditeur Unity )

Test.cs (qui utilise l'exemple singleton):

using UnityEngine;
using UnityEngine.Assertions;

public class Test : MonoBehaviour {
    void Start() {
        ExampleSingleton singleton = ExampleSingleton.instance;
        Assert.IsNotNull(singleton); // automatic initialization on first usage
        Assert.AreEqual("abc", singleton.myVar1);
        singleton.myVar1 = "123";
        // multiple calls to instance() return the same object:
        Assert.AreEqual(singleton, ExampleSingleton.instance); 
        Assert.AreEqual("123", ExampleSingleton.instance.myVar1);
    }
}

ExampleSingleton.cs (qui contient un exemple et la classe Singleton actuelle):

using UnityEngine;
using UnityEngine.Assertions;

public class ExampleSingleton : MonoBehaviour {
    public static ExampleSingleton instance { get { return Singleton.get<ExampleSingleton>(); } }
    public string myVar1 = "abc";
    public void Start() { Assert.AreEqual(this, instance, "Singleton more than once in scene"); } 
}

/// <summary> Helper that turns any MonBehaviour or other Component into a Singleton </summary>
public static class Singleton {
    public static T get<T>() where T : Component {
        return GetOrAddGo("Singletons").GetOrAddChild("" + typeof(T)).GetOrAddComponent<T>();
    }
    private static GameObject GetOrAddGo(string goName) {
        var go = GameObject.Find(goName);
        if (go == null) { return new GameObject(goName); }
        return go;
    }
}

public static class GameObjectExtensionMethods { 
    public static GameObject GetOrAddChild(this GameObject parentGo, string childName) {
        var childGo = parentGo.transform.FindChild(childName);
        if (childGo != null) { return childGo.gameObject; } // child found, return it
        var newChild = new GameObject(childName);        // no child found, create it
        newChild.transform.SetParent(parentGo.transform, false); // add it to parent
        return newChild;
    }

    public static T GetOrAddComponent<T>(this GameObject parentGo) where T : Component {
        var comp = parentGo.GetComponent<T>();
        if (comp == null) { return parentGo.AddComponent<T>(); }
        return comp;
    }
}

Les deux méthodes d'extension pour GameObject sont également utiles dans d'autres situations, si vous n'en avez pas besoin, déplacez-les dans la classe Singleton et rendez-les privées.

Classe Singleton basée sur MonoBehaviour & ScriptableObject

La plupart des exemples Singleton utilisent MonoBehaviour comme classe de base. Le principal inconvénient est que cette classe Singleton ne vit que pendant l'exécution. Cela a des inconvénients:

  • Il n'y a aucun moyen de modifier directement les champs singleton autres que la modification du code.
  • Aucun moyen de stocker une référence à d'autres actifs sur le Singleton.
  • Aucun moyen de définir le singleton comme destination d'un événement Unity UI. Je finis par utiliser ce que j'appelle des "composants proxy" dont la seule proposition est d'avoir 1 méthodes de ligne appelant "GameManager.Instance.SomeGlobalMethod ()".

Comme indiqué dans les remarques, il existe des implémentations qui tentent de résoudre ce problème en utilisant ScriptableObjects comme classe de base, mais perdent les avantages d'exécution du MonoBehaviour. Cette implémentation résout ce problème en utilisant un scriptableObject en tant que classe de base et un comportement mono associé lors de l'exécution:

  • C'est un atout pour que ses propriétés puissent être mises à jour dans l'éditeur comme n'importe quel autre actif Unity.
  • Il joue bien avec le processus de sérialisation Unity.
  • Est-il possible d'attribuer des références sur le singleton à d'autres actifs de l'éditeur (les dépendances sont injectées via l'éditeur).
  • Les événements Unity peuvent appeler directement des méthodes sur le Singleton.
  • Peut l'appeler de n'importe où dans la base de code en utilisant "SingletonClassName.Instance"
  • A accès aux événements et méthodes MonoBehaviour en cours d'exécution, tels que: Mise à jour, Réveil, Démarrer, FixedUpdate, StartCoroutine, etc.
/************************************************************
 * Better Singleton by David Darias
 * Use as you like - credit where due would be appreciated :D
 * Licence: WTFPL V2, Dec 2014
 * Tested on Unity v5.6.0 (should work on earlier versions)
 * 03/02/2017 - v1.1 
 * **********************************************************/

using System;
using UnityEngine;
using SingletonScriptableObjectNamespace;

public class SingletonScriptableObject<T> : SingletonScriptableObjectNamespace.BehaviourScriptableObject where T : SingletonScriptableObjectNamespace.BehaviourScriptableObject
{
    //Private reference to the scriptable object
    private static T _instance;
    private static bool _instantiated;
    public static T Instance
    {
        get
        {
            if (_instantiated) return _instance;
            var singletonName = typeof(T).Name;
            //Look for the singleton on the resources folder
            var assets = Resources.LoadAll<T>("");
            if (assets.Length > 1) Debug.LogError("Found multiple " + singletonName + "s on the resources folder. It is a Singleton ScriptableObject, there should only be one.");
            if (assets.Length == 0)
            {
                _instance = CreateInstance<T>();
                Debug.LogError("Could not find a " + singletonName + " on the resources folder. It was created at runtime, therefore it will not be visible on the assets folder and it will not persist.");
            }
            else _instance = assets[0];
            _instantiated = true;
            //Create a new game object to use as proxy for all the MonoBehaviour methods
            var baseObject = new GameObject(singletonName);
            //Deactivate it before adding the proxy component. This avoids the execution of the Awake method when the the proxy component is added.
            baseObject.SetActive(false);
            //Add the proxy, set the instance as the parent and move to DontDestroyOnLoad scene
            SingletonScriptableObjectNamespace.BehaviourProxy proxy = baseObject.AddComponent<SingletonScriptableObjectNamespace.BehaviourProxy>();
            proxy.Parent = _instance;
            Behaviour = proxy;
            DontDestroyOnLoad(Behaviour.gameObject);
            //Activate the proxy. This will trigger the MonoBehaviourAwake. 
            proxy.gameObject.SetActive(true);
            return _instance;
        }
    }
    //Use this reference to call MonoBehaviour specific methods (for example StartCoroutine)
    protected static MonoBehaviour Behaviour;
    public static void BuildSingletonInstance() { SingletonScriptableObjectNamespace.BehaviourScriptableObject i = Instance; }
    private void OnDestroy(){ _instantiated = false; }
}

// Helper classes for the SingletonScriptableObject
namespace SingletonScriptableObjectNamespace
{
    #if UNITY_EDITOR
    //Empty custom editor to have cleaner UI on the editor.
    using UnityEditor;
    [CustomEditor(typeof(BehaviourProxy))]
    public class BehaviourProxyEditor : Editor
    {
        public override void OnInspectorGUI(){}
    }
    
    #endif
    
    public class BehaviourProxy : MonoBehaviour
    {
        public IBehaviour Parent;

        public void Awake() { if (Parent != null) Parent.MonoBehaviourAwake(); }
        public void Start() { if (Parent != null) Parent.Start(); }
        public void Update() { if (Parent != null) Parent.Update(); }
        public void FixedUpdate() { if (Parent != null) Parent.FixedUpdate(); }
    }

    public interface IBehaviour
    {
        void MonoBehaviourAwake();
        void Start();
        void Update();
        void FixedUpdate();
    }

    public class BehaviourScriptableObject : ScriptableObject, IBehaviour
    {
        public void Awake() { ScriptableObjectAwake(); }
        public virtual void ScriptableObjectAwake() { }
        public virtual void MonoBehaviourAwake() { }
        public virtual void Start() { }
        public virtual void Update() { }
        public virtual void FixedUpdate() { }
    }
}

Ici, il y a un exemple de classe singleton GameManager utilisant SingletonScriptableObject (avec beaucoup de commentaires):

using System;
using System.Collections;
using System.Collections.Generic;
using UnityEngine;

//this attribute is optional but recommended. It will allow the creation of the singleton via the asset menu.
//the singleton asset should be on the Resources folder.
[CreateAssetMenu(fileName = "GameManager", menuName = "Game Manager", order = 0)]
public class GameManager : SingletonScriptableObject<GameManager> {

    //any properties as usual
    public int Lives;
    public int Points;

    //optional (but recommended)
    //this method will run before the first scene is loaded. Initializing the singleton here
    //will allow it to be ready before any other GameObjects on every scene and will
    //will prevent the "initialization on first usage". 
    [RuntimeInitializeOnLoadMethod(RuntimeInitializeLoadType.BeforeSceneLoad)]
    public static void BeforeSceneLoad() { BuildSingletonInstance(); }

    //optional,
    //will run when the Singleton Scriptable Object is first created on the assets. 
    //Usually this happens on edit mode, not runtime. (the override keyword is mandatory for this to work)
    public override void ScriptableObjectAwake(){
        Debug.Log(GetType().Name + " created." );
    }

    //optional,
    //will run when the associated MonoBehavioir awakes. (the override keyword is mandatory for this to work)
    public override void MonoBehaviourAwake(){
        Debug.Log(GetType().Name + " behaviour awake." );

        //A coroutine example:
        //Singleton Objects do not have coroutines.
        //if you need to use coroutines use the atached MonoBehaviour
        Behaviour.StartCoroutine(SimpleCoroutine());
    }

    //any methods as usual
    private IEnumerator SimpleCoroutine(){
        while(true){
            Debug.Log(GetType().Name + " coroutine step." );
            yield return new WaitForSeconds(3);
        }
    }

    //optional,
    //Classic runtime Update method (the override keyword is mandatory for this to work).
    public override void Update(){

    }

    //optional,
    //Classic runtime FixedUpdate method (the override keyword is mandatory for this to work).
    public override void FixedUpdate(){

    }
}

/*
*  Notes:
*  - Remember that you have to create the singleton asset on edit mode before using it. You have to put it on the Resources folder and of course it should be only one. 
*  - Like other Unity Singleton this one is accessible anywhere in your code using the "Instance" property i.e: GameManager.Instance
*/


Modified text is an extract of the original Stack Overflow Documentation
Sous licence CC BY-SA 3.0
Non affilié à Stack Overflow