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ê
| Ator | O que faz nesta página |
|---|---|
schema-author | declara aspectos, a política de sincronização deles e o predicado de visibilidade |
qualquer ator | assina um alvo; retoma a partir de uma posição; pede o estado completo |
backend-service | hooks antes e depois da mudança |
operator | lê o custo do pacote por ator; vê quando a entrega se degrada ou um pacote é cortado |
Resumo
tank: movimento a 30 envios por segundo e equipamento só para o donopublic class Motion
{
public Vector3 Position;
[Sync(Hz = 4)] public float Fuel; // one field overrides the aspect
}
public class Loadout { public int Ammo; }
[Entity("tank")]
public class Tank
{
[Aspect("motion", Priority = 10, Hz = 30)] // policy lives on the aspect
public Motion Motion = new();
[Aspect("loadout", Visible = "owner == caller.player")]
public Loadout Loadout = new();
public float InternalHeat; // in no aspect — never leaves the server
}
tank.Motion.Position = next; // ← the change; the delta is its consequenceexport class Motion {
position!: Vector3;
@Sync({ hz: 4 }) fuel = 0; // one field overrides the aspect
}
export class Loadout { ammo = 0; }
@Entity('tank')
export class Tank {
@Aspect('motion', { priority: 10, hz: 30 }) // policy lives on the aspect
motion = new Motion();
@Aspect('loadout', { visible: 'owner == caller.player' })
loadout = new Loadout();
internalHeat = 0; // in no aspect — never leaves the server
}
tank.motion.position = next; // ← the change; the delta is its consequenceclass Motion:
position: Vector3
fuel: float = sync(hz=4) # one field overrides the aspect
class Loadout:
ammo: int = 0
@entity("tank")
class Tank:
motion: Motion = aspect("motion", priority=10, hz=30) # policy lives on the aspect
loadout: Loadout = aspect("loadout", visible="owner == caller.player")
internal_heat: float = 0.0 # in no aspect — never leaves the server
tank.motion.position = next_pos # ← the change; the delta is its consequenceAinda 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 consequencepublic class Motion
{
public Vector3 Position;
[Sync(Hz = 4)] public float Fuel; // one field overrides the aspect
}
public class Loadout { public int Ammo; }
[Entity("tank")]
public class Tank
{
[Aspect("motion", Priority = 10, Hz = 30)] // policy lives on the aspect
public Motion Motion = new();
[Aspect("loadout", Visible = "owner == caller.player")]
public Loadout Loadout = new();
public float InternalHeat; // in no aspect — never leaves the server
}
tank.Motion.Position = next; // ← the change; the delta is its consequenceO modelo
O que um delta carrega.
| Campo | O que é |
|---|---|
campos alterados | só eles, nunca o objeto inteiro |
pair | o par instância × aspecto a que ele pertence |
number | um 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.
| Declara | Valores, e o que não é |
|---|---|
priority | ordena 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ção | um limite superior de envio. Não é uma promessa de recebimento nessa taxa — o recebimento depende do canal |
só deltas | não enviar o que não mudou |
modo de entrega | shared 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 visibilidade | o predicado que decide quem recebe; Visibility trata disso em detalhes |
O que uma assinatura guarda.
| Guarda | O que é |
|---|---|
target | uma 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 |
position | de 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 |
state | active → gap detected → resynchronised | closed, e closed é terminal |
O que vale para todo fluxo.
| Sempre | O que é |
|---|---|
fusão | os 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 lacunas | perder um delta em silêncio é proibido; o número de sequência no par é o que quem consome conta |
ordem | vale dentro de um par instância × aspecto; entre pares, não é garantida de forma nenhuma |
navegação | só 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 pacotes | se 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.
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)Ainda não está decidido como declarar com atributos em Go (O-1). Os exemplos chegam quando a forma de escrita for escolhida.
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.
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.
| Hook | O que pode fazer |
|---|---|
| antes de uma mudança | alterá-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ça | adicionar 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
closedda 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.