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
The gamehoster-entities/ folder inside a game: one directory per entity type, each with a schema and, if it moves itself, its update. Pong's ball keeps a single legacy body.

An entity has no commands: it isn't driven by a client. Its behaviour, if it has any, is its own per-tick update: one authoritative gamehoster-entity-update.js body, run on the server. A game that has not converted an entity may instead ship the legacy pair, gamehoster-entity-update-frontend.js and an optional gamehoster-entity-update-backend.js, run one after the other. Like a player's update it reads the previous state and writes the entity's own. Many entities need no update body at all: a ballistic entity, one whose motion is fixed the moment it is created, stores the constant parameters of its flight and lets the server and every front end compute its live position from those and the clock. Each .js file is a raw function body (no export, no header) reaching the world through Gamehoster.*, and it always has Gamehoster.Time() (ms), Gamehoster.Tick() (integer), the predictable Gamehoster.Random() ([0,1), seeded by the tick and actor) and the unpredictable Gamehoster.Entropy(). An entity body names the ids it acts on through Gamehoster.Context, here Gamehoster.Context.entityId and Gamehoster.Context.instanceId.

Contents

The gamehoster-entities/ folder holds one directory per entity type; each type directory holds a schema and, if the entity moves itself, an update script.

NameTypeDescription
gamehoster-entity-schema.jsonfileThe entity's fields.
gamehoster-entity-update.jsfileOptional: the entity's one authoritative per-tick update, run on the server.

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 a position, a velocity, and a reset counter:

[
  {
    "gamehoster-entity-schema-name": "x",
    "gamehoster-entity-schema-type": "number",
    "gamehoster-entity-schema-default": 400,
    "gamehoster-entity-schema-visibility": "public",
    "gamehoster-entity-schema-min": 0,
    "gamehoster-entity-schema-max": 800,
    "gamehoster-entity-schema-decimals": 1
  },
  {
    "gamehoster-entity-schema-name": "y",
    "gamehoster-entity-schema-type": "number",
    "gamehoster-entity-schema-default": 240,
    "gamehoster-entity-schema-visibility": "public",
    "gamehoster-entity-schema-min": 0,
    "gamehoster-entity-schema-max": 480,
    "gamehoster-entity-schema-decimals": 1
  },
  {
    "gamehoster-entity-schema-name": "vx",
    "gamehoster-entity-schema-type": "number",
    "gamehoster-entity-schema-default": 0,
    "gamehoster-entity-schema-visibility": "public",
    "gamehoster-entity-schema-min": -6,
    "gamehoster-entity-schema-max": 6,
    "gamehoster-entity-schema-decimals": 2
  },
  {
    "gamehoster-entity-schema-name": "vy",
    "gamehoster-entity-schema-type": "number",
    "gamehoster-entity-schema-default": 0,
    "gamehoster-entity-schema-visibility": "public",
    "gamehoster-entity-schema-min": -6,
    "gamehoster-entity-schema-max": 6,
    "gamehoster-entity-schema-decimals": 2
  },
  {
    "gamehoster-entity-schema-name": "resets",
    "gamehoster-entity-schema-type": "int",
    "gamehoster-entity-schema-default": 0,
    "gamehoster-entity-schema-visibility": "public"
  }
]
pong · gamehoster-entities/ball/gamehoster-entity-schema.json
NameTypeVisibilityRangeDescription
xnumberpublic0800, 1 dpBall position, x; starts at 400. Streamed to viewers, who ease it between updates.
ynumberpublic0480, 1 dpBall position, y; starts at 240. Streamed and eased the same way.
vxnumberpublic-66, 2 dpVelocity, x; starts at 0. Drives the server's per-tick integration.
vynumberpublic-66, 2 dpVelocity, y; starts at 0.
resetsintpublicBumped by one each time the ball is re-centred after a point. An unbounded counter (no range hints); the client's optional smoothing body watches it to snap the ball across the jump rather than sliding it back.

