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.

Provisional. Tracks the frontend API as it settles.

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>
Loading the game's generated library

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.

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.

StepCallYou 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…
Browse the live instances, then matchmake into one

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
One subscription for logical state, one for the render view

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
One call for inputs and commands alike

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.