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é
| Actor | Qué hace en esta página |
|---|---|
schema-author | declara los aspectos, su política de sincronización y el predicado de visibilidad |
cualquier actor | se suscribe a un destino; reanuda desde una posición; pide el estado completo |
backend-service | hooks antes y después de un cambio |
operator | consulta el costo de los paquetes por actor; ve cuándo se degrada la entrega o se corta un paquete |
En resumen
tank: el movimiento con 30 envíos por segundo, el equipamiento solo para su dueñopublic 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 consequenceTodaví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 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 consequenceEl modelo
Qué lleva un delta.
| Campo | Qué es |
|---|---|
campos modificados | solo esos, nunca el objeto entero |
pair | el par instancia × aspecto al que pertenece |
number | un 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.
| Declara | Valores, y lo que no es |
|---|---|
priority | ordena 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ón | un límite superior del envío. No promete recibir a esa frecuencia: la recepción depende del canal |
solo deltas | no enviar lo que no cambió |
modo de entrega | shared 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 visibilidad | el predicado que decide quién recibe algo en absoluto; Visibility lo explica a fondo |
Qué contiene una suscripción.
| Contiene | Qué es |
|---|---|
target | una 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 |
position | desde 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 |
state | active → gap detected → resynchronised | closed, y closed es final |
Lo que vale para cualquier flujo.
| Siempre | Qué es |
|---|---|
fusión | los 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 huecos | está prohibido perder un delta en silencio; quien consume cuenta el número de secuencia del par |
orden | se mantiene dentro de un mismo par instancia × aspecto; entre pares no se garantiza de ninguna forma |
recorrido | solo 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 |
historial | se 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 paquetes | se 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.
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)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.
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.
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.
| Hook | Qué puede hacer |
|---|---|
| antes de un cambio | modificarlo 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 cambio | agregar 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
closedde 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.