Дисклеймер
Этот пост является некоторым развитием идей поста «Простая система событий в Unity». Я не претендую на единственный верный подход к вопросу и вообще являюсь посредственным программистом, относительно мастодонтов, которые обитают на хабре. Так же я очень поверхностно знаком с C#, так как в основном работе использую Java. Тем ни менее судьба занесла меня в Unity и я понял что у меня есть некоторый инструмент, которым можно отплатить сообществу за то, что я у него взял. Проще говоря внести свой, пусть и маленький, вклад в открытый и, хочется верить, хороший код.
Кому лень читать проблематику, выводы и прочее могут сразу посмотреть код с примерами на гитхабе — github.com/erlioniel/unity-smessage
Там даже можно .unitypackage скачать :)
Проблема
Только начав собирать проект в Unity я сразу пришел к выводу, что нужна какая-то система событий. Что бы не говорили вам религиозные фанатики, но ситуация, когда GUI жестко связан с игровыми объектами, худшее, что может быть. Архитектура проекта, построенная на макаронной передаче объектов друг друга очень сложно масштабируется и подвергается изменениям. Поэтому система событий должна быть.
Другой вопрос, что и тут нужно без фанатизма, потому что так недолго дойти до ситуации, когда поведение программы станет невозможно отслеживать, ведь события достаточно непредсказуемая абстракция. Но что мы можем сделать, чтобы чуть-чуть упростить задачу?
Параметры — первый подход
Если посмотреть код статьи, на которую я ссылаюсь изначально, видно, что система событий там крайне простая, ведь событие не может содержать никаких параметров. Изначально я реализовал другую систему, которая позволяла использовать параметры:
// Подписка
Callback.Add(Type.TURN_START, Refresh);
// Вызов
Callback.Call(Type.TURN_START, TurnEntity());
Как видно в качестве ключа события использовалось значение ENUM, что несколько упрощало работу (всегда можно было получить от IDE список возможных значений), а так же без проблем передать какие-то параметры. Это в общем меня устроило на первое время.
Типизация — второй подход
Главная проблема простой реализации системы событий — ее слабая предсказуемость и невозможность помощи IDE в написании кода. В какой-то момент я начал ловить себя на мысли, что для сколько-нибудь сложных событий мне приходится вспоминать, в каком порядке аргументы нужно передать, чтобы они нормально записались в модель. Да и все эти касты модели в другую в обработчиках тоже напрягали. В общем разросшаяся система начала вести себя непредсказуемо и требовала все больше внимания и знания старого кода чтобы поддерживать себя.
После одного вечера колдовства с дженериками получилась система, в которой IDE отлично помогает в любой ситуации. Список событий легко получить, если принять какие-то правила наименования классов-событий (например префикс SMessage...), никаких кастов, обработчики сразу получают объекты финального класса и все это базируется на классических C# делегатах.
Разбирать куски кода смысла, мне кажется, нет, там все просто. Тем кому лень лазить по репозиторию, вот несколько листингов
public class SManager {
private readonly Dictionary<Type, object> _handlers;
// INSTANCE
public SManager() {
_handlers = new Dictionary<Type, object>();
}
/// <summary>
/// Just add new handler to selected event type
/// </summary>
/// <typeparam name="T">AbstractSMessage event</typeparam>
/// <param name="value">Handler function</param>
public void Add<T>(SCallback<T> value) where T : AbstractSMessage {
var type = typeof (T);
if (!_handlers.ContainsKey(type)) {
_handlers.Add(type, new SCallbackWrapper<T>());
}
((SCallbackWrapper<T>) _handlers[type]).Add(value);
}
public void Remove<T>(SCallback<T> value) where T : AbstractSMessage {
var type = typeof (T);
if (_handlers.ContainsKey(type)) {
((SCallbackWrapper<T>) _handlers[type]).Remove(value);
}
}
public void Call<T>(T message) where T : AbstractSMessage {
var type = message.GetType();
if (_handlers.ContainsKey(type)) {
((SCallbackWrapper<T>) _handlers[type]).Call(message);
}
}
// STATIC
private static readonly SManager _instance = new SManager();
public static void SAdd<T>(SCallback<T> value) where T : AbstractSMessage {
_instance.Add(value);
}
public static void SRemove<T>(SCallback<T> value) where T : AbstractSMessage {
_instance.Remove(value);
}
public static void SCall<T>(T message) where T : AbstractSMessage {
_instance.Call(message);
}
}
internal class SCallbackWrapper<T>
where T : AbstractSMessage {
private SCallback<T> _delegates;
public void Add(SCallback<T> value) {
_delegates += value;
}
public void Remove(SCallback<T> value) {
_delegates -= value;
}
public void Call(T message) {
if (_delegates != null) {
_delegates(message);
}
}
}
Пример использования
Много примеров вы можете найти в папке github.com/erlioniel/unity-smessage/tree/master/Assets/Scripts/Examples
Но тут я разберу самый простой кейс, как использовать эту систему. Для примера я буду использовать синглтон реализацию менеджера событий, хотя вы вправе создать свой инстанс под любые нужды. Допустим нам нужно создать новое событие, которое будет оповещать о том, что какой-то объект был кликнут. Создадим объект события:
public class SMessageExample : AbstractSMessage {
public readonly GameObject Obj;
public SMessageExample (GameObject obj) {
Obj = obj;
}
}
В самом объекте нам нужно будет его вызвать
public class ExampleObject : MonoBehaviour {
public void OnMouseDown () {
SManager.SCall(new SMessageExample(this));
}
}
Ну и создадим другой объект, который будет отслеживать это событие
public class ExampleHandlerObject : MonoBehaviour {
public void OnEnable() {
SManager.SAdd<SMessageExample>(OnMessage);
}
public void OnDisable() {
SManager.SRemove<SMessageExample>(OnMessage);
}
private void OnMessage (SMessageExample message) {
Debug.Log("OnMouseDown for object "+message.Obj.name);
}
}
Все достаточно просто и очевидно, но что гораздо важнее компилятор/IDE все проверит за вас и поможет вам в работе.
P.S. Код не проверял, могут быть ошибки :) Вечером обновлю пример
Вместо заключения
Система событий — очень сильный инструмент и недооценивать ее не стоит. Высокая связанность кода это не так хорошо, как может казаться некоторым программистам, особенно когда проект разрастается до средних масштабов.
Надеюсь код будет кому-то полезен. Я же буду рад каким-то замечаниям и предложениям.
Автор: erlioniel