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.
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()
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 sync | What you get |
|---|---|
shared | Reproduced locally by running the game's prediction each tick and reconciled against the server's keyframes: motion that keeps moving between packets. |
sent | The authoritative server value (a score, a phase) snapped in as soon as it lands. |
smooth fields | Continuous quantities marked smooth (a paddle, a ball) are render-interpolated between updates so motion is fluid, whether their sync is shared or sent. |
audience owner | This player's own fields; everyone else's owner data isn't there to read. |
server | Never 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.