Spatial

Entity State

Mutable opaque per-entity state blob, broadcast on change and replayed on interest reveal.

Server
services.AddCrossplayEntityState();
Unity package
com.crossplay.entitystate
Depends on
world

Services you call 1

Crossplay implements these. Resolve them from DI and call them from your own systems.

IEntityStateService

Server-authoritative mutable per-entity game state — the mutable sibling of the spawn-time appearance blob. The GAME calls Set when something about an entity changes (health, nameplate, buffs, a door's open flag…); the piece broadcasts the new blob reliably to the entity's current observers and replays it to every observer the entity is later revealed to, so late joiners always see the truth. Clients only receive — there is no client request. The framework never interprets the bytes.

  • bool Set(long entityId, byte[] state)

    Sets (replaces) the entity's state and broadcasts it to current observers. Null/empty clears the state (observers receive an empty blob; nothing is replayed on future reveals). False when the entity is unknown or the blob exceeds MaxStateBytes.

  • bool TryGet(long entityId, out byte[] state)

    The entity's current state, when one is set.

Configuration 1

Every tunable lives in an options object — there are no magic numbers to hunt for.

EntityStateOptions

Configurable EntityState parameters (defaults here; never inlined in logic).

  • bool DeltaCompress { get; set; }

    When true, a state change is sent as an EntityStateDelta (changed middle only, against the observer's previous blob) whenever that is smaller than the full EntityStateChanged — the bandwidth win for large blobs that mutate in small parts (a buff toggles inside a 200-byte combat summary). Safe because state rides the reliable-ordered channel and the piece always sends FULL state on the first change and on every reveal, so a delta always has the exact base to apply to. Off by default for wire compatibility: enable only once every client decodes EntityStateDelta (a client that doesn't would drop it and miss the change until the next full reveal).

  • int DeltaOverheadBytes { get; set; }

    Estimated wire overhead (bytes) of the delta envelope beyond its changed-middle bytes (ids + prefix/suffix lengths + array framing). A change is sent as a delta only when middleBytes + this < fullBlobBytes, so a delta is emitted only when it genuinely wins; marginal changes fall back to full state. Conservative default 24; leave it unless profiling the delta path shows the envelope is consistently larger or smaller.

  • int MaxStateBytes { get; set; }

    Largest state blob (bytes) the piece accepts from the game. State rides the reliable channel to every observer and replays on every reveal — keep it a compact summary (flags, small numbers), not a data dump. Default 256; raise only if your per-entity state genuinely needs it, since every reveal replays the full blob.

Wire messages 2

The protocol this piece speaks. Ids are allocated per piece so they can never collide.

1801 EntityStateChanged

Server → observers: an entity's mutable, game-defined state. The blob is 100% the game's vocabulary — health/nameplate/buffs for one game, a door's open flag for another, a card's face-up state for a third; the framework never interprets it. Sent on every change to current observers AND replayed to each observer the moment the entity enters its view, so late joiners always see the truth (the mutable sibling of the spawn-time appearance blob).

1802 EntityStateDelta

Server → observers: an entity's state change expressed as a DELTA against the blob the observer already holds — the unchanged common prefix and suffix are elided, only the changed middle rides the wire. Sent instead of a full EntityStateChanged when it is smaller and delta compression is enabled; the piece sends full state on the first change and on every interest reveal, so a delta always has a base. EntityState rides the reliable-ordered channel, so a delta is applied to exactly the blob the sender diffed against — no divergence. The reconstruction is new = previous[0..PrefixLen] + Middle + previous[len-SuffixLen..], which expresses an in-place replace, an insert, or a delete in the middle (length may change).

Unity services you inject 1

Crossplay binds these in the client context. Inject and call them from your own MonoBehaviours and presenters.

IEntityStateClient

The EntityState piece, client side: receive each visible entity's mutable, game-defined state blob — sent on change and replayed automatically when an entity enters your view. The piece carries no presentation: subscribe to StateChanged (or read the cache) and map the bytes onto your own health bars / nameplates / door visuals. Remove this package and entity state simply stops — nothing else breaks.

  • event Action<long, byte[]> StateChanged

    A visible entity's state changed (or was first revealed). Empty blob = cleared.

  • bool TryGet(long entityId, out byte[] state)

    The entity's current state, when one is known.

UI views you can replace 1

Each ships a working uGUI panel you can drag into a scene. Substitute your own view to keep the logic and change the look — the presenter never knows.

ICrossplayEntityStateView

Narrow contract the presenter drives. UI-only, read-only: the EntityState wire is server-set (no client mutation path), so there are no user intents to raise.