Pular para o conteúdo principal
O modelo do seu jogo

Esta página ainda não foi traduzida: mostrando a versão em inglês.

Entity

Uma entidade é uma declaração de esquema com aspectos vivos: dados 0..*, estados 0..*, RPC 0..*, eventos 0..*, hooks e histórico de mudanças. Os mapas vinculam obstáculos a entidades, as colisões se vinculam a um aspecto de transform, stats é, ele próprio, um preset, e objetos do mundo são um preset mais uma máquina de estados.

O módulo Entity é montado na raiz, então room.Entity<Door>(id) e playserv.Entities<KeyDef>() ficam direto na raiz, e não atrás de um namespace. Ele se apoia em três primitivas — eventos, RPC e dados — e em mais nada. Colisões, movimentação e predição ficam acima dele: cada uma se vincula a um aspecto, e não à entidade inteira. É por isso que um contato consegue disparar uma transição sem que o módulo Collision saiba nada sobre permissões.

Quando usar

  • Um objeto do mundo precisa de comportamento, e não só de campos: máquinas de estados, RPCs com permissões e eventos em uma única declaração.
  • Portas, armadilhas, coletas (pickup): as transições precisam disparar a partir de eventos do cliente, de contatos de colisão ou de limiares de stats, sem código na sala (room).
  • Você quer criar objetos do jogo com uma linha: aplique ou derive presets como world-objects.
  • Uma contestação exige o estado exato do mundo no momento do tiro: leia uma instância em um sim_time passado, dentro da janela declarada.
  • Dispensável quando a coisa não tem identidade. Um valor que só existe dentro de outra coisa, como o texto da placa de uma porta, é um campo em um aspecto, e não uma entidade própria. Tudo o que tem endereço é uma entidade: Data & Subscriptions é a mecânica por baixo, e nenhum caminho de tabela passa por fora dela.

Quem faz o quê

AtorO que faz nesta página
schema-authordeclara entidades, aspectos, máquinas de estados e presets
todo atorconsulta, assina (subscription), chama RPCs de entidade, lê o estado

Resumo

A porta da masmorra: dois aspectos com política própria, uma máquina com condições de transição, um evento declarado e um RPC que indica a permissão de que precisa
[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()
Em breve — Go

Ainda não está decidido como declarar com atributos em Go (O-1). Os exemplos chegam quando a forma de escrita for escolhida.

// 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 — os mesmos atributos C# compilam no Unity (base 2021.3, sem exigir C# mais novo) — as declarações são publicadas no mesmo 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();
    }
}

A unidade declarada é o aspecto, e não o campo: motion tem a própria cadência e a própria máscara, info tem outras, e cada campo pertence a exatamente um deles. É isso que permite a um preset anexar um grupo inteiro de uma vez e à colisão se vincular ao único aspecto que carrega um transform, sem enxergar mais nada da entidade.

Três regras governam a máquina desse bloco:

  • Um nome com ponto cria um nível de aninhamento. open.blocked se associa a open sozinho, então a transição close_requested declarada em open vale dentro dele sem precisar ser repetida. Enquanto a máquina está em open.blocked, ela também está em open — uma verificação de estado para open dá verdadeiro, e OnEntered("open") já disparou na entrada e não dispara de novo para o subestado.
  • Um timer declarado em um estado só corre enquanto esse estado é o atual. Sair de opening descarta o AfterSeconds dele, e entrar de novo começa uma contagem nova.
  • Uma condição de transição (guard) é um predicado declarado sobre os campos da própria entidade, na mesma linguagem de um predicado de linhas de acesso. Lógica que precisa de código é um hook, e não uma condição de transição.

Os caminhos de campo seguem a grafia do modelo publicado. Predicados e caminhos de consulta nomeiam os campos como a declaração os publicou — motion.jammed, gate.state, info.name —, seja qual for a grafia local de cada binding.

Quem pode chamar o RPC. Um RPC de entidade indica a permissão de que precisa como qualquer operação: uma permissão atômica (entity × execute) mais um predicado de linhas que diz quais instâncias ela cobre (as duas coisas pertencem a Access & Roles).

