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

в 8:53, , рубрики: Events, unity, unity3d

Дисклеймер
Этот пост является некоторым развитием идей поста «Простая система событий в 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
Но тут я разберу самый простой кейс, как использовать эту систему. Для примера я буду использовать синглтон реализацию менеджера событий, хотя вы вправе создать свой инстанс под любые нужды. Допустим нам нужно создать новое событие, которое будет оповещать о том, что какой-то объект был кликнут. Создадим объект события:

  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

Источник

* - обязательные к заполнению поля


https://ajax.googleapis.com/ajax/libs/jquery/3.4.1/jquery.min.js