Not translated yet — showing English.
Data
Change a field, and the sync follows. Data is the mechanics under every synced field — deltas against the last acknowledged state, the aspect as the unit of policy, priority and send rate, resumable subscriptions, the retained window, and before/after change hooks.
You address entities, not tables — see Entity for the read and change surface (find, filter, sort, page, subscribe to a selection); this page is the mechanics underneath. There is no consumer path to a table, and no second way to write: a change is an Entity operation, and the delta is what follows from it.
When to use it
- You need state replicated to clients without snapshot code: changing a field is the whole sync.
- Fields differ in urgency or audience: priority and a send-rate ceiling per aspect, and a visibility predicate for fog of war.
- A reconnecting client must not diverge silently: a gap is detected and named, and a gap past the retained window is answered with full state.
- You need the recent past: the retained window of deltas, indexed by
sim_time, is what prediction and lag compensation read. - A validation rule belongs in one place: a before-change hook clamps or vetoes before the change lands.
- Skip the knobs when all you need is to read or query. Entity's surface rides these mechanics without touching them.
Who does what
| Actor | On this page |
|---|---|
schema-author | declares aspects, their sync policy and the visibility predicate |
any actor | subscribes to a target; resumes from a position; requests full state |
backend-service | before/after change hooks |
operator | reads per-actor packet cost; sees when delivery degrades or a packet is cut |
At a glance
tank: motion at 30 sends a second, loadout only for its ownerpublic 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 consequenceAttribute-style declaration in Go is still open (O-1) — the samples land when the carrier is decided.
// 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 consequenceThe model
What a delta carries.
| Field | What it is |
|---|---|
changed fields | only those, never the whole object |
pair | the instance × aspect it belongs to |
number | a sequence number within that pair, which is what makes a gap detectable |
What an aspect declares.
All of it by attribute, on the aspect or on a single field, never by a call at runtime. The aspect sets the default and a field may override it; the aspect stays the unit of policy, because otherwise there is nothing to assemble presets from.
| Declares | Values, and what it is not |
|---|---|
priority | orders what is sent first when the channel is not enough. Not a promise of latency: it is relative, and it orders sending between fields rather than guaranteeing a delivery deadline |
max update rate | an upper bound on sending. Not a promise of receiving at that rate — receiving depends on the channel |
delta only | do not send what has not changed |
delivery mode | shared packet: the same thing to everyone, cheap on CPU. per-actor packet: each receiver gets its own, by its visibility zone; expensive on CPU and necessary at large populations |
visibility rule | the predicate deciding who receives at all; Visibility covers it in full |
What a subscription holds.
| Holds | What it is |
|---|---|
target | an instance, a selection, or an aspect, and it receives that target's deltas. A target is not a stream: one target may cover many pairs, and ordering is promised inside a pair rather than across a target |
position | where it resumes from: the consumer presents it. If the gap is larger than the retained window the full state arrives instead of a stream of deltas, so a long disconnect never leaves a client silently wrong |
state | active → gap detected → resynchronised | closed, and closed is terminal |
What is true of every stream.
| Always | What it is |
|---|---|
merging | deltas admit it (see below). This is what separates a delta from an event, where losing one loses information for good |
gap detection | silently losing a delta is forbidden; the sequence number in the pair is what the consumer counts |
ordering | holds within one instance × aspect pair; between pairs it is not promised in any form |
traversal | is over declared things only: what may be a filter, a sort or an inclusion is a declared field and a declared reference. The selection surface of entity is the projection of that model, and this primitive gives the consumer no traversal of its own — there is no second query language |
history | is built from deltas: an entity's instantaneous window is a retained window of deltas indexed by sim_time. Its depth is this primitive's limit, and it promises no reproducibility over floating-point fields |
the packet budget | degrades as declared: when the per-actor budget runs out the platform falls back to the shared packet as declared, rather than beginning to lose recipients arbitrarily |
Intermediate values can be skipped
Deltas merge between sends: 100 → 90 → 80 may arrive as 100 → 80, because the final state is still correct. A list of delivered values is not a history of what happened.
What a hook may do, and when.
motion aspect: negative fuel is rejected before the change lands[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)Attribute-style declaration in Go is still open (O-1) — the samples land when the carrier is decided.
A hook is a cloud function: it executes on the platform, not in the engine. Write it in C#, TypeScript or Python — your Unreal code subscribes to the resulting events. Engine-authored functions are planned: Blueprint graphs, and C++ (translated to a platform-validated script target). Both are analyzed and pushed like any declaration; execution stays on the platform.
A hook is a cloud function: it executes on the platform, not in the engine. Write it in C#, TypeScript or Python — your Unity code subscribes to the resulting events.
| Hook | What it may do |
|---|---|
| before a change | mutate it, or veto it. A vetoed change produces no delta at all — subscribers see nothing, rather than a value and then a correction |
| after a change | add side effects, and it can never fail the change |
Deleting is hooked on Entity, where the delete lives; this primitive hooks the change.
Errors
- An undeclared subscription target is a validation refusal.
- No permission to subscribe answers forbidden or not found depending on whether the target's existence is itself a secret — the refusal must not become an oracle.
- A resumption position that does not parse is a bad request, never a silent restart from now.
- A subscription closed by the platform's side is a conflict, and it is observable: the machine's
closedis terminal and reaching it is not something a client has to infer. - The subscription count exhausted is a conflict: the permission is held, but there is no capacity left.
Limits
The numbers arrive with the platform limits chapter; what happens at each ceiling is fixed now.
- The delta retention window: resuming from older than the window gives the full state rather than a refusal.
- Subscriptions per actor: a new one is refused and the existing ones continue.
- Delta size: the delta is split rather than truncated, and the split is observable.
- The send rate: an upper bound, not a guarantee.
- The cost of a per-actor packet: on exhaustion, declared degradation to the shared packet.