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/).
File shape
Section titled “File shape”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: Playerentity: The type name; generated code uses it in type names (e.g.SyncedPlayerforPlayeringo-server).vars: Snake_case keys; each must carry an explicittag(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-levelcollisionblock.
Position
Section titled “Position”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.
Tags (tag)
Section titled “Tags (tag)”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.
Global entities
Section titled “Global entities”If an entity should ignore interest management distance, set global: true:
entity: Scoreboardglobal: 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.
Excluding from snapshots (persistent)
Section titled “Excluding from snapshots (persistent)”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: Projectilepersistent: 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.
Authoritative colliders (collider)
Section titled “Authoritative colliders (collider)”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.
Sync modes (sync)
Section titled “Sync modes (sync)”| Value | Behavior |
|---|---|
tick (default) | Field is tracked every tick for changes and included in delta serialization when dirty. |
once | Skips the per-tick dirty bitmask; templates treat these fields as full snapshot / spawn-time data. |
Omitting sync means tick.
Owner-only fields (visibility)
Section titled “Owner-only fields (visibility)”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: ownerThe 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.
Field types (type)
Section titled “Field types (type)”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.
Collection fields
Section titled “Collection fields”Entity vars can also hold lists or dictionaries using the following syntax:
| Type expression | Go type | TypeScript type |
|---|---|---|
list<int32> | []int32 | number[] |
list<Item> | []Item | Item[] |
dict<string, int32> | map[string]int32 | Record<string, number> |
dict<string, Item> | map[string]Item | Record<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.
What golem-bake generates
Section titled “What golem-bake generates”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_schemadirectory is an error. - Every variable must have a
taginteger ≥ 1, unique within its entity. Missing or duplicate tags result in a bake error. visibilitymay be omitted,all, orowner. Owner-only fields are redacted for non-owner sessions but remain part of authoritative persistence.- The names
pos_xandpos_yare reserved for implicit position fields. In 3D projects,pos_zis reserved too. Names that generateServerorBindServeralso conflict with generated runtime accessors. Using reserved names invarsresults in a bake error. list<T>anddict<K,V>fields can reference any proto scalar or a custom type defined in your types directory (defaultschemas/types/). Referencing an unknown custom type results in a bake error.
After edits, run golem-bake to regenerate outputs.