Saltar al contenido principal
El modelo de tu juego

Esta página aún no está traducida: se muestra la versión en inglés.

Entity

Una entidad es una declaración de esquema con aspectos vivos: datos 0..*, estados 0..*, RPC 0..*, eventos 0..*, hooks e historial de cambios. Los mapas vinculan los obstáculos a entidades, las colisiones se vinculan a un aspecto de transformación, las estadísticas son un preset y los objetos del mundo son un preset más una máquina de estados.

Entity se monta en la raíz, así que room.Entity<Door>(id) y playserv.Entities<KeyDef>() están directamente en la raíz y no detrás de un espacio de nombres. Se apoya en tres primitivas —eventos, RPC y datos— y en nada más. Los módulos Collision, Locomotion y Prediction & Lag Comp están por encima: cada uno se vincula a un solo aspecto, no a la entidad entera. Por eso un contacto puede disparar una transición sin que el módulo de colisiones sepa nada de permisos.

Cuándo usarlo

  • Un objeto del mundo necesita comportamiento, no solo campos: máquinas de estados, RPC con permisos y eventos en una sola declaración.
  • Puertas, trampas, objetos que se recogen: las transiciones deben dispararse con eventos del cliente, contactos de colisión o umbrales de estadísticas, sin código en la sala (room).
  • Quieres crear objetos del juego con una sola línea: aplica o deriva presets como world-objects.
  • Para resolver una disputa necesitas el estado exacto del mundo en el momento del disparo: lee una instancia en un sim_time pasado, dentro de la ventana declarada.
  • No lo uses si la cosa no tiene identidad propia. Un valor que solo vive dentro de otra cosa, como el texto de la placa de una puerta, es un campo de un aspecto, no una entidad aparte. Todo lo que sí se direcciona es una entidad: Data & Subscriptions es la mecánica que está debajo, y ninguna ruta por tablas se la salta.

Quién hace qué

ActorQué hace en esta página
schema-authordeclara entidades, aspectos, máquinas de estados y presets
cualquier actorconsulta, se suscribe, llama a los RPC de las entidades y lee su estado

En resumen

La puerta de la mazmorra: dos aspectos con su propia política, una máquina con condiciones de transición, un evento declarado y un RPC que indica el permiso que necesita
[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()
Próximamente — Go

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()
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();
};
Unity C# is the same C# API here — los mismos atributos de C# compilan en Unity (base 2021.3, no requiere una versión más nueva de C#): las declaraciones se suben al mismo modelo
[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();
    }
}

La unidad que se declara es el aspecto, no el campo: motion tiene su propia cadencia y su propia máscara, info tiene otras, y cada campo pertenece exactamente a uno de ellos. Gracias a eso un preset puede agregar un grupo entero de una vez, y el módulo Collision puede vincularse al único aspecto que lleva la transformación sin ver nada más de la entidad.

En ese bloque, tres reglas rigen la máquina de estados:

  • Un nombre con punto anida un nivel. open.blocked queda asociado con open por sí solo, así que la transición close_requested declarada en open también se aplica dentro de él sin repetirla. Mientras la máquina está en open.blocked, está en open: una comprobación del estado open da verdadero, y OnEntered("open") se disparó al entrar y no vuelve a dispararse por el subestado.
  • Un temporizador declarado en un estado solo corre mientras ese estado es el actual. Al salir de opening se descarta su AfterSeconds, y al volver a entrar arranca uno nuevo.
  • Una condición de transición (guard) es un predicado declarado sobre los campos de la propia entidad, en el mismo lenguaje que un predicado de filas de acceso. La lógica que necesita código es un hook, no una condición de transición.

Las rutas de los campos se escriben como en el modelo subido. Los predicados y las rutas de consulta nombran los campos tal como los subió la declaración —motion.jammed, gate.state, info.name—, sin importar cómo los escriba localmente cada binding.

Quién puede llamar al RPC. Un RPC de entidad indica el permiso que necesita igual que cualquier operación: un permiso atómico (entity × execute) más un predicado de filas que dice qué instancias cubre (ambos son de Access & Roles).

