Unity3D: Автоматический агрегатор скриптов-менеджеров

Вступление


В этой статье речь пойдет об одном виде организации взаимодействия между скриптами-менеджерами (синглтонами именуемыми), а конкретно — использование отдельного класса-агрегатора, в котором содержаться ссылки на все instance менеджеров. Идея создать класс-агрегатор пришла мне в голову после прочтения этой статьи.

Задачи


Я пришел к выводу, что забивать своими собственными ручками менеджеры в класс-агрегатор каждый раз — это дико неудобно. При создании нового менеджера придется открывать класс-агрегатор и вносить изменения. При достаточном количестве опыта можно такие нудные процессы автоматизировать. Таким образом поставились задачи: автоматическое создание синглтонов и их автоматический сборщик.

Класс-Singleton


using UnityEngine;
public class Singleton<T>: MonoBehaviour where T: MonoBehaviour
{
	public static T instance { get; private set; }

	public void Awake()
	{
		if (instance == null)
		{
			instance = GetComponent<T>();
			ManagersAregator.addManager(instance);
		}
		else
		{
			Debug.Log($"<color=red>Удаление дубликата синглтона {typeof(T).ToString()}</color>");
			Destroy(gameObject);
		}
	}

	public void OnDestroy()
	{
		ManagersAregator.removeManager<T>();
	}
}

В классической реализации синглтона в Unity используется статическая переменная instance. В методе Awake() производится проверка, определен ли instance. Если нет, то получаем ссылку на экземпляр класса с помощью ключевого слова this. Но т.к. в конкретной реализации используется «шаблонная» переменная класса Т, использовать this не получилось. Но мы с легкостью можем получить компонент Т с объекта, на котором висит скрипт. Если же instance определен, то его необходимо уничтожить. Таким образом объект всегда будет на сцене в единственном экземпляре.

В методе Awake() в класс-агрегатор добавляется новый элемент, а в методе OnDestroy() элемент удаляется из класса-агрегатора.

Создание синглтона класса MyClass происходит так:

public class MyClass: Singleton<MyClass>

Если надо использовать метод Awake() в классе MyClass, то надо воспользоваться ключевым словом base (подробнее о base и наследовании):

new void Awake()
	{
		base.Awake(); //Вызывается метод Awake() класса-родителя
		//необходимый код
	}

Класс-агрегатор


using System.Collections.Generic;
using UnityEngine;

public static class ManagersAregator
{
	static Dictionary<string, MonoBehaviour> Managers = new Dictionary<string, MonoBehaviour>();

	public static void addManager<T>(T newManager)
	{
		string keyWord = typeof(T).ToString();

		if(Managers.ContainsKey(keyWord))
		{
			Debug.Log($"[ManagersAregator] Менеджер -{newManager}- с ключом -{keyWord}- уже существует");
		}
		else
		{
			Managers.Add(keyWord, newManager as MonoBehaviour);
			Debug.Log($"<color=green>[ManagersAregator] Добавлен новый менеджер -{newManager}- с ключом -{keyWord}-</color>");
		}
	}

	public static T getManager<T>(string callback) where T: Singleton<T>
	{
		string keyWord = typeof(T).ToString();

		if(Managers.ContainsKey(keyWord))
		{
			Debug.Log($"<color=yellow>[{callback}] Получение менеджера -{keyWord}-</color>");

			MonoBehaviour mbTemp = null;
			T manager = null;

			if(Managers.TryGetValue(keyWord, out mbTemp))
			{
				manager = (T)mbTemp;
				Debug.Log($"<color=green>[{callback}] Менеджер -{manager}- получен</color>");
			}
			else
			{
				Debug.Log($"<color=red>[{callback}] Ошибка получения менеджера -{keyWord}-</color>");
			}

			return manager;
		}

		Debug.Log($"<color=red>[ManagersAregator] Менеджер с ключом -{keyWord}- отсутствует в словаре.</color>");
		return null;
	}

	public static void removeManager<T>()
	{
		string keyWord = typeof(T).ToString();

		if(Managers.ContainsKey(keyWord))
		{
			Managers.Remove(keyWord);
			Debug.Log($"[ManagersAregator] Менеджер с ключом -{keyWord}- удален из словаря");
		}
		else
		{
			Debug.Log($"[ManagersAregator] Менеджер с ключом -{keyWord}- отсутствует в словаре.");
		}
	}
}

Класс является статическим, т.е. у него не может быть экземпляра на сцене. Таким образом он будет доступен из любой сцены в любое время.

