Table of Contents

Projections & Codecs

What of an archetype travels, and how it is quantized — network policy, declared once, never part of the storage schema.

Status: ✅ Implemented · Visibility: Public · Level: 🔵 Core · Category: Subscriptions

🎯 What it solves

A component holds far more than a client needs, at far more precision: a float health, a double coordinate, AI scratch written every tick. A projection names only what travels, quantizes it to what the client can use, and splits it so an update carries only what changed.

⚙️ How it works (in brief)

  • Position. Motion(comp, …) for movers: sent as motion segments — a start point, a velocity, a start tick — that the client extrapolates. A new segment is sent when the extrapolation would drift past Tolerance (5 cm by default), on a teleport (a jump faster than Teleport(maxSpeedMps), which is required and also sizes the velocity codec), or as a MaxAge heartbeat (5 s). Position(comp) for things that never move. The component must hold the archetype's [SpatialIndex] field; positions are quantized to 24 bits per axis over the spatial world's bounds.
  • Fields travel under a Codec and in a change group (the default one when unnamed): an update record carries only the groups that changed.
  • OnEnter fields are sent once, when an entity enters a session's view.
  • Fraction(comp, value, max, bits) sends a value as a fraction of another field — an 8-bit health bar.
  • Heading(comp, angle, bits, toleranceDeg) sends an angle only when it turned past the tolerance.
  • Owner(o => o.Field(…)) fields go only to the controlling session (Owner state).
  • Static<T>(…) declares an archetype whose state never changes: sent once on enter, no groups, no per-tick comparison.

Codecs (Codec.X): Bool, U8/I8/U16/I16/U32/I32, VarUInt/VarInt, F32, F16, Quant(min, max, bits), Unorm(bits), Snorm(bits), Angle(bits), Bits(n), Enum<T>(bits), Vec2/Vec3(scale, bits), EntityRef, Str/Blob/Bytes, List(of, min, max), TickLo, Quat3. .Saturate() clamps out-of-range values instead of refusing them. Quantizers take 8, 16, 24 or 32 bits. The same arithmetic runs bit-exactly in the engine and both SDKs.

💻 Usage

subs.Archetype<Creature>(a => a
    .Motion(Creature.Bounds, m => m.Tolerance(0.05).Teleport(maxSpeedMps: 12))
    .OnEnter(Creature.Ai, x => x.AggroRadius, Codec.F16, name: "aggro")
    .Field(Creature.Ai, x => x.Mode, Codec.U8, name: "mode")
    .Fraction(Creature.Vitals, v => v.Health, v => v.MaxHealth,
              bits: 8, name: "hp", group: "vitals"));

subs.Static<WorldObject>(a => a
    .Position(WorldObject.Bounds)
    .OnEnter(WorldObject.Struct, s => s.Kind, Codec.U8, name: "kind"));

The same declarations can be written as attributes on the data (Replication by attributes).

⚠️ Guarantees & limits

  • No 64-bit integer reaches the wire implicitly: a long needs an explicit codec, and a narrowing one must .Saturate() (clamps are counted). Refused at declaration.
  • At most 8 change groups per section — the public fields, and the owner fields — because the group mask is a byte; up to 255 replicated archetypes.
  • Components are read through the cluster layout (SUB-01); a projection never reads storage any other way.
  • Replication state per entity of an observed archetype: 64 B hot + 32 B cold for a 2D mover with a short state body, in native pools bounded by StatePoolBudgetBytes.
  • The projection is not part of the component's schema identity: changing a codec is never a migration.

🧪 Tests