Skip to content

EventBus

Bronuh edited this page Feb 27, 2024 · 14 revisions

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

Хотя чисто технически шина событий отвечает за довольно простую реализацию паттерна 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>)

Publish

С помощью EventBus.Publish можно опубликовать любой объект, чей тип реализует интерфейс IEvent

EventBus.Publish(new SomeEvent());

Require

С помощью 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)

TryRequire

Для более простой проверки наличия ответа в запросе можно использовать bool EventBus.TryRequire<Result>(Query<Result>, out Result) Метод вернет true, если получен ответ на запрос.

if (EventBus.TryRequire(new SomeQuery(), out var result))
{
	// Дальнейшие действия с result...
}

TryPublish

Отменяемые события можно сразу проверить с помощью EventBus.TryPublish. Метод возвращает значение свойства cancellableEvent.IsCancelled, так что если вернулось true, то событие было отменено.

if (EventBus.TryPublish(new SomeCancellableEvent()))
{
	return; // Событие отменено, уходим отсюда
}

В метод EventBus.TryPublish можно передавать только подтипы CancellableEvent (см. далее).

Создание новых событий

Для создания своего события достаточно создать тип, реализующий интерфейс IEvent.

Для обычных событий, не подразумевающих изменения их состояния лучше всего подойдет следующий шаблон:

public readonly record struct SomeEvent(SomeEventData Data) : IEvent;

Однако, если событие подразумевает внесение в него изменений в процессе обработки рекомендуется наследовать его от одного из нескольких абстрактных типов, представленных на изображении бледно-голубым цветом (кроме необобщенного QueryEvent, его лучше не трогать):

Event Bus Hierarchy

  • 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))