Na declaraçãoO que significa
caller in entity.roomqualquer ator na sala em que a porta está, seja qual for o build que ele roda. A proximidade não entra aqui: a distância a que você precisa estar para receber os deltas da porta é uma regra de visibilidade na política de sincronização do aspecto — questão de banda, e não de permissão, e ampliar uma visão nunca amplia uma permissão
Actora identidade de quem chama, o mesmo objeto que whoami retorna
caller.Inventoryo handle de Inventory para esse jogador, disponível onde quer que esse módulo esteja montado

É o playserv push que torna uma declaração real. Essa etapa fica a cargo de Esquema como código: o push compara as suas declarações com o modelo publicado, leva junto a revisão usada no diff e, se o esquema publicado tiver mudado, recusa em vez de sobrescrever. Uma nova publicação que quebraria instâncias já ativas passa por propor → planejar → aplicar, para que o plano possa ser lido antes de qualquer mudança.

No cliente, a entidade é a API:

A porta como API: conectar, entrar, chamar o RPC e tratar os dois resultados — o som de abertura e o aviso de porta trancada
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"))
Em breve — Go

Ainda não está decidido como declarar com atributos em Go (O-1). Os exemplos chegam quando a forma de escrita for escolhida.

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 — roda sem mudanças no Unity com os tipos gerados
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"));

O sinal locked é um evento da própria porta: o alvo dele é o da declaração — esta instância —, então todos que assinam a porta o recebem, e nenhuma lista de destinatários viaja com o envio.

O modelo

Um tanque, uma porta, uma barra de stat e uma missão são todos entidades. Eles diferem apenas nos aspectos que carregam, e é isso que permite a todos os outros módulos se apoiarem neste.

O que uma entidade declara.

DeclaraO que é
aspectum grupo de campos com nome, declarado por inteiro, com política de sincronização e máscara de acesso próprias. Uma entidade tem vários, e nenhum campo está em dois
máquina de estadosestados aninháveis em um nível, transições e condições de transição. Pode haver várias por entidade
gatilhoo que dispara uma transição — as quatro fontes estão logo abaixo
RPC de entidadeum verbo da entidade, declarado dentro da visão com a permissão atômica de que precisa
evento de entidadeum sinal que a entidade emite, entregue a quem assina aquela instância
hookpré e pós, em operações de dados e em transições, com deploy como cloud functions. Extensibility declara a ordem, o formato do veredito e o que uma falha provoca
rastreamento de históricose a visão mantém ou não a janela instantânea
refum link para outra entidade que guarda o id dela e nunca a key, então renomear uma chave nunca quebra um link. Include traz a entidade junto com a página

O que dispara uma transição. Nenhuma das quatro fontes é código rodando em uma sala.

FonteComo dispara
evento do cliente ou RPCqualquer um que esteja declarado — RequestOpen, acima, dispara open_requested
colisãoum contato ou a entrada em uma área de gatilho (trigger volume) — armadilhas, placas de pressão — por meio do aspecto ao qual Collision se vincula
limiar de dadosdeclarado em um stat, 0 HP → death, e garantido pela ordem dos hooks, e não por código em uma sala
tempoAfterSeconds em um estado é um gatilho declarado, e não uma corrotina: ele segue o relógio de simulação da sala, avança com sim_time e para enquanto a sala não está simulando. Excluir a instância encerra as máquinas dela e, junto, os timers pendentes

O que uma seleção pode fazer.

EixoO que é permitido
filter e sortapenas campos declarados — não existe handle de tabela, e uma seleção é endereçada por entidade, no escopo de uma sala ou do projeto
includeuma ref declarada, trazida junto com a página
pagingpor cursor opaco: não é um offset nem um id de linha, e o significado dele não sobrevive a uma mudança de versão. Devolva o cursor como veio, nunca o interprete
accessos predicados são aplicados antes da paginação, então uma página nunca tem buracos no lugar das linhas ocultas
liveassinar uma seleção a mantém ao vivo, com membros entrando e saindo conforme os dados deles mudam
Consultar, filtrar, ordenar, paginar por cursor — e assinar a própria seleção
// 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()
Em breve — Go

Ainda não está decidido como declarar com atributos em Go (O-1). Os exemplos chegam quando a forma de escrita for escolhida.

// 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 — roda sem mudanças no Unity com os tipos gerados
// 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();

O que vale para toda entidade.

