-
Notifications
You must be signed in to change notification settings - Fork 1
EventBus
Шина событий - это, пожалуй, самая важная часть, вокруг которой крутится весь проект. Все игровые механики, взаимодействия, запросы данных и даже вызовы некоторых методов из других систем, всё это происходит через шину событий.
Хотя чисто технически шина событий отвечает за довольно простую реализацию паттерна sub/pub, на самом деле название шина событий не отражает особенности её применения в проекте в полной мере.
Сейчас в проекте шина используется для:
- публикации и обработки событий
- отправки запросов на вызов какого-либо функционала в сервисы
- отправки запросов на получение данных
- работы с сетью (обработка пакетов)
Note
Текущая реализация шины не поддерживает очереди событий. Публикуемые события попадают сразу в подписанные на них обработчики, поэтому возможна ситуация, когда события "падают на пол" а не ждут, пока кто-нибудь их заберет.
События существуют в шине только в момент вызова EventBus.Publish
и покидают её уже на следующей строке после публикации, даже если их никто не обработал.
Есть несколько способов подписки на события:
- ручной
- автоматический
Ручной способ взаимодействия в целом отличается от автоматического только тем, что вам нужно самостоятельно подписывать свои обработчики на события путем вызова метода Subscribe
:
// Подписка метода OnSomeEvent на событие SomeEvent
EventBus.Subcribe(OnSomeEvent);
// Обработчик события SomeEvent
private void OnSomeEvent(SomeEvent evt)
{
evt.DoSomething();
}
Также допускаются анонимные (лямбда) методы:
// Подписка анонимного обработчика на событие SomeEvent
EventBus.Subcribe<SomeEvent>((evt) =>
{
evt.DoSomething();
});
Автоматический способ требует введения еще одной сущности - сервиса.
Сервис - это класс, который:
- помечен атрибутом
[GameService]
- имеет конструктор по умолчанию (без параметров)
- содержит обработчики событий (см. далее)
Все обработчики событий сервиса будут подписаны на соответствующие события автоматически при запуске.
Чтобы считаться обработчиком события метод должен:
- быть публичным
- иметь атрибут
[EventListener]
- иметь только ОДИН параметр, реализующий интерфейс
IEvent
- дополнительно, если событие является запросом, допускается иметь возвращаемое значение того же типа, что и тип значения в запросе.
Note
Рекомендуется именовать обработчики событий по следующему шаблону: On + <тип события>
.
Пример:
// Объявление сервиса. Рекомендуется сразу отемчать класс как sealed
// и больше не наследоваться от него.
[GameService]
public sealed class GameService
{
// Обычный обработчик событий
[EventListener]
public void OnSomeEvent(SomeEvent evt)
{
evt.DoSomething();
}
// Классический вариант обработчика запросов
[EventListener]
public void OnSomeQuery(SomeQuery query)
{
query.SetResult(new SomeResult());
}
// Упрощенный вариант обработчика запросов.
// Возвращаемое значение будет автоматически передано в SetResult()
[EventListener]
public SomeResult OnSomeQuery(SomeQuery query)
{
return new SomeResult();
}
}
Обработчики событий также могут иметь приоритет выполнения.
Существует 2 способа явно назначить приоритет выполнения обработчика:
- Передать приоритет в метод
EventBus.Subscribe()
вторым параметром - Указать приоритет в атрибуте
[EventListener()]
Существует 6 уровней приоритета выполнения, описанных в перечислении:
public enum ListenerPriority
{
/// <summary>
/// It is not recommended to write anything to events when using Monitor priority.
/// Consider processing them in read-only mode or use Highest priority instead.
/// </summary>
Monitor,
Highest,
High,
Normal,
Low,
Lowest
}
Первыми выполняются обработчики с приоритетом Monitor
, последними - с приоритетом Lowest
.
По умолчанию, при регистрации обработчиков событий им назначается приоритет Normal
.
Также стоит учитывать то, что в рамках одного и того же приоритета, обработчики будут выполняться в порядке их регистрации. Это может быть важно при разработке модификаций, так как порядок загрузки модификаций (и их обработчиков) может быть настроен вручную.
Основных способа 2:
EventBus.Publish<Event>(Event)
Result EventBus.Require<Result>(Query<Result>)
С помощью EventBus.Publish
можно опубликовать любой объект, чей тип реализует интерфейс IEvent
EventBus.Publish(new SomeEvent());
С помощью EventBus.Require
можно запросить данные из сервисов через шину событий. Проще говоря, какой-то обработчик должен поместить запрошенное значение внутрь события, после чего оно будет возвращено из EventBus.Require
. В Require
можно передавать только объекты, чей тип унаследован от QueryEvent<T>
, где T
- тип возвращаемого (запрашиваемого) значения.
var result = EventBus.Require(new SomeQuery());
Note
Обратите внимание, что результатом вполне может оказаться null
, что может говорить о:
- запрос не был обработан
- обработчик вернул
null
Для того, чтобы в этой ситуации проверить, был ли обработан запрос, можно проверить свойство запроса HasResult
, который будет равен true
, если хотя бы один обработчик попытался вернуть значение, даже если оно было null
.
Дополнительных способа 2:
bool EventBus.TryRequire<Result>(Query<Result>, out Result)
bool EventBus.TryPublish(CancellableEvent cancellableEvent)
Для более простой проверки наличия ответа в запросе можно использовать
bool EventBus.TryRequire<Result>(Query<Result>, out Result)
Метод вернет true
, если получен ответ на запрос.
if (EventBus.TryRequire(new SomeQuery(), out var result))
{
// Дальнейшие действия с result...
}
Отменяемые события можно сразу проверить с помощью EventBus.TryPublish
. Метод возвращает значение свойства cancellableEvent.IsCancelled
, так что если вернулось true
, то событие было отменено.
if (EventBus.TryPublish(new SomeCancellableEvent()))
{
return; // Событие отменено, уходим отсюда
}
В метод EventBus.TryPublish
можно передавать только подтипы CancellableEvent
(см. далее).
Для создания своего события достаточно создать тип, реализующий интерфейс IEvent
.
Для обычных событий, не подразумевающих изменения их состояния лучше всего подойдет следующий шаблон:
public readonly record struct SomeEvent(SomeEventData Data) : IEvent;
Однако, если событие подразумевает внесение в него изменений в процессе обработки рекомендуется наследовать его от одного из нескольких абстрактных типов, представленных на изображении бледно-голубым цветом (кроме необобщенного QueryEvent
, его лучше не трогать):
-
CancellableEvent - событие, имеющее в себе свойство
IsCancelled
, позволяющее контролировать логику, следующую ПОСЛЕ публикации и обработки события. -
HandleableEvent - событие, имеющее в себе свойство
IsHandled
, позволяющее контролировать процесс обработки события. Шина будет передавать событие в обработчики по очереди, в зависимости от их приоритета до тех пор, пока один из них не выставитtrue
свойству событияIsHandled
. -
QueryEvent<T> - содержит в себе свойство
Result
, в которое можно поместить значение запрашиваемого типаT
, и достать его из события ПОСЛЕ обработки.QueryEvent
- этоrecord
, а значит и его подтипы также должны бытьrecord
. -
AbstractPacket - тип, выделенный под сетевые пакеты. Требует переопределения нескольких свойств и может хранить в себе информацию для других игроков или сервера. При создании подтипа
AbstractPacket
необходимо пометить его атрибутом[GamePacket]
, а также рекомендуется сделать егоsealed
.
Note
При создании событий рекомендуется соблюдать следующие соглашения именования:
- Имена запросов (подтипы
QueryEvent
) должны оканчиваться словомQuery
(например GetBattleWorldQuery или просто BattleWorldQuery) - Имена сетевых пакетов (подтипы
AbstractPacket
) должны оканчиваться словомPacket
(например PlayerInfoPacket(PlayerInfo Info)) - Имена запросов на выполнение действия, предполагающие обязательную обработку события должны оканчиваться словом
Request
(например ApplyDamageRequest(Damage Damage, Caharcter Target)) - Все остальные события должны оканчиваться словом
Event
(например CharacterTookDamageEvent(Damage Damage, Caharcter Target))