Pular para o conteúdo principal
Blocos de construção

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.* e on.* 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ê

AtorO que faz nesta página
schema-authordeclara eventos com [Event] e publica o esquema
qualquer atoremite via send.* e assina via on.*

Resumo

Declare 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))
Em breve — Go

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.

DeclaraO que é
nameum nome explícito na rede, declarado em vez de derivado do símbolo
payloado esquema do que cada emissão carrega
targetpara 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
clocksim_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
retentiontransient ou retained, veja abaixo
termem um tipo retained: por quanto tempo ele é guardado e o que acontece quando expira. Não existe “para sempre”
deliveryno 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
contextglobal 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.

CampoO que é
typeo 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
payloadde acordo com o esquema do tipo
sourceo ator que emitiu, mais a instância dele quando quem emitiu foi uma entidade
stampno relógio declarado pelo tipo
chave de deduplicaçãopresente em todos os modos de entrega
chave de causaem 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.

GuardaO que é
evento tipo declarado ao qual ela está ligada
surfaceo nó em que ela é feita, dentro do target declarado pelo tipo
handlertipado conforme o payload
positionde 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.

SempreO que é
audiencenunca 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
phasesemitido, depois entregue. Um evento não tem máquina de estados: acontece uma vez
ordemgarantida 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 lacunasonde 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órioRetido
Chega aquem está assinando naquele momentoesses, e também quem assinar depois
Depoissomefica guardado por um prazo declarado
Pode ser lido de voltanãosim, durante o prazo
Depois do prazo—uma seleção é recusada, em vez de responder vazia
Um evento retido: declarado com o prazo e lido de volta por período
[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)
Em breve — Go

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 Problem tipado, 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.