Saltar al contenido principal
Bloques de construcción

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

Data

Cambias un campo y la sincronización viene sola. Data es la mecánica debajo de cada campo sincronizado: deltas respecto del último estado confirmado, el aspecto como unidad de política, prioridad y frecuencia de envío, suscripciones que se pueden reanudar, la ventana de retención y hooks antes y después de cada cambio.

Te diriges a entidades, no a tablas: la interfaz para leer y cambiar (buscar, filtrar, ordenar, paginar, suscribirse a una selección) está en Entity; esta página describe la mecánica que hay debajo. Quien consume no tiene ningún camino hacia una tabla, y no hay una segunda forma de escribir: un cambio es una operación de Entity, y el delta es lo que resulta de ella.

Cuándo usarlo

  • Necesitas replicar el estado a los clientes sin escribir código de snapshots: cambiar un campo es toda la sincronización.
  • Los campos difieren en urgencia o en audiencia: prioridad y un tope de frecuencia de envío por aspecto, y un predicado de visibilidad para la niebla de guerra.
  • Un cliente que se reconecta no debe desincronizarse en silencio: el hueco se detecta y se nombra, y a un hueco más largo que la ventana de retención (retained window) se responde con el estado completo.
  • Necesitas el pasado reciente: la ventana de retención de deltas, indexada por sim_time, es lo que leen la predicción y la compensación de lag (lag compensation).
  • Una regla de validación debe vivir en un solo lugar: un hook previo al cambio acota el valor o veta el cambio antes de que se aplique.
  • No toques los ajustes si solo necesitas leer o consultar. La interfaz de Entity se apoya en esta mecánica sin modificarla.

Quién hace qué

ActorQué hace en esta página
schema-authordeclara los aspectos, su política de sincronización y el predicado de visibilidad
cualquier actorse suscribe a un destino; reanuda desde una posición; pide el estado completo
backend-servicehooks antes y después de un cambio
operatorconsulta el costo de los paquetes por actor; ve cuándo se degrada la entrega o se corta un paquete

En resumen

Dos aspectos en tank: el movimiento con 30 envíos por segundo, el equipamiento solo para su dueñ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
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
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 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 — los mismos atributos de C# compilan en Unity (base 2021.3): las declaraciones se suben al mismo modelo, y cambiar un campo es la misma sincronización completa
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

El modelo

Qué lleva un delta.

CampoQué es
campos modificadossolo esos, nunca el objeto entero
pairel par instancia × aspecto al que pertenece
numberun número de secuencia dentro de ese par, que es lo que permite detectar un hueco

Qué declara un aspecto.

Todo se declara con atributos, en el aspecto o en un campo concreto, nunca con una llamada en runtime. El aspecto fija el valor por defecto y un campo puede sobrescribirlo; el aspecto sigue siendo la unidad de política, porque si no, no habría nada con qué armar los presets.

DeclaraValores, y lo que no es
priorityordena qué se envía primero cuando el canal no alcanza. No es una promesa de latencia: es relativa, y ordena el envío entre campos en lugar de garantizar un plazo de entrega
frecuencia máxima de actualizaciónun límite superior del envío. No promete recibir a esa frecuencia: la recepción depende del canal
solo deltasno enviar lo que no cambió
modo de entregashared packet: lo mismo para todos, barato en CPU. per-actor packet: cada receptor recibe el suyo, según su zona de visibilidad; caro en CPU y necesario con poblaciones grandes
regla de visibilidadel predicado que decide quién recibe algo en absoluto; Visibility lo explica a fondo
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 recibe la diferencia respecto del estado que confirmó; uno que quedó fuera de la ventana de retención recibe el estado completo (los números de revisión son ilustrativos)

Qué contiene una suscripción.

ContieneQué es
targetuna instancia, una selección o un aspecto, y recibe los deltas de ese destino. Un destino no es un flujo: un destino puede abarcar muchos pares, y el orden se garantiza dentro de un par, no en todo el destino
positiondesde dónde se reanuda: la presenta quien consume. Si el hueco es mayor que la ventana de retención, llega el estado completo en lugar de un flujo de deltas, así que una desconexión larga nunca deja a un cliente equivocado sin que lo sepa
stateactive → gap detected → resynchronised | closed, y closed es final