En la declaraciónQué significa
caller in entity.roomcualquier actor de la sala en la que está la puerta, sin importar qué build ejecute. La cercanía no forma parte de esto: qué tan cerca tienes que estar para recibir los deltas de la puerta lo decide una regla de visibilidad en la política de sincronización del aspecto. Es una cuestión de ancho de banda, no de permisos, y ampliar una vista nunca amplía un permiso
Actorla identidad de quien llama, el mismo objeto que devuelve whoami
caller.Inventoryel handle de Inventory para ese jugador, disponible dondequiera que ese módulo esté montado

playserv push es lo que vuelve real una declaración. Ese paso es de Esquema como código: compara tus declaraciones con el modelo desplegado, lleva la revisión contra la que se hizo la comparación y rechaza la subida en lugar de sobrescribir si el esquema desplegado cambió. Una nueva subida que rompería instancias ya activas pasa por propuesta → plan → aplicación, así que el plan se puede leer antes de que cambie nada.

En el cliente, la entidad es la API:

La puerta como API: conectarse, unirse, llamar al RPC y recibir los dos resultados —el sonido de apertura y la señal de puerta cerrada con llave—
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"))
Próximamente — Go

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.

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.
Unity C# is the same C# API here — funciona tal cual en Unity con los tipos generados
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"));

La señal locked es el propio evento de la puerta: su destino es el de la declaración —esta instancia—, así que la escuchan todos los que están suscritos a la puerta y el envío no lleva ninguna lista de destinatarios.

El modelo

Un tanque, una puerta, una barra de estadística y una misión son todos entidades. Solo se diferencian en los aspectos que llevan, y eso es lo que permite que todos los demás módulos se construyan sobre este.

Qué declara una entidad.

DeclaraQué es
aspectun grupo de campos con nombre que se declara completo, con su propia política de sincronización y su máscara de acceso. Una entidad lleva varios, y ningún campo está en dos
máquina de estadosestados que se anidan un nivel, transiciones y condiciones de transición; puede haber varias por entidad
triggerlo que dispara una transición; las cuatro fuentes están más abajo
RPC de entidadun verbo sobre la entidad, declarado dentro de la vista (view) junto con el permiso atómico que necesita
evento de entidaduna señal que emite la entidad y que reciben quienes se suscriben a esa instancia
hookantes y después, en operaciones de datos y en transiciones, desplegado como funciones en la nube. Extensibility declara el orden, la forma del veredicto y qué pasa si falla
registro de historialsi la vista conserva o no la ventana de historial inmediato
refun vínculo a otra entidad que guarda su id y nunca su key, así que renombrar una clave nunca rompe un vínculo. Include la trae junto con la página

Qué dispara una transición. Ninguna de las cuatro fuentes es código que se ejecuta en una sala.

FuenteCómo la dispara
evento o RPC del clientecualquiera que esté declarado: RequestOpen, más arriba, dispara open_requested
colisiónun contacto o la entrada en un volumen de activación —trampas, placas de presión— a través del aspecto al que se vincula Collision
umbral de datosdeclarado en una estadística, 0 HP → death; lo garantiza el orden de los hooks, no un código en la sala
tiempoAfterSeconds en un estado es un trigger declarado, no una corrutina: corre con el reloj de simulación de la sala, avanza con sim_time, se detiene mientras la sala no está simulando, y al borrar la instancia terminan con ella sus máquinas y sus temporizadores pendientes

Qué puede hacer una selección.

EjeQué se admite
filter y sortsolo campos declarados: no hay handle de tabla, y una selección se direcciona por entidades, dentro de una sala o de todo el proyecto
includeun ref declarado, que se trae junto con la página
pagingpor cursor opaco: no es un offset ni un id de fila, y su significado no sobrevive a un cambio de versión. Devuélvelo tal cual; nunca lo analices
accesslos predicados se aplican antes de paginar, así que una página nunca tiene huecos donde estarían las filas ocultas
livesuscribirse a una selección la mantiene viva: los miembros entran y salen a medida que cambian sus datos
Consultar, filtrar, ordenar, paginar por cursor y suscribirse a la propia selección
// 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()
Próximamente — Go

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.

// 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.
Unity C# is the same C# API here — funciona tal cual en Unity con los tipos generados
// 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();