Все скрипты-менеджеры (синглтоны) хранятся в словаре Managers. Ключом для каждого менеджера является имя класса этого менеджера. Возможно, новички в программировании зададут вопрос: «Ба, а что это словарь хранит MonoBehaviour, а все классы наследуются от Singleton?». Это хороший вопрос, ответ на который и является ключом к реализации автоматического агрегатора менеджеров любых классов.

В программировании существует понятие upcasting — преобразование типа к базовому классу. Благодаря тому, что все классы в Unity наследуются от MonoBehaviour, их можно апкастить к MonoBehabiour. Поэтому словарь Managers содержит только объекты класса MonoBehaviour.

Рассмотрим методы класса-агрегатора:

void addManager<T>(T newManager)

Этот метод вызывается в методе Awake() класса Singleton. Аргументом является статическая переменная класса instance. Далее создается ключ по имени класса, которому принадлежит instance, и менеджер добавляется в словарь.

T getManager<T>(string callback) where T: Singleton<T>

Функция принимает аргументом строку с именем класса, откуда вызывается метод. Это сделано исключительно для удобного дебага (в консоли отображается класс, откуда вызывается метод). Пример использования этого метода в класса AnotherMyClass:

public class AnotherMyClass: MonoBehaviour
	void Start()
	{
		string cb = GetType().ToString(); //Получение имени класса в качестве строки
		MyClass MC = ManagersAregator.getManager<MyClass >(cb);
	}

В консоли будет висеть сообщение: "Ху**я, переделывай [AnotherMyClass] Менеджер -MyClass- получен".

void removeManager<T>()

Удаляет из словаря менеджер типа Т, если он содержится в словаре.

Итоги


Фактически, из трех функций класса-агрегатора разработчику достаточно использовать только метод getManager. Особым плюсом является хорошая видимость сообщений дебага на все случаи жизни. По желанию, конечно же, их можно отключить. Я же считаю, что будет очень удобно увидеть в какой момент времени, какой класс пытается что-то получить и что он пытается получить.

Надеюсь, эта статья была для вас полезной и вы узнали что-то полезное для себя!
AdBlock has stolen the banner, but banners are not teeth — they will be back

More
Ads

