Aller au contenu principal
Briques de base

Cette page n’est pas encore traduite : la version anglaise est affichée.

Data

Modifiez un champ, la synchronisation suit. Data, c’est la mécanique sous chaque champ synchronisé : des deltas calculés par rapport au dernier état acquitté, l’aspect comme unité de politique, la priorité et la fréquence d’envoi, des abonnements qui reprennent là où ils s’étaient arrêtés, la fenêtre de rétention, et des hooks avant et après changement.

Vous vous adressez à des entités, pas à des tables — la page Entity décrit l’interface de lecture et de modification (rechercher, filtrer, trier, paginer, s’abonner à une sélection) ; cette page-ci décrit la mécanique en dessous. Aucun chemin ne mène un consommateur jusqu’à une table, et il n’existe pas de seconde façon d’écrire : un changement est une opération d’Entity, et le delta en est la conséquence.

Quand l’utiliser

  • Vous voulez répliquer l’état vers les clients sans écrire de code de snapshot : modifier un champ suffit à le synchroniser.
  • Les champs n’ont pas la même urgence ni la même audience : une priorité et un plafond de fréquence d’envoi par aspect, et un prédicat de visibilité pour le brouillard de guerre.
  • Un client qui se reconnecte ne doit pas diverger en silence : un trou est détecté et signalé, et un trou qui dépasse la fenêtre de rétention reçoit l’état complet.
  • Vous avez besoin du passé récent : la fenêtre de rétention des deltas, indexée par sim_time, est ce que lisent la prédiction et la compensation de latence (lag compensation).
  • Une règle de validation doit vivre à un seul endroit : un hook avant changement borne ou bloque la valeur avant que le changement ne soit appliqué.
  • Laissez les réglages de côté si vous voulez seulement lire ou interroger. L’interface d’Entity s’appuie sur cette mécanique sans y toucher.

Qui fait quoi

ActeurCe qu’il fait sur cette page
schema-authordéclare les aspects, leur politique de synchronisation et le prédicat de visibilité
tout acteurs’abonne à une cible ; reprend depuis une position ; demande l’état complet
backend-servicehooks avant et après changement
operatorlit le coût des paquets par acteur ; voit quand la livraison se dégrade ou qu’un paquet est coupé

En bref

Deux aspects sur tank : le mouvement à 30 envois par seconde, l’équipement pour son seul propriétaire
public class Motion
{
    public Vector3 Position;
    [Sync(Hz = 4)] public float Fuel;                    // one field overrides the aspect
}

public class Loadout { public int Ammo; }

[Entity("tank")]
public class Tank
{
    [Aspect("motion", Priority = 10, Hz = 30)]           // policy lives on the aspect
    public Motion Motion = new();

    [Aspect("loadout", Visible = "owner == caller.player")]
    public Loadout Loadout = new();

    public float InternalHeat;                           // in no aspect — never leaves the server
}

tank.Motion.Position = next;   // ← the change; the delta is its consequence
export class Motion {
  position!: Vector3;
  @Sync({ hz: 4 }) fuel = 0;                             // one field overrides the aspect
}

export class Loadout { ammo = 0; }

@Entity('tank')
export class Tank {
  @Aspect('motion', { priority: 10, hz: 30 })            // policy lives on the aspect
  motion = new Motion();

  @Aspect('loadout', { visible: 'owner == caller.player' })
  loadout = new Loadout();

  internalHeat = 0;                                      // in no aspect — never leaves the server
}

tank.motion.position = next;   // ← the change; the delta is its consequence
class Motion:
    position: Vector3
    fuel: float = sync(hz=4)                             # one field overrides the aspect

class Loadout:
    ammo: int = 0

@entity("tank")
class Tank:
    motion: Motion = aspect("motion", priority=10, hz=30)          # policy lives on the aspect
    loadout: Loadout = aspect("loadout", visible="owner == caller.player")
    internal_heat: float = 0.0                           # in no aspect — never leaves the server

tank.motion.position = next_pos   # ← the change; the delta is its consequence
Bientôt — Go

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()
struct FMotion
{
    GENERATED_BODY()

    UPROPERTY() FVector3f Position;
    UPROPERTY(PSSync = (Hz = 4)) float Fuel;             // one field overrides the aspect
};

