unity3d
Singletons in der Einheit
Suche…
Bemerkungen
Zwar gibt es Denkschulen, die überzeugende Argumente dafür liefern , warum der uneingeschränkte Einsatz von Singletons eine schlechte Idee ist, z. B. Singleton auf gameprogrammingpatterns.com . Es gibt jedoch Situationen , in denen Sie ein GameObject in Unity über mehrere Szenen hinweg beibehalten möchten (z. B. für nahtlose Hintergrundmusik). wobei sichergestellt ist, dass nicht mehr als eine Instanz existieren kann; ein perfekter Anwendungsfall für ein Singleton.
Durch das Hinzufügen dieses Skripts zu einem GameObject bleibt es nach der Instanziierung (z. B. durch Einfügen an einer beliebigen Stelle in einer Szene) in allen Szenen aktiv, und es wird nur eine Instanz vorhanden sein.
ScriptableObject ( UnityDoc ) -Instanzen bieten für einige Anwendungsfälle eine gültige Alternative zu Singletons. Obwohl sie die Einzelinstanzregel nicht implizit erzwingen, behalten sie ihren Status zwischen den Szenen bei und spielen gut mit dem Unity-Serialisierungsprozess. Sie fördern auch die Inversion der Kontrolle, da Abhängigkeiten durch den Editor eingefügt werden .
// 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();
}
}
Lesen Sie weiter
Implementierung mit RuntimeInitializeOnLoadMethodAttribute
Seit Unity 5.2.5 ist es möglich, RuntimeInitializeOnLoadMethodAttribute zum Ausführen der Initialisierungslogik unter Umgehung der MonoBehaviour-Reihenfolge der Ausführung zu verwenden . Es bietet eine Möglichkeit, eine sauberere und robustere Implementierung zu erstellen:
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);
}
}
Die resultierende Reihenfolge der Ausführung:
-
GameDirector.OnRuntimeMethodLoad()
gestartet ... -
GameDirector.Awake()
-
GameDirector.OnRuntimeMethodLoad()
abgeschlossen. -
OtherMonoBehaviour1.Awake()
-
OtherMonoBehaviour2.Awake()
usw.
Ein einfaches Singleton MonoBehaviour in Unity C #
In diesem Beispiel wird eine private statische Instanz der Klasse zu Beginn deklariert.
Der Wert eines statischen Feldes wird von Instanzen gemeinsam genutzt. Wenn eine neue Instanz dieser Klasse erstellt wird, findet das if
eine Referenz auf das erste Singleton-Objekt, wodurch die neue Instanz (oder ihr Spielobjekt) zerstört wird.
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
In diesem Beispiel werden mehrere Varianten der im Internet gefundenen MonoBehaviour-Singletons zu einer einzigen kombiniert, und Sie können ihr Verhalten in Abhängigkeit von globalen statischen Feldern ändern.
Dieses Beispiel wurde mit Unity 5 getestet. Um dieses Singleton zu verwenden, müssen Sie es nur wie folgt erweitern: public class MySingleton : Singleton<MySingleton> {}
. Sie können auch außer Kraft setzen müssen AwakeSingleton
es üblich , anstatt zu verwenden Awake
. Ändern Sie für weitere Einstellungen die Standardwerte für statische Felder wie unten beschrieben.
- Diese Implementierung verwendet das DisallowMultipleComponent- Attribut, um eine Instanz pro GameObject beizubehalten.
- Diese Klasse ist abstrakt und kann nur erweitert werden. Es enthält auch eine virtuelle Methode
AwakeSingleton
, die überschrieben werden muss, anstatt normalesAwake
implementieren. - Diese Implementierung ist threadsicher.
- Dieses Singleton ist optimiert. Durch die Verwendung eines
instantiated
Flags anstelle einer Instanznullprüfung vermeiden wir den Overhead, der mit der Implementierung des Operators==
von Unity==
. ( Lesen Sie mehr ) - Diese Implementierung erlaubt keine Aufrufe an die Singleton-Instanz, wenn sie von Unity zerstört wird.
- Dieses Singleton bietet folgende Optionen:
-
FindInactive
:FindInactive
ob nach Instanzen von Komponenten desselben Typs gesucht werden soll, die mit dem inaktiven GameObject verbunden sind. -
Persist
: Ob Komponente zwischen Szenen am Leben erhaltenPersist
. -
DestroyOthers
: ob andere Komponenten desselben Typs zerstört werden sollen und nur eine behalten. -
Lazy
: Ob Singleton-Instanz "on the fly" (inAwake
) oder nur "on demand" (wenn Getter aufgerufen wird) gesetzt wird.
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 Implementierung durch Basisklasse
In Projekten mit mehreren Singleton-Klassen (wie dies häufig der Fall ist), kann es sauber und bequem sein, das Singleton-Verhalten in eine Basisklasse zu abstrahieren:
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);
}
}
}
Ein MonoBehaviour kann dann das Singleton-Muster durch Erweitern von MonoBehaviourSingleton implementieren. Dieser Ansatz ermöglicht die Verwendung des Musters mit einem minimalen Footprint auf dem Singleton selbst:
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>();
}
}
Beachten Sie, dass einer der Vorteile des Singleton-Musters darin besteht, dass auf einen Verweis auf die Instanz statisch zugegriffen werden kann:
// Logs: String Instance
Debug.Log(SingletonImplementation.Instance.Text);
Beachten Sie jedoch, dass diese Vorgehensweise minimiert werden sollte, um die Kopplung zu reduzieren. Dieser Ansatz bringt aufgrund der Verwendung von Dictionary auch einen geringen Performance-Preis mit sich. Da diese Auflistung jedoch nur eine Instanz jeder Einzelklasse enthalten kann, muss dies im Hinblick auf das DRY-Prinzip (Don't Repeat Yourself), Readability und Bequemlichkeit ist klein.
Singleton-Pattern mit Unitys Entity-Component-System
Die Kernidee besteht darin, GameObjects zur Darstellung von Singletons zu verwenden, was mehrere Vorteile bietet:
- Reduziert die Komplexität auf ein Minimum, unterstützt jedoch Konzepte wie Abhängigkeitseinspritzung
- Singletons haben als Teil des Entity-Component-Systems einen normalen Unity-Lebenszyklus
- Singletons können lokal geladen werden, wo sie regelmäßig benötigt werden (z. B. in Update-Schleifen)
- Keine statischen Felder erforderlich
- Bestehende MonoBehaviours / -Komponenten müssen nicht modifiziert werden, um sie als Singletons zu verwenden
- Einfach zurückzusetzen (einfach das Singletons GameObject zerstören), wird bei der nächsten Verwendung wieder faul geladen
- Einfach zu injizierende Mocks (initialisieren Sie es einfach mit dem Mock, bevor Sie es verwenden)
- Überprüfung und Konfiguration mit dem normalen Unity-Editor und kann bereits zur Bearbeitungszeit erfolgen ( Screenshot eines Singleton im Unity-Editor abrufbar )
Test.cs (verwendet das Beispiel-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 (enthält ein Beispiel und die tatsächliche Singleton-Klasse):
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;
}
}
Die beiden Erweiterungsmethoden für GameObject sind auch in anderen Situationen hilfreich. Wenn Sie sie nicht brauchen, verschieben Sie sie in die Singleton-Klasse und machen sie privat.
MonoBehaviour & ScriptableObject-basierte Singleton-Klasse
Die meisten Singleton-Beispiele verwenden MonoBehaviour als Basisklasse. Der Hauptnachteil ist, dass diese Singleton-Klasse nur zur Laufzeit lebt. Dies hat einige Nachteile:
- Sie können die Singleton-Felder nicht direkt bearbeiten, sondern nur den Code ändern.
- Keine Möglichkeit, einen Verweis auf andere Assets im Singleton zu speichern.
- Keine Möglichkeit, das Singleton als Ziel eines Unity-UI-Ereignisses festzulegen. Am Ende verwende ich das, was ich als "Proxy-Komponenten" bezeichne. Sein einziger Vorschlag besteht darin, 1-Zeilen-Methoden zu haben, die "GameManager.Instance.SomeGlobalMethod ()" nennen.
Wie in den Anmerkungen angemerkt, gibt es Implementierungen, die versuchen, dieses Problem mithilfe von ScriptableObjects als Basisklasse zu lösen, ohne jedoch die Laufzeitvorteile des MonoBehaviour zu verlieren. Diese Implementierung löst diese Probleme, indem ein ScriptableObject als Basisklasse und ein zugeordnetes MonoBehavior zur Laufzeit verwendet werden:
- Es ist ein Asset, sodass seine Eigenschaften wie jeder andere Unity-Asset im Editor aktualisiert werden können.
- Es spielt sich gut mit dem Unity-Serialisierungsprozess ab.
- Es ist möglich, anderen Assets Referenzen aus dem Editor zuzuweisen (Abhängigkeiten werden über den Editor eingefügt).
- Unity-Ereignisse können Methoden direkt am Singleton aufrufen.
- Kann von überall in der Codebase mit "SingletonClassName.Instance" aufgerufen werden
- Hat Zugriff auf Laufzeit-MonoBehaviour-Ereignisse und -Methoden wie: Update, Awake, Start, FixedUpdate, StartCoroutine usw.
/************************************************************
* 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 gibt es eine Beispiel-GameManager-Singleton-Klasse, die das SingletonScriptableObject (mit vielen Kommentaren) verwendet:
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
*/