Pular para o conteúdo principal
Blocos de construção

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

Data

Mude um campo, e a sincronização vem junto. Data é a mecânica por trás de todo campo sincronizado — deltas contra o último estado confirmado, o aspecto como unidade de política, prioridade e taxa de envio, assinaturas que podem ser retomadas, a janela de retenção e hooks antes e depois da mudança.

Você endereça entidades, não tabelas — a interface de leitura e mudança (buscar, filtrar, ordenar, paginar, assinar uma seleção) está em Entity; esta página é a mecânica por baixo. Não existe um caminho do consumidor até uma tabela nem uma segunda forma de escrever: uma mudança é uma operação do Entity, e o delta é o que decorre dela.

Quando usar

  • Você precisa de estado replicado para os clientes sem código de snapshot: mudar um campo já é toda a sincronização.
  • Os campos diferem em urgência ou público: prioridade e um teto de taxa de envio por aspecto, e um predicado de visibilidade para a névoa de guerra.
  • Um cliente que reconecta não pode divergir em silêncio: uma lacuna é detectada e identificada, e uma lacuna maior que a janela de retenção é respondida com o estado completo.
  • Você precisa do passado recente: a janela de retenção de deltas, indexada por sim_time, é o que a predição e a compensação de lag leem.
  • Uma regra de validação deve ficar num lugar só: um hook antes da mudança limita ou veta o valor antes que a mudança seja aplicada.
  • Não é preciso mexer nesses ajustes quando você só precisa ler ou consultar. A interface do Entity usa essa mecânica sem alterar nada nela.

Quem faz o quê

AtorO que faz nesta página
schema-authordeclara aspectos, a política de sincronização deles e o predicado de visibilidade
qualquer atorassina um alvo; retoma a partir de uma posição; pede o estado completo
backend-servicehooks antes e depois da mudança
operatorlê o custo do pacote por ator; vê quando a entrega se degrada ou um pacote é cortado

Resumo

Dois aspectos em tank: movimento a 30 envios por segundo e equipamento só para o dono
public class Motion
{
    public Vector3 Position;
    [Sync(Hz = 4)] public float Fuel;                    // one field overrides the aspect
}

public class Loadout { public int Ammo; }

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

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

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

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

export class Loadout { ammo = 0; }

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

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

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

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

class Loadout:
    ammo: int = 0

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

