Saltar al contenido principal
Bloques de construcción

Esta página aún no está traducida: se muestra la versión en inglés.

Events

Un evento es el hecho de que algo pasó, entregado a todos los que deben enterarse. Úsalo para lo que ocurre una sola vez y no se puede recuperar a partir de un valor actual: un disparo, una compra, la entrada a una sala.

Cuándo usarlo

  • Algo pasó y otros tienen que reaccionar: se disparó un tiro, se cerró una puerta, terminó una partida.
  • La audiencia cambia. La misma emisión llega a un escuadrón, a una sala (room) o a un solo actor, según el destino que declare el tipo.
  • Quieres handlers tipados con autocompletado. Un evento declarado se convierte en send.* y on.* en su interfaz, cada uno con su propio contrato.
  • El hecho tiene que poder leerse una hora después: declara el tipo con retención y vuelve a leerlo por período.

Quién hace qué

ActorQué hace en esta página
schema-authordeclara eventos con [Event] y sube el esquema
cualquier actoremite con send.* y se suscribe con on.*

En resumen

Declara RallyCall una vez; emite con send.* y reacciona con 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))
Próximamente — Go

Todavía no está decidido cómo declarar con atributos en Go (O-1). Los ejemplos llegarán cuando se elija la forma de escribirlo.

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

Un evento declarado dentro de un módulo o grupo aparece solo ahí: squad.send.rallyCall existe porque rally_call está declarado para escuadrones, y la emisión llega a los miembros del escuadrón. Los eventos que un módulo emite hacia afuera forman parte de su contrato declarado; quien llama nunca ve los que no están declarados.

El modelo

Qué declara un tipo de evento.

DeclaraQué es
nameun nombre explícito para la red, que se declara en vez de derivarse del símbolo
payloadel esquema de lo que lleva una emisión
targeta dónde van las emisiones: una instancia de entidad, un grupo, una sala o el contexto global. Con un grupo como destino, una sola señal llega a muchos destinatarios. Un destinatario que cambia se expresa como grupo, no como una dirección que se pasa al emitir
clocksim_time o timestamp, nunca ambos. sim_time es para hechos dentro de una simulación, que participan en la predicción, la compensación de lag (lag compensation) y el rollback; timestamp es para hechos fuera de ella, como una compra o un inicio de sesión
retentiontransient o retained, consulta más abajo
termen un tipo retained: cuánto tiempo se guarda y qué pasa al vencer. No existe “para siempre”
deliverycomo máximo una vez, al menos una vez o exactamente una vez, declarado en el tipo para que ningún suscriptor tenga que preguntar. “Exactamente una vez” indica dentro de qué límites se cumple
contextglobal o local. Un nombre declarado globalmente es visible en los contextos locales; uno declarado localmente no es visible hacia arriba

Qué lleva una emisión.

CampoQué es
typeel evento declarado. Las emisiones nunca se fusionan: dos disparos son dos eventos. Eso es lo que distingue un evento de un campo [Sync] en Data & Subscriptions
payloadconforme al esquema del tipo
sourceel actor que emite, más su instancia cuando lo emitió una entidad
stampsegún el reloj declarado en el tipo
clave de deduplicaciónpresente en todos los modos de entrega
clave de causaen un evento que la plataforma emite a raíz de otro: el id de ese evento, para reconstruir la cadena por clave y no comparando marcas de tiempo

Un evento de un cliente es una afirmación

El lado autoritativo verifica un evento emitido por un cliente antes de que algo dependa de él. No tomes el source como prueba.

Prepárate para las reentregas

Cualquier modo de entrega puede volver a entregar: un duplicado del transporte, una segunda lectura de un evento retenido. Deduplica con la clave de deduplicación.

Qué contiene una suscripción.

ContieneQué es
eventel tipo declarado al que está vinculada
surfaceel nodo en el que se toma, dentro del target declarado del tipo
handlertipado según el payload
positiondesde dónde se reanuda tras una reconexión, para que no vuelva a empezar en silencio desde “ahora”. Los eventos perdidos durante el corte no se reproducen: uno transitorio se pierde, y solo uno retained se puede volver a leer

Lo que vale para cualquier evento.

SiempreQué es
audienciael emisor nunca la enumera: es el target declarado, reducido a quienes están suscritos y filtrado después por Access & Roles. Un flujo (stream) puede cerrarse con un predicado aunque el tipo sea visible
fasesemitido y luego entregado. Un evento no tiene máquina de estados: ocurre una sola vez
ordengarantizado dentro de un mismo flujo, y un flujo es una instancia emisora: dos eventos de la misma instancia llegan en el orden en que se emitieron
detección de huecosdonde el modo de entrega admite pérdidas, el suscriptor se entera del hueco en lugar de saltárselo en silencio

No hay orden entre flujos

No se garantiza nada entre dos instancias, ni entre un delta y un evento sobre el mismo cambio. Si necesitas orden entre flujos, lleva los mensajes a un solo flujo o incluye una marca causal en el payload.

La retención es parte del tipo

Si un evento se guarda después de entregarse se declara en su tipo, no se elige al emitirlo, así que un mismo tipo siempre se guarda de la misma forma. Un evento retenido se vuelve a leer por tipo y período, no mediante la interfaz de consultas de Data & Subscriptions.

TransitorioRetenido
A quién llegaa quien esté suscrito en ese momentoa ellos y a quien se suscriba después
Despuésdesaparecese guarda durante el plazo declarado
Se puede volver a leernosí, durante el plazo
Pasado el plazo—una selección devuelve un rechazo en vez de una respuesta vacía
Un evento retenido: se declara con su plazo y se vuelve a leer 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)
Próximamente — Go

Todavía no está decidido cómo declarar con atributos en Go (O-1). Los ejemplos llegarán cuando se elija la forma de escribirlo.

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

Errores

  • Publicar y suscribirse son derechos distintos; ninguno implica el otro. Una suscripción sin ese derecho responde forbidden, no “no encontrado”: el tipo está en el contrato declarado del módulo, así que no hay nada que ocultar.
  • Suscribirse a un tipo que el módulo no declaró es un error de contrato, un Problem tipado, no algo que se ignora en silencio.
  • Una emisión se rechaza si el tipo no está declarado, si el payload no cumple el esquema o si el destino no está permitido por el tipo.
  • Una selección pasado el plazo de un tipo retenido devuelve un rechazo en lugar de una página vacía.

Límites

Los números llegarán con el capítulo sobre los límites de la plataforma; lo que pasa en cada tope ya está definido.

  • Tamaño del payload: por encima del tope, la publicación falla y el evento no ocurre. Los payloads nunca se truncan.
  • Frecuencia de publicación por origen: un rechazo por límite de frecuencia que indica cuándo reintentar.
  • Suscripciones por actor: se rechaza la nueva suscripción y se conservan las existentes.
  • Volumen de retención por tipo: se descartan eventos según la política declarada, por plazo.