Rendering

Once a player is in an instance, the generated client keeps the visible state for you (the instance, its players and its entities) already predicted and interpolated. Between the server's sparse packets the client itself runs the game's own *-update-frontend.js shared bodies at the tick rate for you (the same code the backend runs), so the state keeps simulating and stays live between snaps; you never call those bodies yourself. Your only job is to draw: on your own free-running frame loop (as fast as the display allows, with no fixed render rate) you read the current interpolated state and paint it. You never touch the wire, and you write no netcode.

Provisional: this tracks the frontend API as it settles.

Drawing a frame

Gamehoster.Instance.Frame(instance) returns a subscription handle: set its onChange, and the client calls it on a free-running frame loop, as fast as the display allows; there is no fixed render rate. Each call hands you an interpolated frame view (the visible positions of everything on screen this frame) which you simply read and draw. The view only ever holds what you are allowed to see: a game with per-player relevance streams you just the players and entities relevant to you, and they appear and vanish from the view as they enter and leave. Never assume you can see the whole instance.

const frame = Gamehoster.Instance.Frame(instance)
frame.onChange = (view) => {
  clear()

  for (const p of view.players) {            // only the players relevant to you
    drawPaddle(p.side, p.y)                   // already smoothed and interpolated
  }

  for (const ball of view.entities.ball) {   // entities, grouped by type
    drawBall(ball.x, ball.y)
  }

  drawScores(view.instance)
}
frame.onError = (error) => showRenderError(error)
// later: frame.cancel()
A pong render loop: read the interpolated frame view, draw it

What the library does for you

The values you read are already the right ones to draw. How each one reaches you follows from the sync it was declared with in the schema:

Field syncWhat you get
sharedReproduced locally by running the game's prediction each tick and reconciled against the server's keyframes: motion that keeps moving between packets.
sentThe authoritative server value (a score, a phase) snapped in as soon as it lands.
smooth fieldsContinuous quantities marked smooth (a paddle, a ball) are render-interpolated between updates so motion is fluid, whether their sync is shared or sent.
audience ownerThis player's own fields; everyone else's owner data isn't there to read.
serverNever present: server-only, so there's nothing to draw from it.

Only the objects relevant to you reach the render loop at all: a thing you cannot currently see contributes none of its fields. Because prediction, reconciliation and interpolation all happen underneath, your draw code is just a function of the current visible state: no timing, no rewinding, no wire.

The view you draw is not one single moment. Your own player and the deterministic entities (a ball, a rock) are drawn at the present, so your own input feels instant. Other players and the authoritative sent state are drawn a little behind, easing toward the values the server last confirmed, so their motion stays smooth even when a packet is late. One knob, gamehoster-game-displayDelay (measured in ticks), slides how far behind that shared world is drawn. Turn it down for a snappier feel, up to hide more jitter. Your own player is always at the present, whatever you set.

Purely cosmetic randomness (particle jitter, sparks, screen shake) belongs here in the render loop and nowhere else. It is never synced, so drive it with ordinary randomness; keep it out of the game's synced state, which has to stay identical on every client.