unity3d
Singletons in eenheid
Zoeken…
Opmerkingen
Hoewel er stromingen zijn die dwingende argumenten geven waarom onbeperkt gebruik van Singletons een slecht idee is, bijv. Singleton op gameprogrammingpatterns.com , zijn er gelegenheden waarbij je een GameObject in Unity over meerdere scènes wilt kunnen behouden (bijv. Voor naadloze achtergrondmuziek) terwijl ervoor wordt gezorgd dat niet meer dan één instantie kan bestaan; een perfecte use case voor een Singleton.
Door dit script aan een GameObject toe te voegen, zodra het is geïnstantieerd (bijvoorbeeld door het ergens in een scène op te nemen), blijft het actief in alle scènes en zal er slechts één instantie bestaan.
Instanties van ScriptableObject ( UnityDoc ) bieden een geldig alternatief voor Singletons voor sommige gebruikssituaties. Hoewel ze niet impliciet de regel van één instantie afdwingen, behouden ze hun status tussen scènes en spelen ze netjes met het Unity-serialisatieproces. Ze promoten ook Inversion of Control omdat afhankelijkheden via de editor worden geïnjecteerd .
// 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();
}
}
Verder lezen
Implementatie met RuntimeInitializeOnLoadMethodAttribute
Sinds Unity 5.2.5 is het mogelijk om RuntimeInitializeOnLoadMethodAttribute te gebruiken om initialisatielogica uit te voeren zonder de volgorde van uitvoering van MonoBehaviour te omzeilen. Het biedt een manier om een meer schone en robuuste implementatie te creëren:
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);
}
}
De resulterende volgorde van uitvoering:
-
GameDirector.OnRuntimeMethodLoad()
gestart ... -
GameDirector.Awake()
-
GameDirector.OnRuntimeMethodLoad()
voltooid. -
OtherMonoBehaviour1.Awake()
-
OtherMonoBehaviour2.Awake()
, etc.
Een eenvoudige Singleton MonoBehaviour in Unity C #
In dit voorbeeld wordt een statische privé-instantie van de klasse aan het begin verklaard.
De waarde van een statisch veld wordt gedeeld tussen instanties, dus als een nieuwe instantie van deze klasse wordt gemaakt, vindt de if
een verwijzing naar het eerste Singleton-object, waardoor de nieuwe instantie (of het game-object) wordt vernietigd.
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
}
Geavanceerde eenheid Singleton
Dit voorbeeld combineert meerdere varianten van MonoBehaviour-singletons die op internet zijn gevonden in één en laat u het gedrag veranderen, afhankelijk van globale statische velden.
Dit voorbeeld is getest met Unity 5. Om deze singleton te gebruiken, hoeft u deze alleen als volgt uit te breiden: public class MySingleton : Singleton<MySingleton> {}
. Mogelijk moet u AwakeSingleton
overschrijven AwakeSingleton
te gebruiken in plaats van gewoon Awake
. Wijzig de standaardwaarden van statische velden zoals hieronder beschreven voor verdere aanpassingen.
- Deze implementatie maakt gebruik van het kenmerk DisallowMultipleComponent om één exemplaar per GameObject te behouden.
- Deze klasse is abstract en kan alleen worden uitgebreid. Het bevat ook een virtuele methode
AwakeSingleton
die moet worden genegeerd in plaats van de normaleAwake
implementeren. - Deze implementatie is thread-safe.
- Deze singleton is geoptimaliseerd. Door
instantiated
vlag te gebruiken in plaats van bijvoorbeeld nulcontrole, vermijden we de overhead die gepaard gaat met de implementatie van==
door Unity. ( Lees meer ) - Deze implementatie staat geen enkele oproep toe aan de singleton-instantie wanneer deze op het punt staat vernietigd te worden door Unity.
- Deze singleton wordt geleverd met de volgende opties:
-
FindInactive
: zoeken naar andere exemplaren van componenten van hetzelfde type die zijn gekoppeld aan inactief GameObject. -
Persist
: component in leven houden tussen scènes. -
DestroyOthers
: of u andere componenten van hetzelfde type wilt vernietigen en er slechts één wilt behouden. -
Lazy
: of een singletoninstantie 'on the fly' (inAwake
) of alleen 'on demand' (wanneer getter wordt genoemd) moet worden ingesteld.
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;
}
}
Singleton Implementatie via basisklasse
In projecten met meerdere singleton-klassen (zoals vaak het geval is), kan het eenvoudig en handig zijn om het singleton-gedrag te abstraheren naar een basisklasse:
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);
}
}
}
Een MonoBehaviour kan vervolgens het singleton-patroon implementeren door MonoBehaviourSingleton uit te breiden. Met deze aanpak kan het patroon worden gebruikt met een minimale voetafdruk op de Singleton zelf:
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>();
}
}
Merk op dat een van de voordelen van het singleton-patroon is dat een verwijzing naar de instantie statisch toegankelijk is:
// Logs: String Instance
Debug.Log(SingletonImplementation.Instance.Text);
Houd er echter rekening mee dat deze oefening tot een minimum moet worden beperkt om de koppeling te verminderen. Deze aanpak brengt ook een geringe prestatiekost met zich mee vanwege het gebruik van Dictionary, maar omdat deze collectie slechts één exemplaar van elke singleton-klasse kan bevatten, de afweging in termen van het DRY-principe (Don't Repeat Yourself), leesbaarheid en gemak is klein.
Singleton-patroon met Unitys Entity-Component-systeem
Het kernidee is om GameObjects te gebruiken om singletons te vertegenwoordigen, wat meerdere voordelen heeft:
- Houdt complexiteit tot een minimum beperkt, maar ondersteunt concepten zoals afhankelijkheidsinjectie
- Singletons hebben een normale Unity-levenscyclus als onderdeel van het Entity-Component-systeem
- Singletons kunnen lui worden geladen en lokaal in de cache worden opgeslagen waar dat nodig is (bijvoorbeeld in updatelussen)
- Geen statische velden nodig
- Het is niet nodig om bestaande MonoBehaviours / componenten te wijzigen om ze als singletons te gebruiken
- Eenvoudig te resetten (vernietig gewoon de Singletons GameObject), wordt bij het volgende gebruik weer lui geladen
- Eenvoudig te injecteren mocks (initialiseer het gewoon met de mock voordat u het gebruikt)
- Inspectie en configuratie met behulp van de normale Unity-editor en kan al tijdens de editortijd gebeuren ( Screenshot van een Singleton toegankelijk in de Unity-editor )
Test.cs (die het voorbeeld singleton gebruikt):
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 (die een voorbeeld en de werkelijke Singleton-klasse bevat):
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;
}
}
De twee uitbreidingsmethoden voor GameObject zijn ook nuttig in andere situaties, als je ze niet nodig hebt, verplaats ze dan naar de Singleton-klasse en maak ze privé.
MonoBehaviour & ScriptableObject-gebaseerde Singleton Class
De meeste Singleton-voorbeelden gebruiken MonoBehaviour als de basisklasse. Het grootste nadeel is dat deze Singleton-klasse alleen tijdens runtime leeft. Dit heeft enkele nadelen:
- Er is geen andere manier om de singleton-velden rechtstreeks te bewerken dan de code te wijzigen.
- Geen manier om een verwijzing naar andere middelen op de Singleton op te slaan.
- Geen manier om de singleton in te stellen als de bestemming van een Unity UI-evenement. Ik gebruik uiteindelijk wat ik "Proxy-componenten" noem die als enige voorstellen is om 1-regelmethoden te hebben die "GameManager.Instance.SomeGlobalMethod ()" noemen.
Zoals vermeld in de opmerkingen zijn er implementaties die dit proberen op te lossen met ScriptableObjects als basisklasse, maar de voordelen van de uitvoeringstijd van de MonoBehaviour verliezen. Deze implementatie lost dit probleem op door een ScriptableObject te gebruiken als basisklasse en een bijbehorende MonoBehavior tijdens runtime:
- Het is een activum, zodat de eigenschappen ervan net als elk ander Unity-activum in de editor kunnen worden bijgewerkt.
- Het speelt mooi samen met het Unity-serialisatieproces.
- Is het mogelijk om verwijzingen op de singleton toe te wijzen aan andere activa van de editor (afhankelijkheden worden geïnjecteerd via de editor).
- Unity-evenementen kunnen rechtstreeks methoden op de Singleton oproepen.
- Kan het overal in de codebase aanroepen met "SingletonClassName.Instance"
- Heeft toegang tot MonoBehaviour-evenementen en -methoden zoals: Update, Awake, Start, 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() { }
}
}
Hier is een voorbeeld van de Singleton-klasse GameManager met het SingletonScriptableObject (met veel opmerkingen):
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
*/