Entity
The non-player things. Asteroids has two types: the
bullet every ship fires and the rock that drifts through the arena. Both move by
the same trick: their motion fields are shared, so a
per-tick body integrates them identically on both sides and they need
no packets between the occasional reconciling keyframe. Neither carries a backend update: all the
authoritative work (firing, hit-testing, splitting and culling) lives in the
instance update.
bullet
A single shot. Its x, y, vx and vy are
shared so it drifts predictably, and it is stamped with everything needed to reproduce its
path: the owner who fired it, its origin x0/y0, its angle
and the spawnTick it was born, all sent once.
[
{
"gamehoster-entity-schema-name": "x",
"gamehoster-entity-schema-type": "number",
"gamehoster-entity-schema-default": 0,
"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": 0,
"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"
},
{
"gamehoster-entity-schema-name": "owner",
"gamehoster-entity-schema-type": "string",
"gamehoster-entity-schema-default": "",
"gamehoster-entity-schema-sync": "sent",
"gamehoster-entity-schema-audience": "all"
},
{
"gamehoster-entity-schema-name": "x0",
"gamehoster-entity-schema-type": "number",
"gamehoster-entity-schema-default": 0,
"gamehoster-entity-schema-sync": "sent",
"gamehoster-entity-schema-audience": "all"
},
{
"gamehoster-entity-schema-name": "y0",
"gamehoster-entity-schema-type": "number",
"gamehoster-entity-schema-default": 0,
"gamehoster-entity-schema-sync": "sent",
"gamehoster-entity-schema-audience": "all"
},
{
"gamehoster-entity-schema-name": "angle",
"gamehoster-entity-schema-type": "number",
"gamehoster-entity-schema-default": 0,
"gamehoster-entity-schema-sync": "sent",
"gamehoster-entity-schema-audience": "all"
},
{
"gamehoster-entity-schema-name": "spawnTick",
"gamehoster-entity-schema-type": "int",
"gamehoster-entity-schema-default": 0,
"gamehoster-entity-schema-sync": "sent",
"gamehoster-entity-schema-audience": "all"
}
]
// A bullet's shared update: one integration step, run on both sides. A bullet is the
// cleanest predictable entity there is: it carries the player it belongs to (`owner`),
// the tick it was fired (`spawnTick`), where it started (`x0`, `y0`) and its `angle`,
// so any side can reproduce its whole path. Here we just advance it and wrap it at the
// arena edges; its lifetime and all hit-testing are the backend's authoritative job.
const me = Gamehoster.Context.entityId
const here = Gamehoster.Context.instanceId
const W = Gamehoster.Instance.State.Get(here, "arenaW")
const H = Gamehoster.Instance.State.Get(here, "arenaH")
let x = Gamehoster.Entity.State.Get(me, "x") + Gamehoster.Entity.State.Get(me, "vx")
let y = Gamehoster.Entity.State.Get(me, "y") + Gamehoster.Entity.State.Get(me, "vy")
x = ((x % W) + W) % W
y = ((y % H) + H) % H
Gamehoster.Entity.State.Set(me, "x", x)
Gamehoster.Entity.State.Set(me, "y", y)
rock
A drifting, spinning asteroid. Its position, velocity and rotation (x, y,
vx, vy, rot) are shared so it is fully predictable from
where it started, with x, y and rot smoothed. The rest are
sent once: the spin rate, the size tier (3, 2 or 1), the collision
radius r, the seed the renderer turns into a fixed jagged outline, and the
spawnTick. It stays predictable until the backend splits it.
[
{
"gamehoster-entity-schema-name": "x",
"gamehoster-entity-schema-type": "number",
"gamehoster-entity-schema-default": 0,
"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": 0,
"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"
},
{
"gamehoster-entity-schema-name": "rot",
"gamehoster-entity-schema-type": "number",
"gamehoster-entity-schema-default": 0,
"gamehoster-entity-schema-sync": "shared",
"gamehoster-entity-schema-audience": "all",
"gamehoster-entity-schema-smooth": true
},
{
"gamehoster-entity-schema-name": "spin",
"gamehoster-entity-schema-type": "number",
"gamehoster-entity-schema-default": 0,
"gamehoster-entity-schema-sync": "sent",
"gamehoster-entity-schema-audience": "all"
},
{
"gamehoster-entity-schema-name": "size",
"gamehoster-entity-schema-type": "int",
"gamehoster-entity-schema-default": 3,
"gamehoster-entity-schema-sync": "sent",
"gamehoster-entity-schema-audience": "all"
},
{
"gamehoster-entity-schema-name": "r",
"gamehoster-entity-schema-type": "int",
"gamehoster-entity-schema-default": 46,
"gamehoster-entity-schema-sync": "sent",
"gamehoster-entity-schema-audience": "all"
},
{
"gamehoster-entity-schema-name": "seed",
"gamehoster-entity-schema-type": "int",
"gamehoster-entity-schema-default": 0,
"gamehoster-entity-schema-sync": "sent",
"gamehoster-entity-schema-audience": "all"
},
{
"gamehoster-entity-schema-name": "spawnTick",
"gamehoster-entity-schema-type": "int",
"gamehoster-entity-schema-default": 0,
"gamehoster-entity-schema-sync": "sent",
"gamehoster-entity-schema-audience": "all"
}
]
// A rock's shared update: one integration step, run on both sides (the server for
// authority, every client for prediction). x/y/vx/vy/rot are `sync: "shared"`, so a
// rock is fully predictable from its original position and velocity: given where it
// started and its heading it needs no packets to keep drifting, only the occasional
// reconciling keyframe. It stays predictable right up until the backend splits it.
// The arena wraps, so a rock leaving one edge reappears on the opposite one.
const me = Gamehoster.Context.entityId
const here = Gamehoster.Context.instanceId
const W = Gamehoster.Instance.State.Get(here, "arenaW")
const H = Gamehoster.Instance.State.Get(here, "arenaH")
let x = Gamehoster.Entity.State.Get(me, "x") + Gamehoster.Entity.State.Get(me, "vx")
let y = Gamehoster.Entity.State.Get(me, "y") + Gamehoster.Entity.State.Get(me, "vy")
x = ((x % W) + W) % W
y = ((y % H) + H) % H
Gamehoster.Entity.State.Set(me, "x", x)
Gamehoster.Entity.State.Set(me, "y", y)
Gamehoster.Entity.State.Set(me, "rot", Gamehoster.Entity.State.Get(me, "rot") + Gamehoster.Entity.State.Get(me, "spin"))