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. Особым плюсом является хорошая видимость сообщений дебага на все случаи жизни. По желанию, конечно же, их можно отключить. Я же считаю, что будет очень удобно увидеть в какой момент времени, какой класс пытается что-то получить и что он пытается получить.

Надеюсь, эта статья была для вас полезной и вы узнали что-то полезное для себя!

Специально для сайта ITWORLD.UZ. Новость взята с сайта Хабр