Cette page n’est pas encore traduite : la version anglaise est affichée.
Events
Un événement, c’est le fait que quelque chose s’est produit, livré à tous ceux qui doivent l’apprendre. Servez-vous-en pour ce qui arrive une seule fois et qu’aucune valeur courante ne permet de rattraper : un tir, un achat, l’entrée dans une salle.
Quand l’utiliser
- Quelque chose s’est produit et d’autres doivent réagir : un tir, une porte verrouillée, un match terminé.
- L’audience varie. La même émission atteint une escouade, une salle (room) ou un seul acteur, selon la cible que déclare le type.
- Vous voulez des handlers typés avec l’autocomplétion. Un événement déclaré devient
send.*eton.*sur son interface, chacun avec son propre contrat. - Le fait doit rester lisible une heure plus tard : déclarez le type comme conservé et relisez-le par période.
Qui fait quoi
| Acteur | Ce qu’il fait sur cette page |
|---|---|
schema-author | déclare les événements avec [Event] et publie le schéma |
tout acteur | émet via send.*, s’abonne via on.* |
En bref
RallyCall une fois ; émettez avec send.*, réagissez avec 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))La déclaration par attributs en Go n’est pas encore tranchée (O-1). Les exemples arriveront quand la forme d’écriture sera choisie.
// 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 événement déclaré dans un module ou un groupe n’apparaît que là : squad.send.rallyCall existe parce que rally_call est déclaré pour les escouades, et l’émission atteint les membres de l’escouade. Les événements qu’un module émet vers l’extérieur font partie de son contrat déclaré ; les appelants ne voient jamais ceux qui ne sont pas déclarés.
Le modèle
Ce que déclare un type d’événement.
| Déclare | Ce que c’est |
|---|---|
name | un nom réseau explicite, déclaré plut ôt que déduit du symbole |
payload | le schéma de ce que transporte une émission |
target | la destination des émissions : une instance d’entité, un groupe, une salle ou le contexte global. Une cible de groupe livre un seul signal à de nombreux destinataires. Un destinataire qui change s’exprime par un groupe, pas par une adresse passée au moment de l’émission |
clock | sim_time ou timestamp, jamais les deux. sim_time sert aux faits internes à une simulation, qui participent à la prédiction, à la compensation de latence et au rollback ; timestamp sert aux faits extérieurs à la simulation, comme un achat ou une connexion |
retention | transient ou retained, voir plus bas |
term | sur un type retained : combien de temps il est conservé et ce qui se passe à l’expiration. « Pour toujours » n’existe pas |
delivery | au plus une fois, au moins une fois ou exactement une fois, déclaré sur le type pour qu’un abonné n’ait jamais à le demander. « Exactement une fois » précise les limites dans lesquelles la garantie tient |
context | global ou local. Un nom déclaré globalement est visible dans les contextes locaux ; un nom déclaré localement n’est pas visible au-dessus |
Ce que transporte une émission.
| Champ | Ce que c’est |
|---|---|
type | l’événement déclaré. Les émissions ne fusionnent jamais : deux tirs font deux événements. C’est ce qui distingue un événement d’un champ [Sync] dans Data & Subscriptions |
payload | conforme au schéma du type |
source | l’acteur émetteur, plus son instance quand c’est une entité qui a émis |
stamp | selon l’horloge déclarée par le type |
clé de déduplication | présente quel que soit le mode de livraison |
clé de cause | sur un événement que la plateforme émet à cause d’un autre : l’id de cet autre événement, pour reconstituer une chaîne par clé plutôt qu’en comparant les horodatages |
Un événement venu d’un client n’est qu’une affirmation
Le côté faisant autorité vérifie un événement émis par un client avant que quoi que ce soit en dépende. Ne prenez pas la source pour une preuve.
Gérez les livraisons répétées
Tous les modes de livraison peuvent livrer deux fois : un doublon du transport, une seconde lecture d’un événement conservé. Dédupliquez sur la clé de déduplication.
Ce que contient un abonnement.
| Contient | Ce que c’est |
|---|---|
event | le type déclaré auquel il est lié |
surface | le nœud sur lequel il est pris, à l’intérieur de la target déclarée du type |
handler | typé selon le payload |
position | l’endroit d’où il reprend après une reconnexion, pour ne pas repartir en silence de « maintenant ». Les événements manqués pendant la coupure ne sont pas rejoués : un événement éphémère est perdu, seul un événement retained peut être relu |
Vrai pour tout événement.
| Toujours | Ce que c’est |
|---|---|
audience | jamais énumérée par l’émetteur : c’est la target déclarée, restreinte aux abonnés, puis filtrée par Access & Roles. Un prédicat peut fermer un flux même là où le type est visible |
phases | émis, puis livré. Un événement n’a pas de machine à états : il se produit une fois |
ordre | garanti au sein d’un flux, et un flux correspond à une instance émettrice : deux événements de la même instance arrivent dans l’ordre d’émission |
détection des trous | là où le mode de livraison admet des pertes, l’abonné est informé du trou au lieu de le sauter en silence |
Aucun ordre entre flux
Rien n’est garanti entre deux instances, ni entre un delta et un événement portant sur le même changement. Si vous avez besoin d’un ordre entre flux, faites passer les messages par un seul flux ou transportez un horodatage causal dans le payload.
La rétention fait partie du type
La conservation d’un événement après sa livraison se déclare sur son type, et non au moment de l’émission : un même type est donc toujours conservé de la même façon. Un événement conservé se relit par type et par période, pas via l’interface de requête de Data & Subscriptions.
| Éphémère | Conservé | |
|---|---|---|
| Atteint | les abonnés présents à cet instant | ceux-là, plus un abonné arrivé ensuite |
| Ensuite | disparu | conservé pendant une durée déclarée |
| Relisible | non | oui, pendant cette durée |
| Au-delà de la durée | — | une sélection est refusée au lieu de renvoyer un résultat vide |
[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)La déclaration par attributs en Go n’est pas encore tranchée (O-1). Les exemples arriveront quand la forme d’écriture sera choisie.
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);Erreurs
- Publier et s’abonner sont deux droits distincts ; aucun n’implique l’autre. Un abonnement sans ce droit répond forbidden, et non not found : le type figure dans le contrat déclaré du module, il n’y a donc rien à cacher.
- S’abonner à un type que le module n’a pas déclaré est une erreur de contrat, un
Problemtypé, pas une opération sans effet et silencieuse. - Une émission est refusée pour un type non déclaré, un payload qui ne respecte pas le schéma ou une cible que le type n’autorise pas.
- Une sélection au-delà de la durée d’un type conservé est refusée au lieu de renvoyer une page vide.
Limites
Les valeurs chiffrées arriveront avec le chapitre sur les limites de la plateforme ; ce qui se passe à chaque plafond est déjà fixé.
- Taille du payload : au-delà du plafond, la publication échoue et l’événement n’a pas lieu. Un payload n’est jamais tronqué.
- Débit de publication par source : un refus pour limite de débit, qui indique le moment où réessayer.
- Abonnements par acteur : le nouvel abonnement est refusé, les existants sont conservés.
- Volume de rétention par type : éviction selon la politique déclarée, par durée.