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.*andon.*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
| Actor | On this page |
|---|---|
schema-author | declares events with [Event], pushes the schema |
any actor | emits via send.*, subscribes via on.* |
At a glance
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))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.
| Declares | What it is |
|---|---|
name | an explicit wire name, declared rather than derived from the symbol |
payload | the schema of what an emission carries |
target | where 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 |
clock | sim_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 |
retention | transient or retained, see below |
term | on a retained type: how long it is kept and what happens at expiry. There is no "forever" |
delivery | at 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 |
context | global or local. A globally declared name is visible in local contexts; a locally declared one is not visible above |
What an emission carries.
| Field | What it is |
|---|---|
type | the declared event. Emissions never merge: two shots are two events. This is what separates an event from a [Sync] field in Data |
payload | conforming to the type's schema |
source | the emitting actor, plus its instance when an entity emitted it |
stamp | on the type's declared clock |
dedup key | present under every delivery mode |
cause key | on 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.
| Holds | What it is |
|---|---|
event | the declared type it is bound to |
surface | the node it is taken on, inside the type's declared target |
handler | typed to the payload |
position | where 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.
| Always | What it is |
|---|---|
audience | never 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 |
phases | emitted, then delivered. An event has no state machine: it happens once |
ordering | promised within one stream, and a stream is one emitting instance: two events from the same instance arrive in emit order |
gap detection | where 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.
| Transient | Retained | |
|---|---|---|
| Reaches | whoever is subscribed at that moment | that, and a subscriber arriving afterwards |
| Afterwards | gone | kept for a declared term |
| Readable back | no | yes, over the term |
| Past the term | — | a selection refuses, rather than answering empty |
[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)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.