Qué se cumple en toda entidad.

SiempreQué es
una selecciónes un conjunto de entidades, no un grupo: los miembros de un grupo son actores y existen para que una sola señal llegue a todos, mientras que una selección es una lectura que además se mantiene viva
el historiales solo la ventana inmediata: estados recientes indexados por sim_time, acotados por una profundidad declarada. El historial con ramas —líneas de tiempo alternativas, deshacer, repetir una partida entera— queda fuera del alcance, porque tendría que garantizar valores de punto flotante reproducibles y las reglas de tipos no lo hacen
un método declarado sin implementaciónes un estado terminado, no uno a medio configurar. Al llamarlo, responde con un veredicto que lleva el motivo legible por máquina “no implementation”: no es un rechazo ni un éxito con un resultado vacío
un nombre que nunca se declaróda un resultado distinto al de un nombre declarado sin implementación: el primero es un rechazo de validación y el segundo, un veredicto, y el código puede distinguirlos
una llamada sin implementarno desaparece: el estudio puede observar que alguien la invocó. La forma de esa observación queda fuera del contrato a propósito, así que apóyate en el hecho de que es observable, no en una línea de log
crear una instanciarequiere el permiso atómico entity × write: por defecto lo tienen una función en la nube, un servidor dedicado y un cliente maestro, y un cliente común solo cuando un rol se lo otorga; en todos los bindings, no solo en Unreal

Un cambio es atómico solo dentro de una entidad

Si quien llama cambia dos entidades —por ejemplo, descuenta de la billetera y agrega el artículo—, se pueden observar a medio aplicar. Un par que debe aparecer junto pertenece a una sola instancia. Un hook no puede reemplazar eso: se ejecuta alrededor de un cambio, no a lo largo de dos.

Presets. Un preset es un paquete con nombre de aspectos, máquinas, hooks y límites que se aplica a una vista. No agrega conceptos nuevos: todo lo que trae un preset podrías declararlo a mano. Se incluyen cinco —stats, abilities, projectiles, drops, world-objects— y Entity Presets declara cada uno por completo. Un estudio deriva los suyos a partir de ellos, en código o en el panel; Crate es world-objects más stats:

Deriva Crate de dos presets incluidos y luego crea cada caja con una línea, ajustada a 250 HP
// 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})
Próximamente — Go

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
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.
Unity C# is the same C# API here — la misma declaración de C#; crear es algo que hace el host de la sala, y un cliente de Unity ve llegar la caja
// 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);

Errores

  • Una instancia que no existe y una que un predicado oculta responden igual, con not found, así que un rechazo nunca le revela a quien llama que algo existe pero no es suyo.
  • Un campo no declarado, incluidos los anidados, y un campo obligatorio sin valor son rechazos de validación que nombran el campo.
  • Una transición que la máquina no declara se rechaza como invalid_state_transition; nunca se ignora en silencio. Forzar una máquina más allá de sus condiciones de transición es otra operación, con otro permiso atómico: administer, que ninguna clave de cliente tiene por defecto.
  • Una versión que no coincide es un fallo de precondición: vuelve a leer y decide de nuevo.
  • Una clave ya ocupada es un conflicto.
  • Una escritura en nombre de un jugador que no nombra al jugador es un rechazo de validación, no una escritura atribuida a nadie.
  • Sin permiso, la respuesta es forbidden, y se distingue entre lectura y escritura.

Límites

Las cifras se fijan por proyecto; lo que pasa en cada límite ya está definido.

LímiteAl llegar al límite
aspectos por vista · máquinas por vista · profundidad de anidamiento dentro de un aspectola declaración se rechaza en playserv push; nunca se trunca en silencio
tamaño de la instancia almacenadala escritura se rechaza como conflicto, con el nombre del campo y el tamaño medido; acercarse al límite es observable antes del rechazo
tamaño de página de una selecciónla página se corta en el tope y “hay más” sigue siendo verdadero: nunca recibes una página corta que parezca la última
ventana de historial inmediatouna lectura fuera de la ventana se rechaza; no se responde con el valor más cercano
frecuencia de cambios en una instanciaun rechazo por límite de frecuencia que indica cuánto esperar