Skip to content

Entity schemas

When clients need typed, synchronized game state, define that state once in an entity schema. Each .yaml file controls the generated server entity, client model, wire tags, sync timing, persistence, and who may receive sensitive fields. Files live in the directory set by entity_schema in golem.yaml (default schemas/entities/).

entity: Player # PascalCase type name in your game
vars:
health: { tag: 1, type: double, sync: tick }
display_name: { tag: 2, type: string, sync: once }
inventory: { tag: 3, type: bytes, sync: tick, visibility: owner }
collider:
aabb: [10, 20]
layer: Player
  • entity: The type name; generated code uses it in type names (e.g. SyncedPlayer for Player in go-server).
  • vars: Snake_case keys; each must carry an explicit tag (see below) so the wire format stays stable when you rename or reorder variables.
  • collider: Optional authoritative collision shape registered automatically when the entity enters the server registry. Its layer must appear in the project-level collision block.

Every entity automatically has position fields. You do not declare them in YAML; they are injected by the framework and synchronized like tick fields.

In the default 2D mode, generated entities have pos_x and pos_y. The generated Go server struct exposes Position() (float32, float32) and SetPosition(x, y float32), while JS, Go, and C# clients expose posX / posY, PosX() / PosY(), and PosX / PosY.

In 3D mode (simulation.dimensions: 3 in golem.yaml), generated entities also have pos_z. Go server code uses Position3D() (float32, float32, float32) and SetPosition3D(x, y, z float32) for full 3D movement. Interest management uses the X/Z ground plane for circular fields of interest. JS clients expose posZ, Go clients expose PosZ(), and C# / Unity clients expose PosZ.

The names pos_x, pos_y, and in 3D projects pos_z, are reserved. Using them as var names in your schema results in an error.

Every variable must have a tag, a positive integer that maps to a stable Protobuf field number in the serialized wire format. Tags must be unique within an entity:

vars:
health: { tag: 1, type: double, sync: tick } # required; must be unique within this entity
display_name: { tag: 2, type: string, sync: once }

You can number tags freely starting from 1, and you can leave gaps. The one rule is to never move an existing field to a different tag, since the tag is that field’s identity on the wire and changing it breaks compatibility with any data already serialized under the old number. Missing or duplicate tags result in a bake error.

If an entity should ignore interest management distance, set global: true:

entity: Scoreboard
global: true
vars:
score_a: { tag: 1, type: int32 }
score_b: { tag: 2, type: int32 }

Global entities still have a position, but it is not used for FOI distance checks. A named visibility group can still restrict a global entity to selected sessions. When interest management is not enabled and no visibility group is assigned, global has no effect because every public entity reaches every client.

By default every entity type is included in snapshots. Set persistent: false on types that should be left out: short-lived projectiles, session-specific markers, enemies that respawn from static data:

entity: Projectile
persistent: false
vars:
speed: { tag: 1, type: float, sync: tick }

After golem-bake, the generated type returns false from IsPersistent(). The snapshot system checks this automatically. Omitting persistent defaults to true.

Put a collider on an entity schema when every instance of that type starts with the same server-side shape. The generated entity supplies the shape, and the server registers it before OnSpawn and removes it before OnRemove.

For a 2D project, choose one of aabb or circle:

entity: Player
collider:
aabb: [10, 20] # full width and height
layer: Player
trigger: false
vars:
health: { tag: 1, type: int32 }

For simulation.dimensions: 3, choose aabb3d or sphere:

entity: Pickup
collider:
sphere: 0.75
layer: Pickup
trigger: true
vars:
item_id: { tag: 1, type: uint32, sync: once }

Shape sizes must be positive finite numbers. An entity declares exactly one shape, and the shape must match the project’s simulation dimensions. trigger defaults to false. Baking fails when the layer is absent from collision.layers, when a collider appears without a collision block, or when a 2D and 3D shape are mixed.

Collider metadata configures server behavior only. It does not add protocol fields or invalidate existing snapshot fingerprints. Use the generated runtime’s collision setup before spawning or restoring these entities; see Colliders and 3D collision.

ValueBehavior
tick (default)Field is tracked every tick for changes and included in delta serialization when dirty.
onceSkips the per-tick dirty bitmask; templates treat these fields as full snapshot / spawn-time data.

Omitting sync means tick.

Keep inventory, quest progress, hidden cards, or other per-player state on the authoritative entity without sending it to every observer:

entity: Player
vars:
display_name:
tag: 1
type: string
sync: once
inventory:
tag: 2
type: list<Item>
sync: tick
visibility: owner
private_notes:
tag: 3
type: string
sync: once
visibility: owner

The owning session receives these fields in full state and later deltas. Other sessions still use the same generated client type, but owner-only fields stay at their protobuf default values. Both sync: tick and sync: once fields can be owner-only.

Ownership comes from the session ID passed to CreateEntity, SpawnAvatar, or SetOwner. An unowned entity sends only its public fields. When ownership moves, a previous owner that still knows the entity receives public full state to clear retained private values. The new owner receives authoritative full state when the entity is eligible for that session, on the next replication pass.

Omitting visibility or setting it to all makes a field public. Any other value besides owner fails during golem-bake. This setting controls replication only; entity-targeted command authorization still uses ownership independently. Owner-only fields remain part of authoritative snapshots.

Values use the Protocol Buffers scalar vocabulary (bake maps them straight into the generated serializers). Supported examples include:

double, float, int32, int64, uint32, uint64, sint32, sint64, bool, string, bytes

Unknown types fail at bake time. The same names appear in command schemas.

Entity vars can also hold lists or dictionaries using the following syntax:

Type expressionGo typeTypeScript type
list<int32>[]int32number[]
list<Item>[]ItemItem[]
dict<string, int32>map[string]int32Record<string, number>
dict<string, Item>map[string]ItemRecord<string, Item>

T in list<T> and V in dict<K,V> can be any proto scalar or a custom type you define under types_schema in golem.yaml (default schemas/types/). K must be a proto scalar. See Custom types & collections for how to define custom types and the delta semantics for collections.

For an entity named Player, the go-server integration emits a SyncedPlayer struct with a typed getter and setter for every field you declared: Health() and SetHealth, DisplayName() and SetDisplayName, plus the implicit position accessors (Position() / SetPosition in 2D, adding Position3D() / SetPosition3D() in 3D). You read and write these in your game code, usually from an entity type that embeds *SyncedPlayer. Your first entity walks through wrapping the generated type and driving it each tick.

  • At least one entity schema must exist; an empty entity_schema directory is an error.
  • Every variable must have a tag integer ≥ 1, unique within its entity. Missing or duplicate tags result in a bake error.
  • visibility may be omitted, all, or owner. Owner-only fields are redacted for non-owner sessions but remain part of authoritative persistence.
  • The names pos_x and pos_y are reserved for implicit position fields. In 3D projects, pos_z is reserved too. Names that generate Server or BindServer also conflict with generated runtime accessors. Using reserved names in vars results in a bake error.
  • list<T> and dict<K,V> fields can reference any proto scalar or a custom type defined in your types directory (default schemas/types/). Referencing an unknown custom type results in a bake error.

After edits, run golem-bake to regenerate outputs.