Esta página ainda não foi traduzida: mostrando a versão em inglês.
Events
Um evento é o registro de que algo aconteceu, entregue a todos que precisam saber. Use eventos para o que acontece uma vez só e não pode ser recuperado a partir de um valor atual: um tiro, uma compra, a entrada numa sala.
Quando usar
- Algo aconteceu e outros precisam reagir: um tiro disparado, uma porta trancada, uma partida encerrada.
- O público varia. A mesma emissão chega a um esquadrão, a uma sala (room) ou a um único ator, conforme o destino que o tipo declara.
- Você quer handlers tipados com autocompletar. Um evento declarado vira
send.*eon.*na interface dele, cada um com seu próprio contrato. - O fato precisa continuar legível uma hora depois: declare o tipo como retido e leia de volta por período.
Quem faz o quê
| Ator | O que faz nesta página |
|---|---|
schema-author | declara eventos com [Event] e publica o esquema |
qualquer ator | emite via send.* e assina via on.* |
Resumo
RallyCall uma vez; emita com send.* e reaja com 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))Ainda não está decidido como declarar com atributos em Go (O-1). Os exemplos chegam quando a forma de escrita for escolhida.
// 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()));Um evento declarado dentro de um módulo ou grupo só aparece ali: squad.send.rallyCall existe porque rally_call está declarado para esquadrões, e a emissão chega aos membros do esquadrão. Os eventos que um módulo emite para fora fazem parte do contrato declarado dele; quem chama nunca vê os que não foram declarados.
O modelo
O que um tipo de evento declara.
| Declara | O que é |
|---|---|
name | um nome explícito na rede, declarado em vez de derivado do símbolo |
payload | o esquema do que cada emissão carrega |
target | para onde vão as emissões: uma instância de entidade, um grupo, uma sala ou o contexto global. Um grupo como destino entrega um único sinal a muitos destinatários. Um destinatário que muda é expresso como grupo, não como um endereço passado na emissão |
clock | sim_time ou timestamp, nunca os dois. sim_time serve para fatos dentro de uma simulação, que participam da predição, da compensação de lag e do rollback; timestamp serve para fatos fora dela, como uma compra ou um login |
retention | transient ou retained, veja abaixo |
term | em um tipo retained: por quanto tempo ele é guardado e o que acontece quando expira. Não existe “para sempre” |
delivery | no máximo uma vez, pelo menos uma vez ou exatamente uma vez, declarado no tipo para que uma assinatura (subscription) nunca precise perguntar. “Exatamente uma vez” informa os limites dentro dos quais a garantia vale |
context | global ou local. Um nome declarado no contexto global é visível nos contextos locais; um declarado localmente não é visível acima |
O que uma emissão carrega.
| Campo | O que é |
|---|---|
type | o evento declarado. Emissões nunca se fundem: dois tiros são dois eventos. É isso que separa um evento de um campo [Sync] em Data & Subscriptions |
payload | de acordo com o esquema do tipo |
source | o ator que emitiu, mais a instância dele quando quem emitiu foi uma entidade |
stamp | no relógio declarado pelo tipo |
chave de deduplicação | presente em todos os modos de entrega |
chave de causa | em um evento que a plataforma emite por causa de outro: o id desse outro evento, para que uma cadeia seja reconstruída pela chave, e não comparando carimbos de tempo |
Um evento vindo de um cliente é uma alegação
O lado autoritativo verifica um evento emitido por um cliente antes que qualquer coisa dependa dele. Não trate o source como prova.
Trate a reentrega
Todo modo de entrega pode entregar de novo: uma duplicata do transporte, uma segunda leitura de um evento retido. Elimine duplicatas pela chave de deduplicação.
O que uma assinatura guarda.
| Guarda | O que é |
|---|---|
event | o tipo declarado ao qual ela está ligada |
surface | o nó em que ela é feita, dentro do target declarado pelo tipo |
handler | tipado conforme o payload |
position | de onde ela retoma depois de uma reconexão, para não recomeçar em silêncio no “agora”. Eventos perdidos no intervalo não são reenviados: um transitório se perde, só um retained pode ser lido de volta |
Vale para todo evento.
| Sempre | O que é |
|---|---|
audience | nunca listado por quem envia: é o target declarado, reduzido a quem está assinando e depois filtrado por Access & Roles. Um fluxo pode ser fechado por um predicado mesmo onde o tipo é visível |
phases | emitido, depois entregue. Um evento não tem máquina de estados: acontece uma vez |
ordem | garantida dentro de um fluxo, e um fluxo é uma instância emissora: dois eventos da mesma instância chegam na ordem em que foram emitidos |
detecção de lacunas | onde o modo de entrega admite perda, quem assina fica sabendo da lacuna em vez de pulá-la em silêncio |
Não há ordem entre fluxos
Nada é garantido entre duas instâncias, nem entre um delta e um evento sobre a mesma mudança. Quando você precisar de ordem entre fluxos, junte as mensagens em um só fluxo ou leve um carimbo causal no payload.
A retenção faz parte do tipo
Se um evento é guardado depois da entrega é algo declarado no tipo, não escolhido na emissão, então o mesmo tipo é sempre guardado do mesmo jeito. Um evento retido é lido de volta por tipo e período, não pela interface de consulta do módulo Data & Subscriptions.
| Transitório | Retido | |
|---|---|---|
| Chega a | quem está assinando naquele momento | esses, e também quem assinar depois |
| Depois | some | fica guardado por um prazo declarado |
| Pode ser lido de volta | não | sim, durante o prazo |
| Depois do prazo | — | uma seleção é recusada, em vez de responder vazia |
[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)Ainda não está decidido como declarar com atributos em Go (O-1). Os exemplos chegam quando a forma de escrita for escolhida.
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);Erros
- Publicar e assinar são direitos separados; um não implica o outro. Uma assinatura sem o direito responde forbidden, não not found: o tipo está no contrato declarado do módulo, então não há nada a esconder.
- Assinar um tipo que o módulo não declarou é um erro de contrato, um
Problemtipado, e não uma operação que silenciosamente não faz nada. - Uma emissão é recusada para um tipo não declarado, um payload que não passa no esquema ou um destino que o tipo não permite.
- Uma seleção além do prazo de um tipo retido é recusada em vez de devolver uma página vazia.
Limites
Os números chegam com o capítulo de limites da plataforma; o que acontece em cada teto já está definido.
- Tamanho do payload: acima do teto, a publicação falha e o evento não acontece. Payloads nunca são truncados.
- Taxa de publicação por origem: uma recusa por limite de taxa, com o momento em que dá para tentar de novo.
- Assinaturas por ator: a nova assinatura é recusada; as existentes continuam.
- Volume de retenção por tipo: descarte pela política declarada, por prazo.