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.*yon.*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é
| Actor | Qué hace en esta página |
|---|---|
schema-author | declara eventos con [Event] y sube el esquema |
cualquier actor | emite con send.* y se suscribe con on.* |
En resumen
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))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.
| Declara | Qué es |
|---|---|
name | un nombre explícito para la red, que se declara en vez de derivarse del símbolo |
payload | el esquema de lo que lleva una emisión |
target | a 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 |
clock | sim_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 |
retention | transient o retained, consulta más abajo |
term | en un tipo retained: cuánto tiempo se guarda y qué pasa al vencer. No existe “para siempre” |
delivery | como 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 |
context | global 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.
| Campo | Qué es |
|---|---|
type | el 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 |
payload | conforme al esquema del tipo |
source | el actor que emite, más su instancia cuando lo emitió una entidad |
stamp | según el reloj declarado en el tipo |
clave de deduplicación | presente en todos los modos de entrega |
clave de causa | en 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.
| Contiene | Qué es |
|---|---|
event | el tipo declarado al que está vinculada |
surface | el nodo en el que se toma, dentro del target declarado del tipo |
handler | tipado según el payload |
position | desde 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.
| Siempre | Qué es |
|---|---|
audiencia | el 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 |
fases | emitido y luego entregado. Un evento no tiene máquina de estados: ocurre una sola vez |
orden | garantizado 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 huecos | donde 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.
| Transitorio | Retenido | |
|---|---|---|
| A quién llega | a quien esté suscrito en ese momento | a ellos y a quien se suscriba después |
| Después | desaparece | se guarda durante el plazo declarado |
| Se puede volver a leer | no | sí, durante el plazo |
| Pasado el plazo | — | una selección devuelve un rechazo en vez de una respuesta vacía |
[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)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
Problemtipado, 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.