Comments 15

    +3
    Почти DI :)

    Лучше в Dictionary ключи не string, а тип (T). При таком подходе поиск зависимостей по проекту делать проще, чем по стрингам.
      0
      Хорошая мысль, которая не пришла мне в голову, потому что слишком просто и без трудностей) Спасибо!
      0
      Как бы это помягче сказать, чтобы не показаться грубым… Это велосипед, причём плохой.
      По факту это Service Locator, который уже давно считается антипаттерном, к тому же очень скудно реализованный (например, что мешает в качестве ключа использовать тип?). Плюс синглтоны, что тоже весьма печально.
      Курите StrangeIoc/Zenject, как гораздо более вменяемую альтернативу.
        0
        Какие альтернативы синглтона в unity вы можете подсказать?
          0
          вам же подсказали — Zenject. Strange крайне не рекомендую. Слишком свою архитектуру навязывают. Живем с ним на двух проектах как с наследием, дико плюемся (имеется опыт проекта с Зенджектом)
            +1
            Полностью избавиться от синглтонов в Unity действительно весьма сложно и даже не всегда оправданно. Но если инъекция зависимостей поможет распутать код, то почему бы и да.
              –2
              Главное Zenject не берите.
                0
                ага, лучше StrangeIOC, чтобы пораньше проект закрыть )
              0
              Поддерживаю
              Сам синглтон еще терпим — но в крайне редких случаях. Unity позволяет обойтись без него стандартными средствами. Проблем приносит на порядок больше.

              SL — просто отличный пример как не стоит делать (за редким исключением).

              По факту SL хорошо смотрится вкупе с DI — когда каждый сервис является подмножеством взаимозаменяемых сервисов, но сложность этого подхода все же ставит его в антипаттерны.
                0
                Ну, DI обычно воспринимается как преемник SL, вытесняющий его полностью.
                  0
                  Я сильно ориентируюсь по этой статье www.skipy.ru/architecture/module_design.html

                  Применительно к Unity — совсем немного времени на нем провел, пришел из плюсов с полгодика назад.

                  Отсюда вопрос — хорошая литература и проекты по Unity (Net)- не подскажешь? А то я больше занимаюсь обработкой изображений, на Unity у меня небольшой проектик по AR (в ТЦ Капитолий стоит, майки примерять) и чувствую что нагреб граблей по незнанию.
                    0
                    Хорошей литературы (вот чтоб прямо книги) по Unity я не видел. Искал где-то года два назад, может сейчас что-нибудь да поменялось. Что-то толковое может быть по отдельным кускам, типа Shader Graph, но я не слежу.
                    С другой стороны, если интересен их перспективный стек DOTS, есть отличное чтиво R.Fabian «Data-Oriented Design», но он не про конкретно технологию, а про сам подход. Даже официальная бесплатная онлайн-версия есть.
                      0
                      Спасибо!
              0
              Проверяли логику работы данных синглтонов при переходе между сценами?

              Накидаю кейс проверки — нужно вызывать методы синглтона посредством делегатов (UnityEvent). Допустим используем синглтон для музыки, пишем обертку для дизайнера, чтобы он мог ручками накидывать необходимые ему взаимосвязи через интерфейс Unity Editor — как отработает ваш код?

              Я пока данный глюк так и не поборол.

              Под катом мой код сингелтона:

              Singelton.cs
              using UnityEngine;
              
              /// <summary>
              /// Реализация сингелтона для наследования.
              /// </summary>
              /// <typeparam name="T">Класс, который нужно сделать сингелтоном</typeparam>
              /// <remarks>/// Если необходимо обращаться к классу во время OnDestroy или OnApplicationQuit
              /// необходимо проверять наличие объекта через IsAlive. Объект может быть уже 
              /// уничтожен, и обращение к нему вызовет его еще раз.
              /// 
              /// 
              /// При использовании в дочернем классе Awake, OnDestroy, 
              /// OnApplicationQuit необходимо вызывать базовые методы
              /// base.Awake() и тд.
              /// 
              /// Добавил скрываемый метод Initialization - чтобы перегружать его и использовать 
              /// необходимые действия.
              /// 
              /// Создание объекта производится через unity, поэтому использовать блокировку 
              /// объекта нет необходимости. Однако ее можно добавить, в случае если 
              /// понадобится обращение к объекту из других потоков.
              /// 
              /// Из книг:
              ///     - Рихтер "CLR via C#"
              ///     - Chris Dickinson "Unity 2017 Game optimization"
              ///</remarks>
              
              public class Singelton<T> : MonoBehaviour where T : Singelton<T>
              {
              
                  private static T instance = null;
              
                  private bool alive = true;
              
                  public static T Instance
                  {
                      get
                      {
                          if (instance != null)
                          {
                              return instance;
                          }
                          else
                          {
                              //Find T
                              T[] managers = GameObject.FindObjectsOfType<T>();
                              if (managers != null)
                              {
                                  if (managers.Length == 1)
                                  {
                                      instance = managers[0];
                                      DontDestroyOnLoad(instance);
                                      return instance;
                                  }
                                  else
                                  {
                                      if (managers.Length > 1)
                                      {
                                          Debug.LogError($"Have more that one {typeof(T).Name} in scene. " +
                                                          "But this is Singelton! Check project.");
                                          for (int i = 0; i < managers.Length; ++i)
                                          {
                                              T manager = managers[i];
                                              Destroy(manager.gameObject);
                                          }
                                      }
                                  }
                              }
                              //create 
                              GameObject go = new GameObject(typeof(T).Name, typeof(T));
                              instance = go.GetComponent<T>();
              				instance.Initialization();
                              DontDestroyOnLoad(instance.gameObject);
                              return instance;
                          }
                      }
              
                      //Can be initialized externally
                      set
                      {
                          instance = value as T;
                      }
                  }
              
                  /// <summary>
                  /// Check flag if need work from OnDestroy or OnApplicationExit
                  /// </summary>
                  public static bool IsAlive
                  {
                      get
                      {
                          if (instance == null)
                              return false;
                          return instance.alive;
                      }
                  }
              
              
              	protected void Awake()
              	{
              		if (instance == null)
              		{
              			DontDestroyOnLoad(gameObject);
              			instance = this as T;
              			Initialization();
              		}
              		else
              		{
              			Debug.LogError($"Have more that one {typeof(T).Name} in scene. " +
              							"But this is Singelton! Check project.");
              		    DestroyImmediate(this);
              		}
              	}
              
                  protected void OnDestroy() { alive = false; }
              
                  protected void OnApplicationQuit() { alive = false; }
              
              	protected virtual void Initialization() { }
              }
              

                +1
                Думаю будет полезно посмотреть

                Only users with full accounts can post comments. Log in, please.