Entities

Entities are the things in the world that aren't players: a ball, food pellets, a chess piece, a projectile. Every entity definition lives in the game's gamehoster-entities/ folder, which holds one directory per entity type, and entities are spawned and destroyed from the instance update with Gamehoster.Entity.Spawn(type, fields) and Gamehoster.Entity.Destroy(id). Each one carries an engine-assigned stable id, so a ref can point at it and survive as entities come and go.

<gamehoster-config-contentRoot>/
  game.gamehoster.org/
    gamehoster-games/
      pong/
        gamehoster-entities/
          ball/
            gamehoster-entity-schema.json
            gamehoster-entity-update-frontend.js
            gamehoster-entity-update-backend.js
The gamehoster-entities/ folder inside a game: one directory per entity type, each with a schema and its update.

An entity has no commands: it isn't driven by a client. Its motion is its own per-tick update, split by trust into a shared front-end file that runs on both sides and an optional backend-only extra (they see different library sets). Like a player's update it reads the previous state and writes only its own. Each .js file is a raw function body (no export, no header) reaching the world through Gamehoster.*, and both always have Gamehoster.Time() (ms), Gamehoster.Tick() (integer) and the predictable Gamehoster.Random() ([0,1), seeded by the tick so it replays on both sides). The backend additionally has the unpredictable Gamehoster.Entropy(); the front-end predict body does not. An entity body names the ids it acts on through Gamehoster.Context, here Gamehoster.Context.entityId and Gamehoster.Context.instanceId, and its writes are buffered and flushed through the same write firewall.

Contents

The gamehoster-entities/ folder holds one directory per entity type; each type directory holds a schema and up to two update scripts.

NameTypeDescription
gamehoster-entity-schema.jsonfileThe entity's fields.
gamehoster-entity-update-frontend.jsfileThe shared per-tick update, run on both sides.
gamehoster-entity-update-backend.jsfileOptional backend-only extra.

gamehoster-entity-schema.json

The entity's fields, an array of field objects in the same typed form as a player's or an instance's schema. A field name is never used as an object key. Pong's ball is just a position and a velocity:

[
  {
    "gamehoster-entity-schema-name": "x",
    "gamehoster-entity-schema-type": "number",
    "gamehoster-entity-schema-default": 400,
    "gamehoster-entity-schema-sync": "shared",
    "gamehoster-entity-schema-audience": "all",
    "gamehoster-entity-schema-smooth": true
  },
  {
    "gamehoster-entity-schema-name": "y",
    "gamehoster-entity-schema-type": "number",
    "gamehoster-entity-schema-default": 240,
    "gamehoster-entity-schema-sync": "shared",
    "gamehoster-entity-schema-audience": "all",
    "gamehoster-entity-schema-smooth": true
  },
  {
    "gamehoster-entity-schema-name": "vx",
    "gamehoster-entity-schema-type": "number",
    "gamehoster-entity-schema-default": 0,
    "gamehoster-entity-schema-sync": "shared",
    "gamehoster-entity-schema-audience": "all"
  },
  {
    "gamehoster-entity-schema-name": "vy",
    "gamehoster-entity-schema-type": "number",
    "gamehoster-entity-schema-default": 0,
    "gamehoster-entity-schema-sync": "shared",
    "gamehoster-entity-schema-audience": "all"
  }
]
pong · gamehoster-entities/ball/gamehoster-entity-schema.json
NameTypeSyncAudienceSmoothDescription
xnumbersharedalltrueBall position, x; starts at 400.
ynumbersharedalltrueBall position, y; starts at 240.
vxnumbersharedallVelocity, x; starts at 0.
vynumbersharedallVelocity, y; starts at 0.

Every field declares its sync and audience: the whole of its networking. The ball's four fields are sync: "shared", audience: "all": not sent as values but reproduced on every client by running the same commands, reconciled by the occasional keyframe. With no session owner the ball is world-simulated: each client runs the same deterministic integration, so it stays in step without belonging to anyone. Position (x, y) additionally carries smooth: true, so the render loop interpolates it between ticks; velocity (vx, vy) does not. The canonical field-model table (every sync, audience and smooth value and what it means) is on the Players page.

An entity can be made relevant per-viewer (fog of war, or level geometry streamed only to the players near it) through the optional gamehoster-player-view.js. That is the outer gate: an entity a viewer can't see delivers none of its fields, and large data (maps, assets) is modelled as an entity so it is sent once on enter rather than every tick. Omit the view script and every entity is relevant to everyone. Pong does.

gamehoster-entity-update-frontend.js & gamehoster-entity-update-backend.js

