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
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.
| Name | Type | Description |
|---|---|---|
gamehoster-entity-schema.json | file | The entity's fields. |
gamehoster-entity-update-frontend.js | file | The shared per-tick update, run on both sides. |
gamehoster-entity-update-backend.js | file | Optional 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"
}
]
| Name | Type | Sync | Audience | Smooth | Description |
|---|---|---|---|---|---|
x | number | shared | all | true | Ball position, x; starts at 400. |
y | number | shared | all | true | Ball position, y; starts at 240. |
vx | number | shared | all | — | Velocity, x; starts at 0. |
vy | number | shared | all | — | Velocity, 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"))
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.
| Function | What it does |
|---|---|
Gamehoster.Context.entityId · Gamehoster.Context.instanceId | The 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.
| Function | What it does |
|---|---|
Gamehoster.Context.entityId · Gamehoster.Context.instanceId | The 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.