Every field declares its visibility: public (streamed to every viewer who can see the object) or private (owner-only, meaningful only on a player — an entity has no owner, so its streamed fields are all public). The ball's fields are public, so the server sends them to every viewer who can see the ball, whenever they change. A bounded numeric field may also carry the -min/-max/-decimals range hints, as the ball's position and velocity do; an unbounded counter like resets omits them. The server integrates the ball each tick and streams its position; the client runs no logic and simply eases the numeric fields between updates so the ball stays smooth. The canonical field-model table (every visibility value and what it means) is on the Players page, and how a field is eased or snapped is on Types.

An entity keeps server-only state — a velocity you would rather not stream, a per-entity timer, AI memory — in its persistent bag, Gamehoster.Entity.Persistent(id), with .Get()/.Set(value) (.Get() returns the live object to mutate in place). It is never typed, tracked or streamed, the same persistent mechanism the instance, players and bots each have.

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 when it appears rather than every tick. Omit the view script and every entity is relevant to everyone. Pong does.

gamehoster-entity-update.js

An entity's own per-tick update: step 2 of the tick, reading the previous state and writing the entity's own. It is one authoritative body, gamehoster-entity-update.js, run on the server. A game that has not converted an entity may instead ship the legacy pair, gamehoster-entity-update-frontend.js then the optional gamehoster-entity-update-backend.js, run one after the other, both authoritative and both free to write the entity's own state. The client runs no game logic and only draws the streamed public state. The update body is optional, and many entities have none (see ballistic entities below).

Pong's ball moves itself, so it ships one body: a single integration step, in a legacy gamehoster-entity-update-frontend.js, and no second file. Its x/y/vx/vy are all public; the body advances the position by the velocity each tick, and the new position streams to every viewer. Cross-cutting logic (the wall and paddle bounces, scoring, the serve) is not here; it belongs to the instance update.

// gamehoster-entity-update-frontend.js: the ball's whole update, one integration step,
// run on the authoritative server each tick. x/y are public, so the new position streams
// to every viewer, who eases it between updates. The ball has no private or authoritative
// 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 ball's single integration step, run on the server; the ball has no backend file

The update body reads any state it can name and writes the entity's own fields. A legacy -update-backend.js second body, when present, runs after it with the same surface, a natural home for private or authoritative work; pong's ball ships a single body and no such second one.

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() · Gamehoster.Random() · Gamehoster.Entropy()Clock (ms), tick number, predictable random in [0,1) (seeded by tick + actor), and unpredictable Entropy().

Ballistic entities: no update body at all

The cheapest entity has no update body. Model its public state as the constant parameters of its motion, the start position, the velocity and the spawn tick, and it never needs to change: the server and every front end compute its live position from those parameters and the clock, position = start + velocity * (tick - spawnTick). Because the parameters never change, the entity is sent once when it appears and then never again, so it costs nothing on the wire for the rest of its life.

Asteroids is built this way: its rocks and bullets are pure ballistic data, a start, a velocity and a spawn tick, with no update body. Hundreds can be in flight for the price of one appear each. The server computes where a rock is only when it needs to, to test a collision, using the very same formula the client draws with, so the two never disagree. When a rock splits, the server destroys it and spawns fresh rocks at the split point with new parameters and a new spawn tick, which re-baselines them cleanly. Their schemas, all parameter fields and no scripts, are the entities of the asteroids example.

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.

Entities are held per type: an instance keeps a separate table of the live entities of each type, so Gamehoster.Entity.List(type) returns just that type's ids and costs O(entities of that type), never a scan of every entity in the room. The id-keyed accessors are unchanged: Gamehoster.Entity.State.Get(id, name) and Gamehoster.Entity.State.Set(id, name, value), Gamehoster.Entity.Spawn(type, fields) and Gamehoster.Entity.Destroy(id) behave exactly as before and stay O(1) by id. The surface is identical in every update body, whether the single gamehoster-entity-update.js or a legacy -update-frontend.js / -update-backend.js half, so existing game code needs no change: this is a storage and performance improvement, nothing more.