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

Комментарии 9

Это всё очень хорошо, особенно для прототипов и хакатонов. Но а как же работать со связностью, если вдруг захочется расширить проект?
Не очень понимаю, как увеличение проекта ведет к проблемам со связностью. Могу лишь сказать, что мы используем такую систему (с различными расширениями) в наших играх (Pathfinder: Kingmaker и Pathfinder: WoTR). Это весьма большие проекты и никаких особых проблем с EventBus мы не испытываем.
Я попробую ответить.
Если кратко — со связностью нужно работать аккуратно и внимательно следить за ней.
Тут есть 2 аспекта — проблемы с EventBus и связность компонентов с EventBus компонентов с компонентами.

Зависимости:
— Сам по себе EventBus без сомнений станет зависимостью для всех, но это само по себе не большая проблема. Проблемой станет паразитные зависимости — если event bus будет связан с настройками/интерфейсами или конкретными сущностями (например, если его встраивают в главный контроллер игры). Он должен быть стабилен — синглтон с ленивой инициализацией, не ссылающйся ни на какие другие файлы.
— Если вы пишите переносимые компоненты — виджеты, компоненты рендера — не используйте EventBus. Используйте его в игровой логике.
— Не используйте EventBus с абстрактными или строковыми параметрами, используйте на интерфейсах или типах данных. Это защитит при переносах и переиспользовании компонент через зависимость на типы передаваемых сообщений/интерфейсы. Если вы завели тип/интерфейс в неймспейсе поставщика, все подписчики все равно получат явную зависимость на абстракцию от поставщика (его тип данных).

Но на самом деле это меньшие из проблем, самая важная проблема — это когда основной flow приложения неявным образом опирается на порядок вызовов в EventBus (который, по идее, обычно никак не гарантируется контрактом). Есть еще смежная проблема — когда в игре нельзя найти концов того, как игра пришла к такому состоянию (потому что ею управляла цепочка подписок).

Чтобы бороться с этими проблемами нужно ограничить использование EventBus:
— Пассивные реакции — компоненты, реагирующие на ивенты должны менять только свое состояние, но не состояние других компонентов.
— Низкоуровневые реакции — компоненты, реагирующие на ивенты, должны быть ниже по уровню абстракции. Условно, листья не должны шевелить деревом, дерево не должно управлять стартом игры (но наоборот — можно). Если у вас есть такое место, к примеру, рестарт игры завязан на упавшее здоровье игрока — возможно вы что-то делаете не так. Об этом же говорят проблемы с изменением списка подписчиков — скорее всего один из подписчиков слишком важная шишка и не должен быть просто подписчиком. Решайте это выделением специальных механизмов — к примеру, игрок должен иметь специальный колбек, который должен быть задан при его создании — тогда контроллер сущностей получает явную ручку для организации flow. А вот все остальные — интерфейсы здоровья, эффекты и т.д. — они подписываются на ивент изменения здоровья игрока.
— Изоляция. Если вы вынуждены написать логику, нарушающую первые 2 правила — выделите ее в отдельное место, которое об этом явно говорит. Например, ваши ГД хотят сами управлять условиями того, как завершается уровень, и хотят использовать для этого любой ивент, доступный им. Или стартующее новые миссии.
В этом случае это место станет точкой отказа, по логами и стактрейсу можно будет определить его как источник в случае ошибок, и в нем же можно будет разобраться с проблемами очередности и одновременности (к примеру, в этом компоненте можно будет организовать очередь сообщений, если могут быть выполнены сразу несколько условий с конфликтующими управляющими воздействиями)

Есть прекрасная либа (которая портирована под Unity тоже) — Rx.
В ней есть прекрасный EventBus.

В вашем коде я не вижу, чтобы вы использовали WeakRef и значит если где-то забыли отписаться от события, но объект решили удалить, то можем получить утечки памяти.

Поправьте, если я ошибаюсь
Да, в UniRx есть своя система событий и ее можно отнести ко второму типу (подписка по типу события). Я тут представил другой подход.

WeakRef у нас нету, поэтому утечки потенциально возможны. Для борьбы с этим можно придумывать различные тактики. Один из вариантов это использовать паттерн Disposable. То есть можно чуть править метод подписки, чтобы он возвращал IDisposable. Это хорошо сочетается с использованием того же UniRx. Но встроить WeakRef звучит как неплохая идея, спасибо за совет.
опять же никто не отменял встроенные возможности шарпа для работы с событиями, которые, в целом, являются довольно удобным механизмом при наличии ссылки на объект. К тому же их строгость типизации является весомым аргументом, нежели использовать вариант с передачей массива объектов
На встроенных событиях можно построить систему по второму типу (подписка по типу события). Я тут предлагаю другую и на мой взгляд более продвинутую реализацию.

Вариант с массивом объектов я описал для полноты картины. Согласен, что он весьма проблемный и не советую никому к нему прибегать.
В большинстве случаев методы подписки и вызова будут выглядеть примерно следующим образом:

// Подписка
EventSystem.Subscribe(тип_события, подписываемый_метод);

// Вызов
EventSystem.RaiseEvent(тип_события, аргументы);


Замените, пожалуйста, EventSystem на EventBus для понятности.

Отличная статья, спасибо!

Мне кажется можно избавиться от рефлексии, если каждый раз при подписке передавать интерфейс в явном виде (но тогда для каждого интерфейса придется вызывать подписку и отписку).

Так же можно добавить асинхронные RaiseEvent - чтобы их можно было авейтить (внутри сделать как Task.WhenAll). Тогда можно будет делать await RaiseEvent<..> и ждать когда все подписчики выполнят свои таски.

Только полноправные пользователи могут оставлять комментарии. Войдите, пожалуйста.