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"
  }
]
asteroids/gamehoster-entities/bullet/gamehoster-entity-schema.json
// 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)
asteroids/gamehoster-entities/bullet/gamehoster-entity-update-frontend.js

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"
  }
]
asteroids/gamehoster-entities/rock/gamehoster-entity-schema.json
// 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"))
asteroids/gamehoster-entities/rock/gamehoster-entity-update-frontend.js