Перейти к основному содержимому
Строительные блоки

Эта страница ещё не переведена — показана английская версия.

Events

Событие — это факт того, что что-то произошло. Его доставляют всем, кому о нём нужно знать. Событиями передают то, что случается один раз и что нельзя восстановить по текущему значению: выстрел, покупку, вход в комнату.

Когда это нужно

  • Что-то произошло, и остальные должны на это отреагировать: прозвучал выстрел, заперли дверь, закончился матч.
  • Получатели бывают разными. Одна и та же отправка дойдёт до отряда, комнаты или одного актора — смотря какую цель объявляет тип.
  • Нужны типизированные обработчики с автодополнением. Объявленное событие (event) появляется в интерфейсе как send.* и on.*, и у каждого свой контракт.
  • Факт должен читаться и через час. Объявите тип хранимым и читайте события за нужный период.

Кто что делает

АкторЧто делает на этой странице
schema-authorобъявляет события через [Event] и загружает схему
любой акторотправляет через send.* и подписывается через on.*

Коротко

RallyCall объявляется один раз, отправляется через send.*, а реакция пишется в on.*
[Event("rally_call", Clock = Clock.SimTime, Retention = Retention.Transient)]
public record RallyCall(Vector3 Position);

// emitting: the declaration generated the method — and its contract
squad.Send.RallyCall(position);

// subscribing: typed handler, autocompleted beside every other declared event
squad.On.RallyCall(call => ShowRallyMarker(call.Position));
@Event('rally_call', { clock: Clock.SimTime, retention: Retention.Transient })
export class RallyCall { constructor(public position: Vector3) {} }

// emitting: the declaration generated the method — and its contract
squad.send.rallyCall(position);

// subscribing: typed handler, autocompleted beside every other declared event
squad.on.rallyCall((call) => showRallyMarker(call.position));
@event("rally_call", clock=Clock.SIM_TIME, retention=Retention.TRANSIENT)
class RallyCall:
    position: Vector3

# emitting: the declaration generated the method — and its contract
squad.send.rally_call(position)

# subscribing: typed handler, autocompleted beside every other declared event
squad.on.rally_call(lambda call: show_rally_marker(call.position))
Скоро — Go

Как объявлять через атрибуты в Go, пока не решено (O-1). Примеры появятся, когда выберут способ записи.

// declarations compile into the same pushed model — playserv push from the UE project or CI
USTRUCT(PSEvent = (Name = "rally_call", Clock = "SimTime", Retention = "Transient"))
struct FRallyCall
{
    GENERATED_BODY()

    UPROPERTY() FVector Position;
};

// emitting and subscribing — generated, typed
Squad->Publish->RallyCall({ Position });
TPSSubscription RallyMarkers = Squad->Subscribe->RallyCall(
    [this](const FRallyCall& Call) { ShowRallyMarker(Call.Position); });
// co_await support ships later: a built-in SDK coroutine wrapper that respects Unreal's GC and threading.
// the calls are the C# ones; the payload is not — Unity's floor is C# 9 and the generated
// source may carry no records, so a declared payload is a plain serializable type
[Event("rally_call", Clock = Clock.SimTime, Retention = Retention.Transient)]
public sealed class RallyCall
{
    public Vector3 Position;      // converts to and from UnityEngine.Vector3
}

squad.Send.RallyCall(new RallyCall { Position = position });
squad.On.RallyCall(call => ShowRallyMarker(call.Position.ToUnity()));

Событие, объявленное внутри модуля или группы (group), доступно только там. squad.send.rallyCall существует потому, что rally_call объявлено для отрядов, и отправка доходит до участников отряда. События, которые модуль отправляет наружу, входят в его объявленный контракт, а необъявленных вызывающий код не видит никогда.

Модель

Что объявляет тип события.

Что объявляетЧто это
nameявное имя для передачи по сети: его объявляют, а не выводят из имени символа
payloadсхема (schema) данных, которые несёт отправка
targetкуда уходят отправки: экземпляр сущности, группа, комната или глобальный контекст. Если цель — группа, один сигнал получают многие. Меняющегося адресата выражают группой, а не адресом, который передают при отправке
clocksim_time или timestamp, но не оба сразу. sim_time — для фактов внутри симуляции: они участвуют в предсказании, компенсации лагов и откате. timestamp — для фактов вне её, например покупки или входа в игру
retentiontransient или retained, см. ниже
termдля типа retained: сколько событие хранится и что происходит, когда срок истекает. Бессрочного хранения нет
deliveryне более одного раза, хотя бы один раз или ровно один раз. Задаётся в типе, чтобы подписчику не приходилось это выяснять. Для «ровно один раз» указано, в каких границах гарантия действует
contextглобальный или локальный. Имя, объявленное глобально, видно в локальных контекстах, а объявленное локально выше не видно

Что несёт отправка.

ПолеЧто это
typeобъявленное событие. Отправки никогда не сливаются: два выстрела — два события. Этим событие и отличается от поля с [Sync] в Data & Subscriptions
payloadданные по схеме типа
sourceактор-отправитель, а если отправила сущность — ещё и её экземпляр
stampметка времени по часам, объявленным в типе
ключ дедупликацииесть в любом режиме доставки
ключ причиныу события, которое платформа отправила из-за другого события, — id того события. Так цепочку восстанавливают по ключам, а не сравнивая метки времени

Событие от клиента — это только заявление

Авторитетная сторона проверяет событие от клиента до того, как от него что-то начнёт зависеть. Не считайте source доказательством.