USTRUCT()
struct FLoadout
{
    GENERATED_BODY()

    UPROPERTY() int32 Ammo;
};

UCLASS(PSEntity = "tank")
class UTank : public UObject
{
    GENERATED_BODY()

    UPROPERTY(PSAspect = (Name = "motion", Priority = 10, Hz = 30))   // policy lives on the aspect
    FMotion Motion;

    UPROPERTY(PSAspect = (Name = "loadout", Visible = "owner == caller.player"))
    FLoadout Loadout;

    float InternalHeat = 0.f;                            // no UPROPERTY, in no aspect — never leaves the server
};

Tank->Motion.Position = Next;   // ← the change; the delta is its consequence
Unity C# is the same C# API here — les mêmes attributs C# compilent dans Unity (base 2021.3) — les déclarations sont publiées dans le même modèle, et modifier un champ suffit, là aussi, à le synchroniser
public class Motion
{
    public Vector3 Position;
    [Sync(Hz = 4)] public float Fuel;                    // one field overrides the aspect
}

public class Loadout { public int Ammo; }

[Entity("tank")]
public class Tank
{
    [Aspect("motion", Priority = 10, Hz = 30)]           // policy lives on the aspect
    public Motion Motion = new();

    [Aspect("loadout", Visible = "owner == caller.player")]
    public Loadout Loadout = new();

    public float InternalHeat;                           // in no aspect — never leaves the server
}

tank.Motion.Position = next;   // ← the change; the delta is its consequence

Le modèle

Ce que transporte un delta.

ChampCe que c’est
champs modifiéseux seuls, jamais l’objet entier
pairla paire instance × aspect à laquelle il appartient
numberun numéro de séquence au sein de cette paire ; c’est lui qui rend un trou détectable

Ce que déclare un aspect.

Tout se déclare par attribut, sur l’aspect ou sur un champ isolé, jamais par un appel à l’exécution. L’aspect fixe la valeur par défaut et un champ peut la surcharger ; l’aspect reste l’unité de politique, sans quoi il n’y aurait rien à partir de quoi assembler des presets.

DéclareValeurs, et ce que ce n’est pas
priorityfixe ce qui part en premier quand le canal ne suffit pas. Pas une promesse de latence : elle est relative, et ordonne l’envoi entre les champs au lieu de garantir un délai de livraison
fréquence de mise à jour max.une borne supérieure sur l’envoi. Pas une promesse de réception à ce rythme — la réception dépend du canal
deltas uniquementne pas envoyer ce qui n’a pas changé
mode de livraisonshared packet : la même chose pour tout le monde, peu coûteux en CPU. per-actor packet : chaque destinataire reçoit le sien, selon sa zone de visibilité ; coûteux en CPU, et indispensable pour de grandes populations
règle de visibilitéle prédicat qui décide qui reçoit quoi que ce soit ; Visibility le décrit en détail
a field changes — revision 15a receiver that acknowledged 12a receiver that acknowledged 14a receiver past the retained window delta 12 → 15delta 14 → 15full state, marked as such
Chaque destinataire reçoit la différence par rapport à l’état qu’il a acquitté ; celui qui dépasse la fenêtre de rétention reçoit l’état complet (les numéros de révision sont donnés à titre d’exemple)

Ce que contient un abonnement.

ContientCe que c’est
targetune instance, une sélection ou un aspect ; l’abonnement reçoit les deltas de cette cible. Une cible n’est pas un flux : une cible peut couvrir de nombreuses paires, et l’ordre est garanti au sein d’une paire, pas à l’échelle d’une cible
positionle point de reprise, que présente le consommateur. Si le trou est plus grand que la fenêtre de rétention, c’est l’état complet qui arrive au lieu d’un flux de deltas : une longue déconnexion ne laisse jamais un client dans un état faux sans qu’il le sache
stateactive → gap detected → resynchronised | closed, et closed est terminal

Ce qui vaut pour tout flux.

