Как стать автором
Обновить

SMessage — Простая и предсказуемая система событий для Unity

Время на прочтение5 мин
Количество просмотров9.3K
Дисклеймер
Этот пост является некоторым развитием идей поста «Простая система событий в 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# делегатах.

Разбирать работу менеджера предлагаю вам самостоятельно, вот пара листингов. Ниже можно найти пример использования, что куда более интересно конечному пользователю.
SManager.cs
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);
        }
    }
SCallbackWrapper.cs
 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
Но тут я разберу самый простой кейс, как использовать эту систему. Для примера я буду использовать синглтон реализацию менеджера событий, хотя вы вправе создать свой инстанс под любые нужды. Допустим нам нужно создать новое событие, которое будет оповещать о том, что какой-то объект был кликнут. Создадим объект события.

Модель события и есть сам маркер события, поэтому вам следует создавать новый класс на каждое событие. Такова плата за помощь IDE :< В качестве базового класса используется AbstractSMessage, который может хранить в себе какой-то объект
  public class SMessageExample :  AbstractSMessageValued<GameObject> {
    public SMessageExample (GameObject value) : base(value) { }
  }

В самом объекте нам нужно будет его вызвать это событие и передать туда необходимые аргументы
  public class ExampleObject : MonoBehaviour {
    public void OnMouseDown () {
      SManager.SCall(new SMessageExample(gameObject));
    }
  }

Ну и, наконец, создадим другой объект, который будет отслеживать это событие
  public class ExampleHandlerObject : MonoBehaviour {
        // Добавлять слушателей лучше в OnEnable
        public void OnEnable() {
            SManager.SAdd<SMessageExample>(OnMessage);
        }

        // И не забывать удалять в OnDisable
        public void OnDisable() {
            SManager.SRemove<SMessageExample>(OnMessage);
        }

        private void OnMessage (SMessageExample message) {
          Debug.Log("OnMouseDown for object "+message.Value.name);
        }
  }

Все достаточно просто и очевидно, но что гораздо важнее компилятор/IDE все проверит за вас и поможет вам в работе.
P.S. Код не проверял, могут быть ошибки :)

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

Надеюсь код будет кому-то полезен. Я же буду рад каким-то замечаниям и предложениям.

UPD:
Добавил абстрактный класс AbstractSMessageValued и немного обновил пример в статье.
Теги:
Хабы:
+9
Комментарии5

Публикации

Изменить настройки темы

Истории

Работа

Ближайшие события