SempreO que é
uma seleçãoé um conjunto de entidades, e não um grupo: os membros de um grupo são atores e existem para que um sinal chegue a todos eles, enquanto uma seleção é uma leitura que, por acaso, continua ao vivo
históricoé só a janela instantânea: estados recentes indexados por sim_time, limitados por uma profundidade declarada. Um histórico com ramificações — linhas do tempo alternativas, desfazer, replay de uma partida inteira — fica fora do escopo, porque exigiria garantir valores de ponto flutuante reproduzíveis, e as regras de tipo não garantem isso
um método declarado sem implementaçãoé um estado acabado, e não uma configuração pela metade. Chamá-lo retorna um veredito com o motivo legível por máquina “no implementation”: não é uma recusa, nem um sucesso com resultado vazio
um nome nunca declaradotem um desfecho diferente do de um nome declarado sem implementação: o primeiro é uma recusa de validação, o segundo, um veredito, e o código consegue distinguir os dois
uma chamada não implementadanão desaparece — o estúdio consegue observar que alguém a invocou. A forma dessa observação fica deliberadamente fora do contrato: conte com o fato de ela ser observável, e não com uma linha de log
criar uma instânciaexige a permissão atômica entity × write: uma cloud function, um servidor dedicado e um cliente mestre a têm por padrão, e um cliente comum só quando um papel (role) a concede — em todos os bindings, e não só no Unreal

Uma mudança só é atômica dentro de uma entidade

Quando quem chama altera duas entidades — por exemplo, debita a carteira e adiciona o item —, o resultado pode ser observado aplicado pela metade. Um par que precisa aparecer junto deve ficar em uma única instância. Um hook não resolve isso: ele roda em torno de uma mudança, e não entre duas.

Presets. Um preset é um pacote nomeado de aspectos, máquinas de estados, hooks e limites, aplicado a uma visão. Ele não acrescenta nenhum conceito novo: tudo o que um preset traz você poderia declarar à mão. Cinco vêm prontos — stats, abilities, projectiles, drops, world-objects —, e Entity Presets declara cada um por completo. Um estúdio deriva os próprios presets a partir deles, em código ou no painel — Crate é world-objects mais stats:

Derive Crate de dois presets prontos e depois crie cada caixa com uma linha, ajustada para 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})
Em breve — Go

Ainda não está decidido como declarar com atributos em Go (O-1). Os exemplos chegam quando a forma de escrita for escolhida.

// 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 — a mesma declaração C#. Criar instâncias cabe ao host da sala, e um cliente Unity vê a caixa aparecer
// 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);

Erros

  • Uma instância que não existe e uma que um predicado esconde respondem igualmente not found — assim, uma recusa nunca revela a quem chama que algo existe, mas não é dele.
  • Um campo não declarado, inclusive os aninhados, e um campo obrigatório sem valor são recusas de validação que indicam o campo.
  • Uma transição que a máquina não declara é recusada como invalid_state_transition, e nunca ignorada em silêncio. Forçar uma máquina a passar por cima das condições de transição é uma operação diferente, com outra permissão atômica — administer, que nenhuma chave de cliente tem por padrão.
  • Uma divergência de versão é uma falha de pré-condição: leia de novo e decida outra vez.
  • Uma chave já em uso é um conflito.
  • Uma escrita em nome de um jogador que não indica o jogador é uma recusa de validação, e não uma escrita atribuída a ninguém.
  • Sem permissão, a resposta é forbidden, com leitura e escrita diferenciadas.

Limites

Os números são definidos por projeto. O que acontece em cada fronteira já está fixado.

LimiteNa fronteira
aspectos por visão · máquinas de estados por visão · profundidade de aninhamento dentro de um aspectoa declaração é rejeitada no playserv push, e nunca truncada em silêncio
tamanho da instância armazenadaa escrita é recusada como conflito, indicando o campo e o tamanho medido. Dá para observar a aproximação do limite antes da recusa
tamanho da página de uma seleçãoa página é cortada no teto e “há mais” continua verdadeiro — você nunca recebe uma página curta que parece ser a última
janela do histórico instantâneouma leitura fora da janela é recusada, e não respondida com o valor mais próximo
taxa de mudanças em uma instânciauma recusa por limite de taxa, que informa quanto tempo esperar