ToujoursCe que c’est
fusionles deltas l’admettent (voir plus bas). C’est ce qui distingue un delta d’un événement, dont la perte fait disparaître une information pour de bon
détection des trousperdre un delta en silence est interdit ; le consommateur compte le numéro de séquence de la paire
ordregaranti au sein d’une paire instance × aspect ; entre paires, il n’est garanti sous aucune forme
parcoursuniquement sur ce qui est déclaré : ce qui peut servir de filtre, de tri ou d’inclusion, c’est un champ déclaré et une référence déclarée. L’interface de sélection d’une entité est la projection de ce modèle, et cette primitive ne donne au consommateur aucun parcours propre — il n’y a pas de second langage de requête
historiqueconstruit à partir des deltas : la fenêtre instantanée d’une entité est une fenêtre de rétention de deltas indexée par sim_time. Sa profondeur est la limite de cette primitive, et elle ne garantit aucune reproductibilité sur les champs à virgule flottante
le budget de paquetsse dégrade comme déclaré : quand le budget par acteur est épuisé, la plateforme se replie sur le paquet partagé comme déclaré, au lieu de commencer à perdre des destinataires au hasard

Des valeurs intermédiaires peuvent sauter

Les deltas fusionnent entre deux envois : 100 → 90 → 80 peut arriver sous la forme 100 → 80, car l’état final reste correct. La liste des valeurs livrées n’est pas l’historique de ce qui s’est passé.

Ce qu’un hook peut faire, et quand.

Un hook avant changement sur l’aspect motion : un carburant négatif est rejeté avant que le changement ne soit appliqué
[Before(Data.Change, aspect: "tank.motion")]
public static Verdict ClampFuel(Change<Motion> change) =>
    change.Next.Fuel < 0 ? Hook.Reject("negative fuel") : Hook.Continue(change);
export const clampFuel = before(Data.change, { aspect: 'tank.motion' }, (change: Change<Motion>) =>
  change.next.fuel < 0 ? Hook.reject('negative fuel') : Hook.continue(change));
@before(data.change, aspect="tank.motion")
def clamp_fuel(change: Change[Motion]) -> Verdict:
    return hook.reject("negative fuel") if change.next.fuel < 0 else hook.proceed(change)
Bientôt — Go

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.

S’exécute hors du moteur

Un hook est une fonction cloud : il s’exécute sur la plateforme, pas dans le moteur. Écrivez-le en C#, TypeScript ou Python — votre code Unreal s’abonne aux événements qui en résultent. Des fonctions écrites côté moteur sont prévues : des graphes Blueprint, et du C++ (traduit vers une cible de script validée par la plateforme). Les deux sont analysés et publiés comme n’importe quelle déclaration ; l’exécution reste sur la plateforme.

S’exécute hors du moteur

Un hook est une fonction cloud : il s’exécute sur la plateforme, pas dans le moteur. Écrivez-le en C#, TypeScript ou Python — votre code Unity s’abonne aux événements qui en résultent.

HookCe qu’il peut faire
avant un changementle modifier ou y opposer un veto. Un changement refusé ne produit aucun delta — les abonnés ne voient rien, plutôt qu’une valeur suivie d’une correction
après un changementajouter des effets de bord ; il ne peut jamais faire échouer le changement

La suppression se branche sur Entity, où elle est définie ; cette primitive se branche sur le changement.

Erreurs

  • Une cible d’abonnement non déclarée est un refus de validation.
  • L’absence de permission pour s’abonner répond forbidden ou not found, selon que l’existence de la cible est elle-même un secret ou non — le refus ne doit pas devenir un oracle.
  • Une position de reprise illisible est une requête invalide, jamais un redémarrage silencieux à partir de maintenant.
  • Un abonnement fermé côté plateforme est un conflit, et il est observable : l’état closed de la machine est terminal, et le client n’a pas à le deviner.
  • Un nombre d’abonnements épuisé est un conflit : la permission est bien détenue, mais il ne reste plus de capacité.

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é.

  • La fenêtre de rétention des deltas : reprendre depuis un point plus ancien que la fenêtre donne l’état complet plutôt qu’un refus.
  • Abonnements par acteur : un nouvel abonnement est refusé et les existants continuent.
  • Taille d’un delta : le delta est découpé plutôt que tronqué, et le découpage est observable.
  • La fréquence d’envoi : une borne supérieure, pas une garantie.
  • Le coût d’un paquet par acteur : en cas d’épuisement, dégradation déclarée vers le paquet partagé.