Эта страница ещё не переведена — показана английская версия.
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, пока не решено (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 | куда уходят отправки: экземпляр сущности, группа, комната или глобальный контекст. Если цель — группа, один сигнал получают многие. Меняющегося адресата выражают группой, а не адресом, который передают при отправке |
clock | sim_time или timestamp, но не оба сразу. sim_time — для фактов внутри симуляции: они участвуют в предсказании, компенсации лагов и откате. timestamp — для фактов вне её, например покупки или входа в игру |
retention | transient или 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, пока не решено (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, а не вызов, который молча ничего не делает. - Отправку отклоняют, если тип не объявлен, данные не проходят проверку по схеме или тип не разрешает такую цель.
- Выборка за пределами срока хранения возвращает отказ, а не пустую страницу.
Ограничения
Конкретные числа появятся вместе с главой об ограничениях платформы, а поведение на каждой границе уже определено.
- Размер данных: если он превышен, отправка не проходит и события не происходит. Данные никогда не обрезаются.
- Частота отправки от одного источника: отказ по частоте с указанием, когда можно повторить.
- Число подписок на актора: новую подписку отклоняют, существующие сохраняются.
- Объём хранения для типа: старые события вытесняются по объявленной политике, по сроку.