Будьте готовы к повторной доставке

Повторная доставка возможна в любом режиме: транспорт может продублировать сообщение, хранимое событие могут прочитать второй раз. Отсеивайте дубликаты по ключу дедупликации.

Что хранит подписка.

Что хранитЧто это
eventобъявленный тип, к которому привязана подписка (subscription)
surfaceузел внутри объявленного в типе target, на котором оформлена подписка
handlerобработчик, типизированный по данным события
positionместо, с которого подписка продолжится после переподключения, чтобы она молча не начала заново с «сейчас». Пропущенные за время разрыва события не проигрываются повторно: временное теряется, прочитать заново можно только retained

Что верно для любого события.

ВсегдаЧто это
аудиторияотправитель её не перечисляет: это объявленный target, суженный до тех, кто подписан, и затем отфильтрованный правилами Access & Roles. Поток данных можно закрыть предикатом, даже если сам тип виден
фазыотправлено, затем доставлено. Машины состояний у события нет: оно происходит один раз
порядокгарантируется внутри одного потока данных (stream), а поток данных — это один отправляющий экземпляр. Два события от одного экземпляра приходят в том порядке, в каком их отправили
обнаружение пропусковесли режим доставки допускает потери, подписчик узнаёт о пропуске, а не проскакивает его молча

Между потоками данных порядка нет

Порядок не гарантируется ни между двумя экземплярами, ни между дельтой и событием об одном и том же изменении. Если нужен порядок между потоками данных, сведите сообщения в один поток или передавайте в данных причинную метку.

Хранение — часть типа

Хранится ли событие после доставки, объявляется в его типе, а не выбирается при отправке. Поэтому события одного типа всегда хранятся одинаково. Хранимое событие читают по типу и периоду, а не через запросы Data & Subscriptions.

ВременноеХранимое
Кому доходиттем, кто подписан в этот моментим, а также тем, кто подпишется позже
Что потомисчезаетхранится объявленный срок
Можно прочитать зановонетда, в течение срока
После срока—выборка возвращает отказ, а не пустой ответ
Хранимое событие: объявлено со сроком хранения, читается за период
[Event("objective_taken", Clock = Clock.SimTime, Retention = Retention.Retained, Keep = "7d")]
public record ObjectiveTaken(string Objective, PlayerId By);

// a member who joined late reads what it missed — by type and period, nothing wider
var taken = await squad.Retained.ObjectiveTaken(since: matchStart);
@Event('objective_taken', { clock: Clock.SimTime, retention: Retention.Retained, keep: '7d' })
export class ObjectiveTaken { constructor(public objective: string, public by: PlayerId) {} }

// a member who joined late reads what it missed — by type and period, nothing wider
const taken = await squad.retained.objectiveTaken({ since: matchStart });
@event("objective_taken", clock=Clock.SIM_TIME, retention=Retention.RETAINED, keep="7d")
class ObjectiveTaken:
    objective: str
    by: PlayerId

# a member who joined late reads what it missed — by type and period, nothing wider
taken = await squad.retained.objective_taken(since=match_start)
Скоро — Go

Как объявлять через атрибуты в Go, пока не решено (O-1). Примеры появятся, когда выберут способ записи.

USTRUCT(PSEvent = (Name = "objective_taken", Clock = "SimTime", Retention = "Retained", Keep = "7d"))
struct FObjectiveTaken
{
    GENERATED_BODY()

    UPROPERTY() FString     Objective;
    UPROPERTY() FPSPlayerId By;
};

// a member who joined late reads what it missed — by type and period, nothing wider
Squad->Retained->ObjectiveTaken->Select(FPSTimeWindow{ .From = MatchStart })
    .Then(TPSOnResult<TArray<FObjectiveTaken>>::CreateWeakLambda(this, [this](const TPSResult<TArray<FObjectiveTaken>>& Result)
    {
        if (!Result.HasValue()) { return; }
        for (const FObjectiveTaken& Taken : Result.Value()) { Timeline->Add(Taken); }
    }));
// co_await support ships later: a built-in SDK coroutine wrapper that respects Unreal's GC and threading.
// same attribute, same read — the payload is a plain serializable type on the C# 9 floor
[Event("objective_taken", Clock = Clock.SimTime, Retention = Retention.Retained, Keep = "7d")]
public sealed class ObjectiveTaken
{
    public string   Objective;
    public PlayerId By;
}

var taken = await squad.Retained.ObjectiveTaken(since: matchStart);

Ошибки

  • Отправка и подписка — отдельные права, и одно не следует из другого. Подписка без права получает ответ forbidden, а не not-found: тип входит в объявленный контракт модуля, скрывать тут нечего.
  • Подписка на тип, который модуль не объявлял, — это ошибка контракта с типизированным Problem, а не вызов, который молча ничего не делает.
  • Отправку отклоняют, если тип не объявлен, данные не проходят проверку по схеме или тип не разрешает такую цель.
  • Выборка за пределами срока хранения возвращает отказ, а не пустую страницу.

Ограничения

Конкретные числа появятся вместе с главой об ограничениях платформы, а поведение на каждой границе уже определено.

  • Размер данных: если он превышен, отправка не проходит и события не происходит. Данные никогда не обрезаются.
  • Частота отправки от одного источника: отказ по частоте с указанием, когда можно повторить.
  • Число подписок на актора: новую подписку отклоняют, существующие сохраняются.
  • Объём хранения для типа: старые события вытесняются по объявленной политике, по сроку.