Cette page n’est pas encore traduite : la version anglaise est affichée.
Entity
Une entité est une déclaration de schéma dotée d’aspects vivants : données 0..*, états 0..*, RPC 0..*, événements 0..*, hooks et historique des modifications. Les cartes lient les obstacles à des entités, les collisions se lient à un aspect de transformation, les statistiques ne sont qu’un preset, et les objets du monde sont un preset plus une machine à états.
Le module Entity se monte à la racine : room.Entity<Door>(id) et playserv.Entities<KeyDef>() se trouvent donc directement à la racine, et non derrière un espace de noms. Il repose sur trois primitives — les événements, les RPC et les données — et sur rien d’autre. Les modules Collision, Locomotion et Prediction & Lag Comp se placent au-dessus : chacun se lie à un seul aspect, pas à l’entité entière. C’est pourquoi un contact peut déclencher une transition sans que le module de collisions sache quoi que ce soit des permissions.
Quand l’utiliser
- Un objet du monde a besoin d’un comportement, pas seulement de champs : machines à états, RPC soumises à permission et événements dans une même déclaration.
- Portes, pièges, objets à ramasser : les transitions doivent se déclencher sur des événements client, des contacts de collision ou des seuils de statistique, sans code dans la salle (room).
- Vous voulez créer vos objets de jeu en une ligne : appliquez des presets comme
world-objects, ou dérivez-en. - Pour trancher un litige, il faut l’état exact du monde au moment du tir : lisez une instance à un
sim_timepassé, dans la fenêtre déclarée. - Inutile si la chose n’a pas d’identité propre. Une valeur qui ne vit jamais qu’à l’intérieur d’autre chose, comme le texte de la plaque d’une porte, est un champ dans un aspect, pas une entité à part entière. Tout ce que l’on adresse est une entité : Data & Subscriptions fournit la mécanique sous-jacente, et aucun accès direct aux tables ne la contourne.
Qui fait quoi
| Acteur | Ce qu’il fait sur cette page |
|---|---|
schema-author | déclare les entités, les aspects, les machines à états et les presets |
tout acteur | interroge, s’abonne, appelle les RPC des entités, lit l’état |
En bref
[Aspect("info", Read = "any")] // rarely changes, everyone reads it
public class Info { public string Name; }
[Aspect("motion", Hz = 20, Read = "any", Write = "fn")] // 20 updates a second while it swings
public class Motion { public float OpenRatio; public bool Jammed; }
[Machine("gate")]
public class Gate
{
[State(Initial = true), Transition("open_requested", to: "opening")] public State Closed;
[State, AfterSeconds(1.2f, to: "open")] public State Opening;
[State, Transition("close_requested", to: "closed")] public State Open;
[State("open.blocked"), Transition("cleared", to: "open", Guard = "!motion.jammed")] public State Blocked;
}
[Entity("key-def", Persistence = Persistence.Persistent)] // authored content: key.bronze, key.gold
public class KeyDef
{
[Key] public string Key;
[Aspect] public Info Info;
}
[Entity("door", Persistence = Persistence.Runtime)]
public class Door
{
[Aspect] public Info Info;
[Aspect] public Motion Motion;
[Machine] public Gate Gate;
[Ref] public Ref<KeyDef> Needs; // holds the id, never the key
[Event("locked", Clock = Clock.SimTime)] public Event Locked; // reaches whoever sees the door
[EntityRpc(Requires = Entity.Permissions.Execute, Rows = "caller in entity.room")]
public void RequestOpen(Actor caller)
{
if (caller.Inventory.Has(Needs)) Gate.Fire("open_requested");
else Locked.Send();
}
}@Aspect('info', { read: 'any' }) // rarely changes, everyone reads it
export class Info { name = ''; }
@Aspect('motion', { hz: 20, read: 'any', write: 'fn' }) // 20 updates a second while it swings
export class Motion { openRatio = 0; jammed = false; }
@Machine('gate')
export class Gate {
@State({ initial: true }) @Transition('open_requested', { to: 'opening' }) closed: State;
@State() @AfterSeconds(1.2, { to: 'open' }) opening: State;
@State() @Transition('close_requested', { to: 'closed' }) open: State;
@State('open.blocked') @Transition('cleared', { to: 'open', guard: '!motion.jammed' }) blocked: State;
}
@Entity('key-def', { persistence: Persistence.Persistent }) // authored content: key.bronze, key.gold
export class KeyDef {
@Key() key = '';
@Aspect() info: Info;
}
@Entity('door', { persistence: Persistence.Runtime })
export class Door {
@Aspect() info: Info;
@Aspect() motion: Motion;
@Machine() gate: Gate;
@Ref() needs: Ref<KeyDef>; // holds the id, never the key
@Event('locked', { clock: Clock.SimTime }) locked: Event; // reaches whoever sees the door
@EntityRpc({ requires: Entity.permissions.execute, rows: 'caller in entity.room' })
requestOpen(caller: Actor) {
if (caller.inventory.has(this.needs)) this.gate.fire('open_requested');
else this.locked.send();
}
}@aspect("info", read="any") # rarely changes, everyone reads it
class Info:
name: str = ""
@aspect("motion", hz=20, read="any", write="fn") # 20 updates a second while it swings
class Motion:
open_ratio: float = 0.0
jammed: bool = False
@machine("gate")
class Gate:
closed = state(initial=True, on="open_requested", to="opening")
opening = state(after_seconds=1.2, to="open")
open = state(on="close_requested", to="closed")
blocked = state("open.blocked", on="cleared", to="open", guard="!motion.jammed")
@entity("key-def", persistence=Persistence.PERSISTENT) # authored content: key.bronze, key.gold
class KeyDef:
key: str = key()
info: Info = aspect()
@entity("door", persistence=Persistence.RUNTIME)
class Door:
info: Info = aspect()
motion: Motion = aspect()
gate: Gate = machine()
needs: Ref[KeyDef] = ref() # holds the id, never the key
locked = event("locked", clock=Clock.SIM_TIME) # reaches whoever sees the door
@entity_rpc(requires=entity.permissions.execute, rows="caller in entity.room")
def request_open(self, caller: Actor):
if caller.inventory.has(self.needs):
self.gate.fire("open_requested")
else:
self.locked.send()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 FInfo { GENERATED_BODY() UPROPERTY() FString Name; };
USTRUCT()
struct FMotion { GENERATED_BODY() UPROPERTY() float OpenRatio; UPROPERTY() bool bJammed; };
USTRUCT(PSMachine = (Name = "gate"))
struct FGate
{
GENERATED_BODY()
UPROPERTY(PSState = (Name = "closed", Initial = "true"),
PSTransition = (On = "open_requested", To = "opening")) FPSState Closed;
UPROPERTY(PSState = "opening", PSAfterSeconds = (Seconds = "1.2", To = "open")) FPSState Opening;
UPROPERTY(PSState = "open", PSTransition = (On = "close_requested", To = "closed")) FPSState Open;
UPROPERTY(PSState = (Name = "open.blocked"),
PSTransition = (On = "cleared", To = "open", Guard = "!motion.jammed")) FPSState Blocked;
};
USTRUCT(PSEvent = (Name = "locked", Clock = "SimTime"))
struct FLocked { GENERATED_BODY() }; // reaches whoever sees the door
UCLASS(PSEntity = (Name = "key-def", Persistence = "Persistent"))
class UKeyDef : public UObject
{
GENERATED_BODY()
UPROPERTY(PSKey) FString Key;
UPROPERTY(PSAspect = (Name = "info", Read = "any")) FInfo Info;
};
UCLASS(PSEntity = (Name = "door", Persistence = "Runtime"))
class UDoor : public UObject
{
GENERATED_BODY()
UPROPERTY(PSAspect = (Name = "info", Read = "any")) FInfo Info;
UPROPERTY(PSAspect = (Name = "motion", Hz = 20, Read = "any", Write = "fn")) FMotion Motion;
UPROPERTY(PSMachine = "gate")
FGate Gate;
UPROPERTY(PSRef = "key-def") TPSRef<UKeyDef> Needs; // holds the id, never the key
UFUNCTION(PSRpc = (Requires = "Entity.Execute", Rows = "caller in entity.room"))
void RequestOpen();
};[Aspect("info", Read = "any")] // rarely changes, everyone reads it
public class Info { public string Name; }
[Aspect("motion", Hz = 20, Read = "any", Write = "fn")] // 20 updates a second while it swings
public class Motion { public float OpenRatio; public bool Jammed; }
[Machine("gate")]
public class Gate
{
[State(Initial = true), Transition("open_requested", to: "opening")] public State Closed;
[State, AfterSeconds(1.2f, to: "open")] public State Opening;
[State, Transition("close_requested", to: "closed")] public State Open;
[State("open.blocked"), Transition("cleared", to: "open", Guard = "!motion.jammed")] public State Blocked;
}
[Entity("key-def", Persistence = Persistence.Persistent)] // authored content: key.bronze, key.gold
public class KeyDef
{
[Key] public string Key;
[Aspect] public Info Info;
}
[Entity("door", Persistence = Persistence.Runtime)]
public class Door
{
[Aspect] public Info Info;
[Aspect] public Motion Motion;
[Machine] public Gate Gate;
[Ref] public Ref<KeyDef> Needs; // holds the id, never the key
[Event("locked", Clock = Clock.SimTime)] public Event Locked; // reaches whoever sees the door
[EntityRpc(Requires = Entity.Permissions.Execute, Rows = "caller in entity.room")]
public void RequestOpen(Actor caller)
{
if (caller.Inventory.Has(Needs)) Gate.Fire("open_requested");
else Locked.Send();
}
}L’unité déclarée, c’est l’aspect, pas le champ : motion a sa propre cadence et son propre masque, info en a d’autres, et chaque champ appartient à un seul d’entre eux. C’est ce qui permet à un preset d’ajouter tout un groupe d’un coup, et à Collision de se lier au seul aspect qui porte une transformation, sans rien voir d’autre de l’entité.
Trois règles régissent la machine de cet exemple :
- Un nom à point imbrique sur un niveau.
open.blockedse rattache de lui-même àopen: la transitionclose_requesteddéclarée suropens’applique donc à l’intérieur sans qu’on la répète. Tant que la machine est dansopen.blocked, elle est dansopen— un test d’état suropenrenvoie vrai, etOnEntered("open"), déclenché à l’entrée, ne se déclenche pas une seconde fois pour le sous-état. - Un minuteur déclaré sur un état ne tourne que tant que cet état est actif. Quitter
openingannule sonAfterSeconds, et y revenir en démarre un nouveau. - Une condition de transition (guard) est un prédicat déclaré sur les champs de l’entité elle-même, dans le même langage qu’un prédicat de lignes d’accès. Une logique qui demande du code relève d’un hook, pas d’une condition de transition.
Les chemins de champs suivent l’orthographe du modèle publié. Les prédicats et les chemins de requête nomment les champs tels que la déclaration les a publiés — motion.jammed, gate.state, info.name — quelle que soit leur graphie locale dans chaque binding.
Qui peut appeler la RPC. Une RPC d’entité nomme le droit dont elle a besoin comme toute opération : une permission atomique (entity × execute) plus un prédicat de lignes qui indique quelles instances elle couvre (les deux sont décrits sur Access & Roles).
| Dans la déclaration | Ce que cela signifie |
|---|---|
caller in entity.room | tout acteur présent dans la salle où se trouve la porte, quel que soit le build qu’il exécute. La proximité n’entre pas en jeu : la distance à laquelle il faut se tenir pour recevoir les deltas de la porte relève d’une règle de visibilité dans la politique de synchronisation de l’aspect — une question de bande passante, pas de permission, et élargir une vue n’élargit jamais un droit |
Actor | l’identité de l’appelant, le même objet que celui renvoyé par whoami |
caller.Inventory | le handle de Inventory pour ce joueur, disponible partout où ce module est monté |
C’est playserv push qui rend une déclaration effective. Cette étape est décrite dans Le schéma dans le code : elle compare vos déclarations au modèle déployé, transmet la révision qui a servi au diff, et refuse au lieu d’écraser si le schéma déployé a changé entre-temps. Une nouvelle publication qui casserait des instances déjà actives passe par proposer → planifier → appliquer : vous pouvez lire le plan avant que quoi que ce soit ne change.
Côté client, l’entité est l’API :
var playserv = await PlayServ.Connect(projectKey);
var room = await playserv.Rooms.Join(seat); // a seat from Matchmaking, or a room you found
var door = room.Entity<Door>(doorId); // a typed Ref — passable to any RPC as-is
await door.RequestOpen();
door.Gate.OnEntered("open", () => PlayChime());
door.Locked.On(() => Hud.Flash("Locked — the bronze key opens it"));const playserv = await PlayServ.connect(projectKey);
const room = await playserv.rooms.join(seat); // a seat from Matchmaking, or a room you found
const door = room.entity<Door>(doorId); // a typed Ref — passable to any RPC as-is
await door.requestOpen();
door.gate.onEntered('open', () => playChime());
door.locked.on(() => hud.flash('Locked — the bronze key opens it'));playserv = await PlayServ.connect(project_key)
room = await playserv.rooms.join(seat) # a seat from Matchmaking, or a room you found
door = room.entity(Door, door_id) # a typed Ref — passable to any RPC as-is
await door.request_open()
door.gate.on_entered("open", lambda: play_chime())
door.locked.on(lambda: hud.flash("Locked — the bronze key opens it"))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.
FPlayServClient::Connect(ProjectKey,
TPSOnResult<FPlayServClient*>::CreateWeakLambda(this, [this](const TPSResult<FPlayServClient*>& ConnectResult)
{
if (!ConnectResult.HasValue()) { return; }
// a seat from Matchmaking, or a room you found
ConnectResult.Value()->Rooms->Join(Seat,
TPSOnResult<FPSRoom*>::CreateWeakLambda(this, [this](const TPSResult<FPSRoom*>& JoinResult)
{
if (!JoinResult.HasValue()) { return; }
OnJoined(JoinResult.Value());
}));
}));
// in OnJoined(FPSRoom* Room): a typed handle — every declared member generated, passable to any RPC
Room->Entities->Of<UDoor>()->Get(DoorId,
TPSOnResult<UDoor*>::CreateWeakLambda(this, [this](const TPSResult<UDoor*>& DoorResult)
{
if (!DoorResult.HasValue()) { return; }
UDoor* Door = DoorResult.Value();
Door->Call->RequestOpen();
TPSSubscription OpenChime = Door->Gate->Subscribe->Entered(PSKeys::States::Open, [this]() { PlayChime(); });
TPSSubscription LockAlerts = Door->Subscribe->Locked([this]() { Hud->Flash(TEXT("Locked — the bronze key opens it")); });
}));
// co_await support ships later: a built-in SDK coroutine wrapper that respects Unreal's GC and threading.var playserv = await PlayServ.Connect(projectKey);
var room = await playserv.Rooms.Join(seat); // a seat from Matchmaking, or a room you found
var door = room.Entity<Door>(doorId); // a typed Ref — passable to any RPC as-is
await door.RequestOpen();
door.Gate.OnEntered("open", () => PlayChime());
door.Locked.On(() => Hud.Flash("Locked — the bronze key opens it"));Le signal locked est un événement propre à la porte : sa cible est celle de la déclaration — cette instance. Tous les abonnés à la porte le reçoivent donc, et aucune liste de destinataires n’accompagne l’envoi.
Le modèle
Un char, une porte, une barre de statistique et une quête sont tous des entités. Seuls les aspects qu’elles portent les distinguent, et c’est ce qui permet à tous les autres modules de s’appuyer sur celui-ci.
Ce que déclare une entité.
| Déclare | Ce que c’est |
|---|---|
aspect | un groupe de champs nommé, déclaré d’un bloc, avec sa propre politique de synchronisation et son propre masque d’accès. Une entité en porte plusieurs, et aucun champ n’appartient à deux aspects |
machine à états | des états imbricables sur un niveau, des transitions et des conditions de transition ; plusieurs par entité |
trigger | ce qui déclenche une transition — les quatre sources sont décrites plus bas |
RPC d'entité | un verbe sur l’entité, déclaré dans la vue avec la permission atomique dont il a besoin |
événement d'entité | un signal émis par l’entité, livré à quiconque s’abonne à cette instance |
hook | avant et après, sur les opérations de données et sur les transitions, déployés comme fonctions cloud. Extensibility déclare leur ordre, la forme du verdict et l’effet d’un échec |
suivi d'historique | indique si la vue conserve ou non la fenêtre d’historique instantané |
ref | un lien vers une autre entité qui contient son id et jamais sa key : renommer une clé ne casse donc jamais un lien. Include la charge avec la page |
Ce qui déclenche une transition. Aucune des quatre sources n’est du code qui tourne dans une salle.
| Source | Comment elle déclenche |
|---|---|
événement client ou RPC | n’importe lequel parmi ceux déclarés — RequestOpen ci-dessus déclenche open_requested |
collision | un contact ou l’entrée dans un volume de déclenchement — pièges, plaques de pression — via l’aspect auquel se lie Collision |
seuil de données | déclaré sur une statistique, 0 HP → death, et appliqué par l’ordre des hooks plutôt que par du code dans une salle |
temps | AfterSeconds sur un état est un déclencheur déclaré, pas une coroutine : il suit l’horloge de simulation de la salle, avance avec sim_time et s’arrête quand la salle ne simule pas. Supprimer l’instance met fin à ses machines et à leurs minuteurs en attente |
Ce qu’une sélection peut faire.
| Axe | Ce qui est admis |
|---|---|
filter et sort | uniquement les champs déclarés — il n’existe pas de handle de table, et une sélection s’adresse aux entités, dans le périmètre d’une salle ou du projet |
include | une ref déclarée, chargée avec la page |
paging | par curseur opaque : ni un décalage, ni un identifiant de ligne, et sa signification ne survit pas à un changement de version. Renvoyez-le tel quel, ne l’analysez jamais |
access | les prédicats s’appliquent avant la pagination : une page n’a donc jamais de trous à la place des lignes masquées |
live | s’abonner à une sélection la maintient à jour : des membres y entrent et en sortent à mesure que leurs données changent |
// in this room: doors still shut, by name, first page of 20 — with the key each one needs
var shut = await room.Entities<Door>()
.Where(d => d.Gate.State == "closed")
.Include(d => d.Needs)
.OrderBy(d => d.Info.Name)
.Page(20)
.Query();
// live selection: fires as doors swing open and shut
room.Entities<Door>().Where(d => d.Gate.State == "open").Subscribe(open => Minimap.Mark(open));
// project-wide, outside any room: the key catalogue, page by page
var keys = await playserv.Entities<KeyDef>().OrderBy(k => k.Info.Name).Page(50).Query();
var more = await playserv.Entities<KeyDef>().OrderBy(k => k.Info.Name).Page(50, after: keys.Cursor).Query();// in this room: doors still shut, by name, first page of 20 — with the key each one needs
const shut = await room.entities<Door>()
.where((d) => d.gate.state === 'closed')
.include((d) => d.needs)
.orderBy((d) => d.info.name)
.page(20)
.query();
// live selection: fires as doors swing open and shut
room.entities<Door>().where((d) => d.gate.state === 'open').subscribe((open) => minimap.mark(open));
// project-wide, outside any room: the key catalogue, page by page
const keys = await playserv.entities<KeyDef>().orderBy((k) => k.info.name).page(50).query();
const more = await playserv.entities<KeyDef>().orderBy((k) => k.info.name)
.page(50, { after: keys.cursor }).query();# in this room: doors still shut, by name, first page of 20 — with the key each one needs
shut = await (room.entities(Door)
.where("gate.state", "closed")
.include("needs")
.order_by("info.name")
.page(20)
.query())
# live selection: fires as doors swing open and shut
room.entities(Door).where("gate.state", "open").subscribe(lambda open: minimap.mark(open))
# project-wide, outside any room: the key catalogue, page by page
keys = await playserv.entities(KeyDef).order_by("info.name").page(50).query()
more = await playserv.entities(KeyDef).order_by("info.name").page(50, after=keys.cursor).query()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.
// in this room: doors still shut, by name, first page of 20 — with the key each one needs
Room->Entities->Of<UDoor>()->Select()
.Where(PSFields::Door::Gate::State == PSKeys::States::Closed)
.Include(PSFields::Door::Needs)
.OrderBy(PSFields::Door::Info::Name)
.Page(20)
.Then(TPSOnResult<TPSPage<UDoor>>::CreateWeakLambda(this, [this](const TPSResult<TPSPage<UDoor>>& Result)
{
if (!Result.HasValue()) { return; }
const TPSPage<UDoor>& ShutDoors = Result.Value();
Minimap->MarkShut(ShutDoors.Rows);
// the next page rides the cursor this one returned
Room->Entities->Of<UDoor>()->Select()
.Where(PSFields::Door::Gate::State == PSKeys::States::Closed)
.Page(20, ShutDoors.Cursor)
.Then(OnMoreShutDoors);
}));
// live selection: fires as doors swing open and shut
TPSSubscription OpenDoors = Room->Entities->Of<UDoor>()->Select()
.Where(PSFields::Door::Gate::State == PSKeys::States::Open)
.Subscribe([this](const TArray<UDoor*>& Open) { Minimap->Mark(Open); });
// project-wide, outside any room: the key catalogue
Client->Entities->Of<UKeyDef>()->Select()
.OrderBy(PSFields::KeyDef::Info::Name)
.Page(50)
.Then(TPSOnResult<TPSPage<UKeyDef>>::CreateWeakLambda(this, [this](const TPSResult<TPSPage<UKeyDef>>& KeyPage)
{
if (!KeyPage.HasValue()) { return; }
Catalogue->Show(KeyPage.Value().Rows);
}));
// co_await support ships later: a built-in SDK coroutine wrapper that respects Unreal's GC and threading.// in this room: doors still shut, by name, first page of 20 — with the key each one needs
var shut = await room.Entities<Door>()
.Where(d => d.Gate.State == "closed")
.Include(d => d.Needs)
.OrderBy(d => d.Info.Name)
.Page(20)
.Query();
// live selection: fires as doors swing open and shut
room.Entities<Door>().Where(d => d.Gate.State == "open").Subscribe(open => Minimap.Mark(open));
// project-wide, outside any room: the key catalogue, page by page
var keys = await playserv.Entities<KeyDef>().OrderBy(k => k.Info.Name).Page(50).Query();
var more = await playserv.Entities<KeyDef>().OrderBy(k => k.Info.Name).Page(50, after: keys.Cursor).Query();Ce qui vaut pour toute entité.
| Toujours | Ce que c’est |
|---|---|
une sélection | est un ensemble d’entités, pas un groupe : les membres d’un groupe sont des acteurs, réunis pour qu’un même signal les atteigne tous, alors qu’une sélection est une lecture qui se trouve rester à jour |
l'historique | se limite à la fenêtre instantanée : les états récents indexés par sim_time, bornés par une profondeur déclarée. Un historique à branches — chronologies alternatives, annulation, rejeu d’un match entier — sort du périmètre : il faudrait garantir des valeurs flottantes reproductibles, ce que les règles de typage ne garantissent pas |
une méthode déclarée sans implémentation | est un état abouti, pas une configuration à moitié faite. L’appeler renvoie un verdict portant la raison lisible par machine "no implementation" : ni un refus, ni un succès au résultat vide |
un nom jamais déclaré | donne une autre issue qu’un nom déclaré sans implémentation : le premier est un refus de validation, le second un verdict, et le code sait les distinguer |
un appel non implémenté | ne disparaît pas — le studio peut observer que quelqu’un l’a invoqué. La forme de cette observation ne fait volontairement pas partie du contrat : appuyez-vous sur le fait qu’elle est observable, pas sur une ligne de log |
créer une instance | exige la permission atomique entity × write : une fonction cloud, un serveur dédié et un client maître la détiennent par défaut, un simple client seulement si un rôle la lui accorde — dans tous les bindings, pas seulement dans Unreal |
Une modification n’est atomique qu’au sein d’une seule entité
Quand un même appelant modifie deux entités, par exemple en débitant un portefeuille et en ajoutant l’objet, on peut observer l’opération à moitié appliquée. Deux changements qui doivent apparaître ensemble appartiennent à une seule instance. Un hook ne peut pas y suppléer : il encadre une modification, pas deux.
Presets. Un preset est un ensemble nommé d’aspects, de machines, de hooks et de limites appliqué à une vue. Il n’ajoute aucun concept : tout ce qu’il apporte, vous pourriez le déclarer à la main. Cinq sont fournis — stats, abilities, projectiles, drops, world-objects — et Entity Presets les décrit chacun en détail. Un studio en dérive les siens, dans le code ou dans le panneau — Crate, c’est world-objects plus stats :
Crate de deux presets fournis, puis créer chaque caisse en une ligne et la régler à 250 PV// derived once, in the schema
[Entity("crate", Persistence = Persistence.Runtime, Presets = new[] { Preset.WorldObjects, Preset.Stats })]
public class Crate { [Stat(Max = 100, AtMin = "broken")] public Stat Hp; }
// then one line per crate, on the room host
var crate = await room.Create<Crate>(at: pos, tune: c => c.Hp.Max = 250);// derived once, in the schema
@Entity('crate', { persistence: Persistence.Runtime, presets: [Preset.WorldObjects, Preset.Stats] })
export class Crate { @Stat({ max: 100, atMin: 'broken' }) hp: Stat; }
// then one line per crate, on the room host
const crate = await room.create<Crate>({ at: pos, tune: (c) => { c.hp.max = 250; } });# derived once, in the schema
@entity("crate", persistence=Persistence.RUNTIME, presets=[Preset.WORLD_OBJECTS, Preset.STATS])
class Crate:
hp = stat(max=100, at_min="broken")
# then one line per crate, on the room host
crate = await room.create(Crate, at=pos, tune={"hp.max": 250})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
UCLASS(PSEntity = (Name = "crate", Persistence = "Runtime", Presets = "world-objects, stats"))
class UCrate : public UObject
{
GENERATED_BODY()
UPROPERTY(PSStat = (Max = 100, AtMin = "broken")) FPSStat Hp;
};
// then one line per crate, on the room host
Room->Entities->Of<UCrate>()->Create(FPSIdempotencyKey(CrateId),
[SpawnPosition](UCrate& Crate) { Crate.Position = SpawnPosition; }); // Position — from the world-objects preset
// co_await support ships later: a built-in SDK coroutine wrapper that respects Unreal's GC and threading.// derived once, in the schema
[Entity("crate", Persistence = Persistence.Runtime, Presets = new[] { Preset.WorldObjects, Preset.Stats })]
public class Crate { [Stat(Max = 100, AtMin = "broken")] public Stat Hp; }
// then one line per crate, on the room host
var crate = await room.Create<Crate>(at: pos, tune: c => c.Hp.Max = 250);Erreurs
- Une instance inexistante et une instance masquée par un prédicat répondent toutes deux
not found— un refus ne révèle donc jamais à l’appelant qu’une chose existe sans lui appartenir. - Un champ non déclaré, champs imbriqués compris, et un champ obligatoire sans valeur donnent des refus de validation qui nomment le champ.
- Une transition que la machine ne déclare pas est refusée avec
invalid_state_transition, jamais ignorée en silence. Forcer une machine au-delà de ses conditions de transition est une autre opération, avec une autre permission atomique —administer, qu’aucune clé client ne détient par défaut. - Un écart de version est un échec de précondition : relisez, puis décidez à nouveau.
- Une clé déjà prise est un conflit.
- Une écriture au nom d’un joueur qui ne nomme pas ce joueur est un refus de validation, plutôt qu’une écriture attribuée à personne.
- Sans permission, la réponse est forbidden, en distinguant lecture et écriture.
Limites
Les valeurs se fixent par projet ; ce qui se passe à chaque limite est déjà arrêté.
| Limite | À la limite |
|---|---|
| aspects par vue · machines par vue · profondeur d’imbrication dans un aspect | la déclaration est rejetée au playserv push, jamais tronquée en silence |
| taille de l’instance stockée | l’écriture est refusée comme conflit, avec le champ et la taille mesurée ; l’approche de la limite est observable avant le refus |
| taille de page d’une sélection | la page est coupée au plafond et « il y a une suite » reste vrai — vous ne recevez jamais une page courte qui aurait l’air d’être la dernière |
| fenêtre d’historique instantané | une lecture hors de la fenêtre est refusée, et non servie avec la valeur la plus proche |
| fréquence des modifications d’une instance | un refus pour limite de débit, qui indique combien de temps attendre |