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
| Acteur | Ce qu’il fait sur cette page |
|---|---|
schema-author | déclare les aspects, leur politique de synchronisation et le prédicat de visibilité |
tout acteur | s’abonne à une cible ; reprend depuis une position ; demande l’état complet |
backend-service | hooks avant et après changement |
operator | lit le coût des paquets par acteur ; voit quand la livraison se dégrade ou qu’un paquet est coupé |
En bref
tank : le mouvement à 30 envois par seconde, l’équipement pour son seul propriétairepublic 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 consequenceexport 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 consequenceclass 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 consequenceLa 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 consequencepublic 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 consequenceLe modèle
Ce que transporte un delta.
| Champ | Ce que c’est |
|---|---|
champs modifiés | eux seuls, jamais l’objet entier |
pair | la paire instance × aspect à laquelle il appartient |
number | un 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éclare | Valeurs, et ce que ce n’est pas |
|---|---|
priority | fixe 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 uniquement | ne pas envoyer ce qui n’a pas changé |
mode de livraison | shared 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 |
Ce que contient un abonnement.
| Contient | Ce que c’est |
|---|---|
target | une 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 |
position | le 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 |
state | active → gap detected → resynchronised | closed, et closed est terminal |
Ce qui vaut pour tout flux.
| Toujours | Ce que c’est |
|---|---|
fusion | les 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 trous | perdre un delta en silence est interdit ; le consommateur compte le numéro de séquence de la paire |
ordre | garanti au sein d’une paire instance × aspect ; entre paires, il n’est garanti sous aucune forme |
parcours | uniquement 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 |
historique | construit à 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 paquets | se 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.
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)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.
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.
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.
| Hook | Ce qu’il peut faire |
|---|---|
| avant un changement | le 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 changement | ajouter 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
closedde 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é.