Skip to main content
Building blocks

Not translated yet — showing English.

Events

An event is the fact that something happened, delivered to everyone who should hear it. Use it for what happens once and can't be caught up from a current value: a shot, a purchase, a room joined.

When to use it

  • Something happened and others must react: a shot fired, a door locked, a match ended.
  • The audience varies. The same emit reaches a squad, a room or one actor, depending on the target the type declares.
  • You want typed handlers with autocompletion. A declared event becomes send.* and on.* on its surface, each with its own contract.
  • The fact must still be readable an hour later: declare the type retained and read it back by period.

Who does what

ActorOn this page
schema-authordeclares events with [Event], pushes the schema
any actoremits via send.*, subscribes via on.*

At a glance

Declare RallyCall once; emit with send.*, react with 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))
Coming soon — Go

Attribute-style declaration in Go is still open (O-1) — the samples land when the carrier is decided.

// 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()));

An event declared inside a module or group surfaces only there: squad.send.rallyCall exists because rally_call is declared for squads, and the emit reaches squad members. Events a module emits outward are part of its declared contract; callers never see undeclared ones.

The model

What an event type declares.

DeclaresWhat it is
namean explicit wire name, declared rather than derived from the symbol
payloadthe schema of what an emission carries
targetwhere emissions go: an entity instance, a group, a room or the global context. A group target delivers one signal to many recipients. An addressee that changes is expressed as a group, not as an address passed at the emit
clocksim_time or timestamp, never both. sim_time is for facts inside a simulation, which take part in prediction, lag compensation and rollback; timestamp is for facts outside it, like a purchase or a sign-in
retentiontransient or retained, see below
termon a retained type: how long it is kept and what happens at expiry. There is no "forever"
deliveryat most once, at least once or exactly once, declared on the type so a subscriber never has to ask. "Exactly once" states the bounds it holds within
contextglobal or local. A globally declared name is visible in local contexts; a locally declared one is not visible above

What an emission carries.

FieldWhat it is
typethe declared event. Emissions never merge: two shots are two events. This is what separates an event from a [Sync] field in Data
payloadconforming to the type's schema
sourcethe emitting actor, plus its instance when an entity emitted it
stampon the type's declared clock
dedup keypresent under every delivery mode
cause keyon an event the platform emits because of another one: the id of that event, so a chain is rebuilt by key rather than by comparing stamps

An event from a client is a claim

The authoritative side checks a client-emitted event before anything depends on it. Don't treat the source as proof.

Handle redelivery

Every delivery mode can redeliver: a transport duplicate, a second read of a retained event. Deduplicate on the dedup key.

What a subscription holds.

HoldsWhat it is
eventthe declared type it is bound to
surfacethe node it is taken on, inside the type's declared target
handlertyped to the payload
positionwhere it resumes from after a reconnect, so it doesn't silently restart at "now". Events missed in the gap are not replayed: a transient one is lost, only a retained one can be read back

True of every event.

AlwaysWhat it is
audiencenever listed by the sender: it is the declared target, narrowed to whoever is subscribed, then gated by Access. A stream can be closed by a predicate even where the type is visible
phasesemitted, then delivered. An event has no state machine: it happens once
orderingpromised within one stream, and a stream is one emitting instance: two events from the same instance arrive in emit order
gap detectionwhere the delivery mode admits loss, the subscriber learns of the gap instead of skipping it silently

No order across streams

Nothing is promised between two instances, or between a delta and an event about the same change. When you need order across streams, bring the messages into one stream or carry a causal stamp in the payload.

Retention is part of the type

Whether an event is kept after delivery is declared on its type, not chosen at the emit, so the same type is always kept the same way. A retained event is read back by type and period, not through the query surface of Data.

TransientRetained
Reacheswhoever is subscribed at that momentthat, and a subscriber arriving afterwards
Afterwardsgonekept for a declared term
Readable backnoyes, over the term
Past the term—a selection refuses, rather than answering empty
A retained event: declared with its term, read back by period
[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)
Coming soon — Go

Attribute-style declaration in Go is still open (O-1) — the samples land when the carrier is decided.

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

Errors

  • Publishing and subscribing are separate rights; neither implies the other. A subscribe without the right answers forbidden, not not-found: the type is in the module's declared contract, so there is nothing to hide.
  • Subscribing to a type the module hasn't declared is a contract error, a typed Problem, not a silent no-op.
  • An emit is refused for an undeclared type, a payload that fails the schema, or a target the type doesn't allow.
  • A selection past a retained type's term refuses instead of returning an empty page.

Limits

The numbers arrive with the platform limits chapter; what happens at each ceiling is fixed now.

  • Payload size: over the ceiling the publish fails and the event doesn't happen. Payloads are never truncated.
  • Publish rate per source: a rate-limit refusal carrying the moment to retry.
  • Subscriptions per actor: the new subscription is refused, the existing ones are kept.
  • Retention volume per type: eviction by the declared policy, by term.