Frontend
Everything so far describes the game definition: the folder the server runs. This page is the
other side: how you take the generated client library and build a front end with it. The
library is produced for your exact game, so it already knows every field, command and entity you declared.
It opens the connection, speaks the binary socket protocol for you,
and exposes one global, Gamehoster, with a small, high-level API. You write rendering and
input; it handles the wire, so you write no netcode.
Your game library
Your game's domain automatically hosts a library you can load straight into your page with a single
<script>. There is no bundler step and no import: the tag defines the
global Gamehoster object, customised to your game's logic and capabilities.
<script src="https://pong.gameserver.org/gamehoster.js"></script>
The shape of the API
Everything hangs off two namespaces on the Gamehoster
global. You never touch the protocol (binary frames, snapshots and
deltas, tick numbers, acks, compact ids); it stays an implementation detail underneath.
Gamehoster.Browse →
Before joining: subscribe to the live Instances() the game is running, to
build a menu.
Gamehoster.Instance →
A live room from join to leave: Join or Create it, watch its
State, render each Frame, Send inputs, and Leave.
Handle conventions →
Subscriptions carry onChange/onError/cancel(); operations
carry onSuccess/onError/cancel(); Send is
fire-and-forget.
The lifecycle
A front end walks the same path every time: browse for a room, join one to get an Instance, subscribe to its current state and to each render frame, send the player's inputs and commands, and finally leave.
| Step | Call | You get |
|---|---|---|
| Browse | Gamehoster.Browse.Instances() |
A subscription to the list of live instances, to build a menu. |
| Join | Gamehoster.Instance.Join.Any/Instance(playerInfo, …) or
Gamehoster.Create(instanceInfo?) |
An operation whose onSuccess yields an Instance object. |
| State | Gamehoster.Instance.State(instance) |
A subscription to the authoritative logical state (phase, players, scores) arriving at the server's send rate. |
| Frame | Gamehoster.Instance.Frame(instance) |
A subscription giving an interpolated render view on your own free-running frame loop. |
| Send | Gamehoster.Instance.Send(instance, command) |
Nothing; fire-and-forget input and commands. |
| Leave | Gamehoster.Instance.Leave(instance) |
An operation confirming you're out, ending the room's subscriptions. |
Browsing and joining
Whether the player sees a menu first is decided by the game's
browse setting. Where it allows a choice, subscribe to
the live instances and render them; where it doesn't, skip straight to a matchmaking
join. Either way, joining takes the player's info
and yields an Instance:
const instances = Gamehoster.Browse.Instances()
instances.onChange = (list) => renderMenu(list) // fires now, and on every change
// later: instances.cancel()
const join = Gamehoster.Instance.Join.Any({ name: "Ada" })
join.onSuccess = (instance) => enter(instance)
join.onError = (error) => showJoinError(error) // full, closed, invalid info…
A game whose browse is none exposes no list; you simply call
Instance.Join.Any(playerInfo) and the player is placed by matchmaking. See
Browsing and Joining
for the full detail.
Playing: state, frame and send
Once you hold an instance, two subscriptions cover the whole match, and they run on two
different clocks. Instance.State tracks the logical state: the
instance's phase (a lobby is just the waiting phase, which then moves to
playing), its players and their scores. This is the server's authoritative word: its
sent fields (phase, scores) snap in at the game's send rate, while predicted
shared fields (a paddle's y) stay live at the full tick rate between packets. Each player
carries their fixed join info plus whatever per-instance fields the game's player schema
declares.
Instance.Frame is the render path, and it runs on your clock: a
free-running frame loop that draws as fast as you like, with no fixed render rate. Between the sparse
packets the client itself runs the game's own *-update-frontend.js shared bodies
at the tick rate (the very same code the backend runs), so the logical state keeps simulating and stays
live between snaps, and each frame is interpolated from it and ready to
draw. You never call those bodies yourself; you just read State and Frame and
Send commands:
const state = Gamehoster.Instance.State(instance)
state.onChange = (state) => {
if (state.phase === "waiting") showLobby(state) // players, ready flags
else showScoreboard(state)
}
const frame = Gamehoster.Instance.Frame(instance)
frame.onChange = (view) => draw(view) // free-running frame loop, already interpolated
Both subscriptions show you only what this player is allowed to see. The backend
decides per-player relevance (its optional view script), so State and
Frame carry just the players and entities relevant to you; others enter and
leave as that relevance changes. Never assume you can see the whole instance.
The player acts through Instance.Send: high-frequency input and one-off commands alike.
It is fire-and-forget: it returns nothing, and you observe the result through
Instance.State or Instance.Frame, never a per-call handle:
Gamehoster.Instance.Send(instance, { type: "move", y }) // input
Gamehoster.Instance.Send(instance, { type: "ready" }) // lobby command
Gamehoster.Instance.Send(instance, { type: "start" }) // command
When the player is done, Gamehoster.Instance.Leave(instance) disconnects and ends the
room's subscriptions. See Playing for the instance state and
frame view in full.
Where to go next
Each stage of the lifecycle has its own page; read them in order, then dip into the function reference for every field and shape.