Lo que vale para cualquier flujo.

SiempreQué es
fusiónlos deltas la admiten (ver abajo). Eso es lo que distingue un delta de un evento, donde perder uno significa perder información para siempre
detección de huecosestá prohibido perder un delta en silencio; quien consume cuenta el número de secuencia del par
ordense mantiene dentro de un mismo par instancia × aspecto; entre pares no se garantiza de ninguna forma
recorridosolo sobre lo declarado: lo que puede ser un filtro, un orden o una inclusión es un campo declarado y una referencia declarada. La interfaz de selección de entidad es la proyección de ese modelo, y esta primitiva no le da a quien consume ningún recorrido propio: no existe un segundo lenguaje de consultas
historialse construye a partir de los deltas: la ventana instantánea de una entidad es una ventana de retención de deltas indexada por sim_time. Su profundidad es el límite de esta primitiva, y no promete reproducibilidad en campos de punto flotante
el presupuesto de paquetesse degrada según lo declarado: cuando se agota el presupuesto por actor, la plataforma vuelve al paquete compartido tal como se declaró, en lugar de empezar a perder destinatarios al azar

Los valores intermedios pueden saltarse

Los deltas se fusionan entre envíos: 100 → 90 → 80 puede llegar como 100 → 80, porque el estado final sigue siendo correcto. Una lista de valores entregados no es un historial de lo que pasó.

Qué puede hacer un hook, y cuándo.

Un hook previo al cambio en el aspecto motion: el combustible negativo se rechaza antes de que el cambio se aplique
[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)
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.

Se ejecuta fuera del motor

Un hook es una función en la nube: se ejecuta en la plataforma, no en el motor. Escríbelo en C#, TypeScript o Python; tu código de Unreal se suscribe a los eventos que produce. Están previstas funciones escritas desde el motor: grafos de Blueprint y C++ (traducido a un lenguaje de scripts que valida la plataforma). Ambos se analizan y se suben como cualquier declaración; la ejecución sigue en la plataforma.

Se ejecuta fuera del motor

Un hook es una función en la nube: se ejecuta en la plataforma, no en el motor. Escríbelo en C#, TypeScript o Python; tu código de Unity se suscribe a los eventos que produce.

HookQué puede hacer
antes de un cambiomodificarlo o vetarlo. Un cambio vetado no produce ningún delta: los suscriptores no ven nada, en lugar de un valor seguido de una corrección
después de un cambioagregar efectos secundarios; nunca puede hacer fallar el cambio

El borrado tiene su hook en Entity, donde vive la eliminación; esta primitiva engancha el cambio.

Errores

  • Un destino de suscripción no declarado es un rechazo de validación.
  • Sin permiso para suscribirse se responde forbidden o not found, según si la existencia del destino es en sí un secreto: el rechazo no debe convertirse en un oráculo.
  • Una posición de reanudación que no se puede interpretar es una solicitud incorrecta, nunca un reinicio silencioso desde ahora.
  • Una suscripción cerrada del lado de la plataforma es un conflicto, y es observable: el closed de la máquina de estados es final, y el cliente no tiene que deducir que llegó ahí.
  • Agotar el número de suscripciones es un conflicto: el permiso está, pero no queda capacidad.

Límites

Los números llegarán con el capítulo sobre los límites de la plataforma; lo que pasa en cada tope ya está definido.

  • La ventana de retención de deltas: reanudar desde un punto más antiguo que la ventana entrega el estado completo en lugar de un rechazo.
  • Suscripciones por actor: se rechaza la nueva y las existentes continúan.
  • Tamaño del delta: el delta se divide en lugar de truncarse, y la división es observable.
  • La frecuencia de envío: un límite superior, no una garantía.
  • El costo de un paquete por actor: al agotarse, degradación declarada al paquete compartido.