The entity's own per-tick update: step 2 of the tick, reading the previous state and writing only its own. It is split by trust, not by side, across up to two optional files. The gamehoster-entity-update-frontend.js is the shared body: it runs on the front end and on the backend, the predict-where-possible logic, reading sent / shared fields and writing its own shared fields, using the predictable Gamehoster.Random() but never Gamehoster.Entropy(). The optional gamehoster-entity-update-backend.js is the backend-only extra: the private / authoritative part, run after the shared body, free to touch server / sent state and use Entropy(). The backend runs the superset (shared body, then backend extra); a client runs only the shared body, executing the very same -update-frontend.js file, so the shared logic exists once and cannot diverge.

An entity with only predictable motion needs just the shared file; one that also carries private or authoritative logic adds a gamehoster-entity-update-backend.js. Pong's ball is a pure integration step: its x/y/vx/vy are all sync: "shared", so it ships only the shared front-end file and has no backend file. Cross-cutting logic (the wall and paddle bounces, scoring, the serve) is not here; it belongs to the instance update.

// The ball's shared update — one integration step. This file runs on the server
// (authority) and on every client (prediction); x/y/vx/vy are `sync: "shared"`, so
// the client's write sticks locally and reconciles when the next packet arrives.
// The ball has no private logic, so there is no gamehoster-entity-update-backend.js.

const me   = Gamehoster.Context.entityId
const here = Gamehoster.Context.instanceId
if (Gamehoster.Instance.State.Get(here, "phase") !== "playing") return

Gamehoster.Entity.State.Set(me, "x", Gamehoster.Entity.State.Get(me, "x") + Gamehoster.Entity.State.Get(me, "vx"))
Gamehoster.Entity.State.Set(me, "y", Gamehoster.Entity.State.Get(me, "y") + Gamehoster.Entity.State.Get(me, "vy"))
pong · gamehoster-entities/ball/gamehoster-entity-update-frontend.js: the single shared integration, run on both sides; the ball has no backend file

gamehoster-entity-update-frontend.js: the shared body, run on both sides. Set reaches only this entity's own shared fields, reading the sent / shared state of relevant objects only.

FunctionWhat it does
Gamehoster.Context.entityId · Gamehoster.Context.instanceIdThe ids in scope: this entity and its instance (there is no playerId).
Gamehoster.Entity.State.Get(Gamehoster.Context.entityId, name) · .Set(Gamehoster.Context.entityId, name, value)Read and predict this entity's own state: own shared fields only.
Gamehoster.Instance.State.Get(id, name)Read a sent / shared instance state field.
Gamehoster.Player.List()Ids of the players in this instance.
Gamehoster.Player.State.Get(id, name)Read a player's sent / shared state field.
Gamehoster.Entity.List(type)Ids of the live entities of a type.
Gamehoster.Entity.State.Get(id, name)Read another entity's sent / shared state field.
Gamehoster.Time() · Gamehoster.Tick() · Gamehoster.Random()Clock (ms), tick number, and the predictable random in [0,1). There is no Gamehoster.Entropy() here: the shared body runs on the client too and must stay deterministic.

gamehoster-entity-update-backend.js: the optional backend-only extra, run after the shared body; it may write any of the entity's own fields and touch server / sent state. Pong's ball has no such file.

FunctionWhat it does
Gamehoster.Context.entityId · Gamehoster.Context.instanceIdThe ids in scope: this entity and its instance (there is no playerId).
Gamehoster.Entity.State.Get(Gamehoster.Context.entityId, name) · .Set(Gamehoster.Context.entityId, name, value)Read and write this entity's own state.
Gamehoster.Instance.State.Get(id, name)Read an instance state field.
Gamehoster.Player.List()Ids of the players in this instance.
Gamehoster.Player.State.Get(id, name)Read a player's state field.
Gamehoster.Entity.List(type)Ids of the live entities of a type.
Gamehoster.Entity.State.Get(id, name)Read another entity's state field.
Gamehoster.Time() · Gamehoster.Tick()Clock (ms) and tick number.
Gamehoster.Random()Predictable random in [0,1): seeded by the tick, identical on server and client, safe inside shared logic.
Gamehoster.Entropy()Unpredictable random in [0,1): true randomness for authoritative decisions; its outcome must land in a sent/server field or a spawn. Backend only.

Spawning, destroying, referencing

Entities appear and vanish only through the instance update: step 3 of the tick, the one place for global change. It calls Gamehoster.Entity.Spawn(type, fields), which allocates a stable id and sets the new entity up from fields over the schema defaults, or Gamehoster.Entity.Destroy(id) to reclaim one. There is no create or destroy script and no singleton setting: even a lone ball is spawned by the instance when the match begins.

Because ids are stable, entities can hold ref fields pointing at each other or at a player, and those references survive as things are added and removed. That's how a snake segment knows which snake it belongs to, or a piece knows its owner: so "is this my own body?" or "is this my piece?" is a reliable id check, never a guess from position.