unity3d
Синглтоны в единстве
Поиск…
замечания
Несмотря на то, что есть мысли, которые приводят к аргументам, почему безусловное использование Singletons - плохая идея, например Singleton on gameprogrammingpatterns.com , бывают случаи, когда вы захотите сохранить GameObject в Unity в нескольких сценах (например, для бесшовной фоновой музыки) обеспечивая при этом существование не более одного экземпляра; идеальный вариант использования для Singleton.
Добавив этот скрипт в GameObject, как только он будет создан (например, включив его где угодно в сцену), он останется активным во всех сценариях, и только один экземпляр будет когда-либо существовать.
ScriptableObject ( UnityDoc ) экземпляры обеспечивают действительную альтернативу Одиночки для некоторых случаев применения. Хотя они не подразумевают принудительное применение правила единственного экземпляра, они сохраняют свое состояние между сценами и прекрасно играют с процессом сериализации Unity. Они также способствуют инверсии контроля, поскольку зависимости вводятся через редактор .
// 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();
}
}
дальнейшее чтение
Выполнение с использованием RuntimeInitializeOnLoadMethodAttribute
Начиная с Unity 5.2.5 , можно использовать RuntimeInitializeOnLoadMethodAttribute для выполнения логики инициализации в обход порядка исполнения MonoBehaviour . Он обеспечивает возможность создания более чистой и надежной реализации:
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);
}
}
Результирующий порядок выполнения:
-
GameDirector.OnRuntimeMethodLoad()
запущен ... -
GameDirector.Awake()
-
GameDirector.OnRuntimeMethodLoad()
завершено. -
OtherMonoBehaviour1.Awake()
-
OtherMonoBehaviour2.Awake()
и т. Д.
Простой Singleton MonoBehaviour в Unity C #
В этом примере частный статический экземпляр класса объявляется в начале.
Значение статического поля распределяется между экземплярами, поэтому , если новый экземпляр этого класса получает создан , if
будет найти ссылку на первый объект Singleton, разрушив новый экземпляр (или его игровой объект).
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
}
Продвинутое единство Singleton
В этом примере сочетаются несколько вариантов однопользовательских моноблоков MonoBehaviour, найденных в Интернете, в одном и позволяют изменять его поведение в зависимости от глобальных статических полей.
Этот пример был протестирован с использованием Unity 5. Чтобы использовать этот синглтон, все, что вам нужно сделать, это расширить его следующим образом: public class MySingleton : Singleton<MySingleton> {}
. Вам также может потребоваться переопределить AwakeSingleton
чтобы использовать его вместо обычного Awake
. Для дальнейшей настройки измените значения по умолчанию для статических полей, как описано ниже.
- Эта реализация использует атрибут DisallowMultipleComponent для хранения одного экземпляра на GameObject.
- Этот класс является абстрактным и может быть расширен только. Он также содержит один виртуальный метод
AwakeSingleton
который необходимо переопределить, вместо того чтобы реализовать обычныйAwake
. - Эта реализация является потокобезопасной.
- Этот синглтон оптимизирован. Используя
instantiated
флажка вместо экземпляра null check, мы избегаем накладных расходов, связанных с реализацией Unity оператора==
. ( Читать дальше ) - Эта реализация не позволяет вызвать вызовы одиночного экземпляра, когда он собирается уничтожить Unity.
- Этот синглтон поставляется со следующими параметрами:
-
FindInactive
: искать другие экземпляры компонентов того же типа, подключенные к неактивному GameObject. -
Persist
: сохранять ли живые объекты между сценами. -
DestroyOthers
: уничтожить ли какие-либо другие компоненты одного типа и сохранить только один. -
Lazy
: установить одиночный экземпляр «на лету» (вAwake
) или только «по требованию» (когда вызывается геттер).
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 через базовый класс
В проектах, которые имеют несколько одноэлементных классов (как это часто бывает), может быть чисто и удобно абстрагировать поведение singleton к базовому классу:
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);
}
}
}
MonoBehaviour может затем реализовать одноэлементный шаблон, расширив MonoBehaviourSingleton. Такой подход позволяет использовать шаблон с минимальным размером на сам Синглтон:
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>();
}
}
Обратите внимание, что одним из преимуществ шаблона singleton является то, что ссылка на экземпляр может быть получена статически:
// Logs: String Instance
Debug.Log(SingletonImplementation.Instance.Text);
Имейте в виду, что эту практику следует минимизировать, чтобы уменьшить сцепление. Этот подход также имеет небольшую производительность из-за использования словаря, но поскольку эта коллекция может содержать только один экземпляр каждого одноэлементного класса, компромисс с точки зрения принципа DRY («Не повторяйте себя»), читаемости и удобный маленький.
Шаблон Singleton с использованием системы Entity-Componity Unitys
Основная идея состоит в том, чтобы использовать GameObjects для представления синглетов, которые имеют несколько преимуществ:
- Сохраняет сложность до минимума, но поддерживает концепции, такие как инъекция зависимостей
- Синглтоны имеют нормальный жизненный цикл Unity как часть системы Entity-Component
- Синглтоны могут быть лениво загружены и кэшированы локально, где это необходимо (например, в циклах обновления)
- Не требуется статических полей
- Не нужно изменять существующие MonoBehaviours / Components, чтобы использовать их как Singletons
- Легко перезагрузиться (просто уничтожьте GameObject Singletons), будет снова загружен с лени при следующем использовании
- Легко вводить насмешки (просто инициализируйте его макетом перед его использованием)
- Проверка и настройка с использованием обычного редактора Unity и может произойти уже во время редактора ( Снимок экрана Singleton, доступный в редакторе Unity )
Test.cs (который использует пример 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 (который содержит пример и действительный класс Singleton):
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;
}
}
Два метода расширения для GameObject полезны и в других ситуациях, если вам не нужны они, перемещая их внутри класса Singleton и делая их закрытыми.
MonoBehaviour & ScriptableObject на основе Singleton Class
Большинство примеров Singleton используют MonoBehaviour как базовый класс. Основным недостатком является то, что этот класс Singleton работает только во время выполнения. Это имеет некоторые недостатки:
- Нет способа прямого редактирования полей singleton, кроме изменения кода.
- Никакой способ сохранить ссылку на другие активы на Singleton.
- Невозможно установить синглтон в качестве места назначения события Unity UI. Я в конечном итоге использую то, что я называю «Прокси-компоненты», что его единственное предложение состоит в том, чтобы иметь 1 линейный метод, который вызывает «GameManager.Instance.SomeGlobalMethod ()».
Как отмечалось в примечаниях, существуют реализации, которые пытаются решить это, используя ScriptableObjects в качестве базового класса, но теряют преимущества MonoBehaviour во время выполнения. Эта реализация решает эти проблемы, используя ScriptableObject как базовый класс и связанное с ним MonoBehavior во время выполнения:
- Это актив, поэтому его свойства могут быть обновлены в редакторе, как и любой другой ресурс Unity.
- Он отлично работает с процессом сериализации Unity.
- Можно назначить ссылки на singleton другим ресурсам из редактора (зависимости вводятся через редактор).
- События Unity могут напрямую вызывать методы на Singleton.
- Можно вызвать его из любой точки в базе кода, используя «SingletonClassName.Instance»
- Имеет доступ к событиям времени MonoBehaviour и таким методам, как: Обновление, Пробуждение, Старт, ФиксUpdate, StartCoroutine и т. Д.
/************************************************************
* 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() { }
}
}
Здесь приведен пример singleton-класса GameManager с использованием SingletonScriptableObject (с большим количеством комментариев):
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
*/