Перейти до основного вмісту
Будівельні блоки

Цю сторінку ще не перекладено — показано англійську версію.

Events

Подія — це факт, що щось сталося, доставлений усім, хто має про нього дізнатися. Використовуйте її для того, що стається один раз і чого не відновиш із поточного значення: постріл, покупка, вхід у кімнату.

Коли це потрібно

  • Щось сталося, і інші мають на це відреагувати: пролунав постріл, замкнулися двері, закінчився матч.
  • Аудиторія буває різною. Та сама відправка доходить до загону, кімнати чи одного актора — залежно від цілі, яку оголошує тип.
  • Потрібні типізовані обробники з автодоповненням. Оголошена подія (event) перетворюється на send.* і on.* на своєму вузлі, і кожен має власний контракт.
  • Факт має лишатися доступним і за годину: оголосіть тип як retained і читайте його за період.

Хто що робить

АкторЩо робить на цій сторінці
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куди йдуть відправки: екземпляр сутності (entity), група, кімната або глобальний контекст. Ціль-група доставляє один сигнал багатьом одержувачам. Адресата, що змінюється, описують групою, а не адресою, яку передають під час відправки
clocksim_time або timestamp, але не обидва одразу. sim_time — для фактів усередині симуляції, які беруть участь у передбаченні (prediction), компенсації лагів і відкаті; timestamp — для фактів поза нею, як-от покупка чи вхід
retentiontransient або retained, див. нижче
termдля типу retained: скільки подію зберігають і що стається, коли строк мине. «Назавжди» не буває
deliveryне більше одного разу, щонайменше один раз або рівно один раз. Режим оголошують у типі, тож підписникові не треба про нього питати. Для «рівно один раз» указано межі, у яких ця гарантія діє
contextглобальний або локальний. Назву, оголошену глобально, видно в локальних контекстах; оголошену локально вище не видно

Що несе відправка.

ПолеЩо це
typeоголошена подія. Відправки ніколи не зливаються: два постріли — це дві події. Саме цим подія відрізняється від поля [Sync] у Data & Subscriptions
payloadдані за схемою типу
sourceактор-відправник, а якщо надіслала сутність — ще й її екземпляр
stampмітка часу за годинником, оголошеним у типі
ключ дедуплікаціїє в будь-якому режимі доставки
ключ причиниу події, яку платформа надсилає через іншу подію: ідентифікатор тієї події. Так ланцюжок відновлюють за ключем, а не порівнюючи мітки часу

Подія від клієнта — лише заява

Авторитетна сторона перевіряє подію від клієнта, перш ніж щось від неї залежатиме. Не вважайте source доказом.

Обробляйте повторну доставку

Будь-який режим доставки може доставити подію повторно: дублікат на транспортному рівні, друге читання збереженої події. Відсіюйте дублікати за ключем дедуплікації.

Що містить підписка.

МіститьЩо це
eventоголошений тип, до якого вона прив’язана
surfaceвузол, на якому її оформили, у межах цілі target, оголошеної в типі
handlerобробник, типізований за даними
positionмісце, з якого підписка (subscription) продовжує після перепідключення, щоб мовчки не почати з «зараз». Події, пропущені за цей час, не відтворюються: transient зникає, прочитати знову можна лише 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, а не мовчазне «нічого не сталося».
  • Відправку відхиляють, якщо тип не оголошено, дані не проходять схему або тип не дозволяє такої цілі.
  • Вибірка за межами строку збереженого типу повертає відмову, а не порожню сторінку.

Обмеження

Конкретні числа з’являться разом із розділом про обмеження платформи, а що стається на кожній межі, визначено вже зараз.

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