Skip to main content
Building blocks

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

ActorOn this page
schema-authordeclares aspects, their sync policy and the visibility predicate
any actorsubscribes to a target; resumes from a position; requests full state
backend-servicebefore/after change hooks
operatorreads per-actor packet cost; sees when delivery degrades or a packet is cut

At a glance

Two aspects on tank: motion at 30 sends a second, loadout only for its owner
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
Coming soon — Go

Attribute-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 consequence
Unity C# is the same C# API here — the same C# attributes compile in Unity (2021.3 baseline) — declarations push into the same model, and changing a field is the same whole sync
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

The model

What a delta carries.

FieldWhat it is
changed fieldsonly those, never the whole object
pairthe instance × aspect it belongs to
numbera 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.

DeclaresValues, and what it is not
priorityorders 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 ratean upper bound on sending. Not a promise of receiving at that rate — receiving depends on the channel
delta onlydo not send what has not changed
delivery modeshared 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 rulethe predicate deciding who receives at all; Visibility covers it in full
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
Each receiver gets the difference against the state it acknowledged; one past the retained window gets the full state (revision numbers are illustrative)

What a subscription holds.

HoldsWhat it is
targetan 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
positionwhere 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
stateactive → gap detected → resynchronised | closed, and closed is terminal

What is true of every stream.

AlwaysWhat it is
mergingdeltas admit it (see below). This is what separates a delta from an event, where losing one loses information for good
gap detectionsilently losing a delta is forbidden; the sequence number in the pair is what the consumer counts
orderingholds within one instance × aspect pair; between pairs it is not promised in any form
traversalis 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
historyis 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 budgetdegrades 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.

A before-change hook on the 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)
Coming soon — Go

Attribute-style declaration in Go is still open (O-1) — the samples land when the carrier is decided.

Runs off the engine

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.

Runs off the engine

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.

HookWhat it may do
before a changemutate 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 changeadd 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 closed is 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.