tank.motion.position = next_pos   # ← the change; the delta is its consequence
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 FMotion
{
    GENERATED_BODY()

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

USTRUCT()
struct FLoadout
{
    GENERATED_BODY()

    UPROPERTY() int32 Ammo;
};

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

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

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

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

Tank->Motion.Position = Next;   // ← the change; the delta is its consequence
Unity C# is the same C# API here — os mesmos atributos de C# compilam no Unity (base 2021.3) — as declarações são publicadas no mesmo modelo, e mudar um campo continua sendo toda a sincronização
public class Motion
{
    public Vector3 Position;
    [Sync(Hz = 4)] public float Fuel;                    // one field overrides the aspect
}

public class Loadout { public int Ammo; }

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

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

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

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

O modelo

O que um delta carrega.

CampoO que é
campos alteradossó eles, nunca o objeto inteiro
pairo par instância × aspecto a que ele pertence
numberum número de sequência dentro desse par, que é o que torna uma lacuna detectável

O que um aspecto declara.

Tudo por atributo, no aspecto ou num único campo, nunca por uma chamada em runtime. O aspecto define o padrão, e um campo pode sobrescrevê-lo; o aspecto continua sendo a unidade de política, porque senão não haveria de onde montar presets.

DeclaraValores, e o que não é
priorityordena o que é enviado primeiro quando o canal não dá conta. Não é uma promessa de latência: é relativa e ordena o envio entre campos, em vez de garantir um prazo de entrega
taxa máxima de atualizaçãoum limite superior de envio. Não é uma promessa de recebimento nessa taxa — o recebimento depende do canal
só deltasnão enviar o que não mudou
modo de entregashared packet: a mesma coisa para todos, barato em CPU. per-actor packet: cada receptor recebe o seu, conforme a zona de visibilidade dele; caro em CPU e necessário com populações grandes
regra de visibilidadeo predicado que decide quem recebe; Visibility trata disso em detalhes
a field changes — revision 15a receiver that acknowledged 12a receiver that acknowledged 14a receiver past the retained window delta 12 → 15delta 14 → 15full state, marked as such
Cada receptor recebe a diferença em relação ao estado que confirmou; quem ficou além da janela de retenção recebe o estado completo (os números de revisão são ilustrativos)

O que uma assinatura guarda.

GuardaO que é
targetuma instância, uma seleção ou um aspecto, e a assinatura (subscription) recebe os deltas desse alvo. Um alvo não é um fluxo (stream): um alvo pode cobrir muitos pares, e a ordem é garantida dentro de um par, não em todo o alvo
positionde onde ela retoma: quem consome a apresenta. Se a lacuna for maior que a janela de retenção, chega o estado completo em vez de uma sequência de deltas, então uma desconexão longa nunca deixa um cliente errado em silêncio
stateactive → gap detected → resynchronised | closed, e closed é terminal

O que vale para todo fluxo.

SempreO que é
fusãoos deltas admitem (veja abaixo). É isso que separa um delta de um evento, em que perder um significa perder informação para sempre
detecção de lacunasperder um delta em silêncio é proibido; o número de sequência no par é o que quem consome conta
ordemvale dentro de um par instância × aspecto; entre pares, não é garantida de forma nenhuma
navegaçãosó sobre o que foi declarado: o que pode ser filtro, ordenação ou inclusão é um campo declarado e uma referência declarada. A interface de seleção de entidade é a projeção desse modelo, e esta primitiva não oferece a quem consome nenhuma navegação própria — não existe uma segunda linguagem de consulta
históricoé construído a partir de deltas: a janela instantânea de uma entidade é uma janela de retenção de deltas indexada por sim_time. A profundidade dela é o limite desta primitiva, e ela não promete reprodutibilidade em campos de ponto flutuante
o orçamento de pacotesse degrada como foi declarado: quando o orçamento por ator acaba, a plataforma volta para o pacote compartilhado como foi declarado, em vez de começar a perder destinatários de forma arbitrária

Valores intermediários podem ser pulados

Os deltas se fundem entre um envio e outro: 100 → 90 → 80 pode chegar como 100 → 80, porque o estado final continua correto. Uma lista de valores entregues não é um histórico do que aconteceu.

O que um hook pode fazer, e quando.

Um hook antes da mudança no aspecto motion: combustível negativo é rejeitado antes de a mudança ser aplicada
[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)
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.

Executa fora da engine

Um hook é uma cloud function: ele roda na plataforma, não na engine. Escreva-o em C#, TypeScript ou Python — o seu código Unreal assina os eventos resultantes. Funções escritas na engine estão planejadas: grafos de Blueprint e C++ (traduzido para um formato de script validado pela plataforma). Os dois são analisados e publicados como qualquer declaração; a execução continua na plataforma.

Executa fora da engine

Um hook é uma cloud function: ele roda na plataforma, não na engine. Escreva-o em C#, TypeScript ou Python — o seu código Unity assina os eventos resultantes.

HookO que pode fazer
antes de uma mudançaalterá-la ou vetá-la. Uma mudança vetada não gera delta nenhum — quem assina não vê nada, em vez de ver um valor e depois uma correção
depois de uma mudançaadicionar efeitos colaterais, e nunca pode fazer a mudança falhar

A exclusão tem hook no Entity, onde ela fica; esta primitiva põe o hook na mudança.

Erros

  • Um alvo de assinatura não declarado é uma recusa de validação.
  • Sem permissão para assinar, a resposta é forbidden ou not found, conforme a própria existência do alvo seja ou não um segredo — a recusa não pode virar um oráculo.
  • Uma posição de retomada que não pode ser interpretada é uma requisição inválida, nunca um recomeço silencioso a partir de agora.
  • Uma assinatura encerrada pelo lado da plataforma é um conflito, e é observável: o closed da máquina é terminal, e chegar a ele não é algo que o cliente precise deduzir.
  • O número de assinaturas esgotado é um conflito: a permissão existe, mas não há mais capacidade.

Limites

Os números chegam com o capítulo de limites da plataforma; o que acontece em cada teto já está definido.

  • A janela de retenção de deltas: retomar de um ponto mais antigo que a janela dá o estado completo, e não uma recusa.
  • Assinaturas por ator: uma nova é recusada, e as existentes continuam.
  • Tamanho do delta: o delta é dividido em vez de truncado, e a divisão é observável.
  • A taxa de envio: um limite superior, não uma garantia.
  • O custo de um pacote por ator: quando se esgota, degradação declarada para o pacote compartilhado.