Frontend

Everything so far has been the backend, the schemas and scripts the server runs. The front end is the browser half, and Pointing is built on the front-end abstraction: the page hands over a <canvas> by id, and the game ships its drawing as snippets in a gamehoster-frontend/ folder beside its backend files. The game's baked client (gamehoster.client.js, generated and served with the game) owns the loop, the input and the per-object lifecycle. It hands each snippet a Gamehoster library that reads the whole interpolated world at once: Gamehoster.Instance, Gamehoster.Players and Gamehoster.Entities, each object a view with its interpolated fields at the top level plus .current (its newest authoritative sample), .prev and .data (its front-end persistent bag). Pointing ships the smallest handful of these snippets: setup caches the 2D context once, render draws the dots and cursors each frame, a tick body streams the pointer to the server as a point command, and one input body drops a dot on a click; a small settings file throttles the point stream. There is no prediction and no per-object body: the server is authoritative, and your own cursor is simply drawn at the live pointer.

<gamehoster-config-contentRoot>/
  game.gamehoster.org/
    gamehoster-games/
      pointing/
        gamehoster-frontend/
          gamehoster-frontend-setup.js
          gamehoster-frontend-render.js
          gamehoster-frontend-tick.js
          gamehoster-frontend-settings.json
          gamehoster-frontend-input-mouse-button-left-press.js
The gamehoster-frontend/ folder: setup, render, the tick loop, the settings, and the one per-event input body.

Contents

The folder holds a one-time setup, the per-frame render, a per-tick control loop that streams the pointer, a settings file, and a single per-event input body for the click. The dots are spawned by the server, so there is no dot body here.

NameTypeDescription
gamehoster-frontend-setup.jsfileCache the 2D context once, on connect.
gamehoster-frontend-render.jsfileDraw the dots and every cursor each frame.
gamehoster-frontend-tick.jsfileStream the pointer to the server as point each tick.
gamehoster-frontend-settings.jsonfileThrottle the point command.
gamehoster-frontend-input-mouse-button-left-press.jsfileDrop a dot on a left click.

Loading the client

The Play page provides only a <canvas>, loads the game's baked client from the game server, and calls Gamehoster.start on the canvas with a random display name. The baked client is generic: it carries this game's config and the snapshot runtime, so the same client serves every game. All the game-specific drawing and input lives in the gamehoster-frontend/ snippets, which the client loads and runs for you.

(function () {
  var local = /^(localhost$|127\.|\[?::1)/.test(location.hostname)
  if (!local) window.GAMEHOSTER_SERVER = 'wss://game.gamehoster.org'
  var httpBase = local ? '' : 'https://game.gamehoster.org'
  function load(src, next) { var s = document.createElement('script'); s.src = src; s.onload = next; document.body.appendChild(s) }
  load(httpBase + '/pointing/gamehoster.client.js', function () {
    Gamehoster.start('game', { name: 'P' + Math.floor(Math.random() * 1000) })
  })
})()
How the Play page boots the demo.

gamehoster-frontend/gamehoster-frontend-setup.js

Runs once, on connect. The page handed the client a canvas as the root element, so setup grabs it with Gamehoster.Root() and stashes its 2D context in the game's front-end persistent bag with Gamehoster.Persistent.Set, ready for every render frame.

// Runs once, on connect. The page hands us the canvas as the root element; stash its 2D context in
// the game's front-end persistent bag so every render frame can reach it without touching the DOM.
var canvas = Gamehoster.Root()
Gamehoster.Persistent.Set('ctx', canvas.getContext('2d'))
pointing/gamehoster-frontend/gamehoster-frontend-setup.js

gamehoster-frontend/gamehoster-frontend-render.js

The whole draw, run automatically each frame by the abstraction. It reads the context back from the persistent bag and the world from Gamehoster.Instance, Gamehoster.Players and Gamehoster.Entities. The canvas is 960×600 and matches the instance, so it draws straight in world coordinates. The dropped dots (entities of type 'dot') and the rival cursors come from the interpolated frame, while your own cursor is drawn at the live pointer read from Gamehoster.Mouse (already in canvas coordinates) so it tracks the mouse exactly. When the socket drops it shows a prompt and reconnects on a click via Gamehoster.Reconnect().

// The whole Pointing draw, run automatically each frame by the abstraction. The canvas is 960×600 and
// matches the instance, so we draw straight in world coordinates. The dropped dots and the rival
// cursors come from the interpolated frame — every field read off the world is already a fixed buffer
// behind the server, so motion stays smooth. Our OWN cursor is drawn at the live pointer instead
// (Gamehoster.Mouse, already in canvas coordinates), so it tracks the mouse exactly, ahead of the
// round-trip; the server catches up from the point stream the tick body sends.
var ctx = Gamehoster.Persistent.Get('ctx')
var canvas = Gamehoster.Root()
var W = canvas.width, H = canvas.height
ctx.fillStyle = '#0b0c0f'; ctx.fillRect(0, 0, W, H)

// still connecting, or the socket dropped
var inst = Gamehoster.Instance
if (!inst || inst.width == null) {
  ctx.fillStyle = '#8a8f98'; ctx.font = '18px sans-serif'; ctx.textAlign = 'center'
  ctx.fillText(Gamehoster.connected ? 'connecting…' : 'connection lost — click to reconnect', W / 2, H / 2)
  if (!Gamehoster.connected && Gamehoster.Mouse.pressed('left')) Gamehoster.Reconnect()
  return
}

// dots — the dropped entities, straight from the interpolated frame
for (var d of Gamehoster.Entities.ofType('dot')) {
  ctx.fillStyle = d.color; ctx.beginPath(); ctx.arc(d.x, d.y, 6, 0, 7); ctx.fill()
}

// cursors — rivals interpolated from the frame, our own at the live pointer so it tracks exactly.
// Before the pointer is over the canvas (mouse.inside false) we fall back to our interpolated position.
var mouse = Gamehoster.Mouse
for (var p of Gamehoster.Players.values()) {
  var own = p.self
  var cx = own && mouse.inside ? mouse.x : p.x
  var cy = own && mouse.inside ? mouse.y : p.y
  ctx.fillStyle = p.color || '#fff'
  ctx.beginPath(); ctx.arc(cx, cy, own ? 9 : 7, 0, 7); ctx.fill()
  if (own) { ctx.strokeStyle = '#fff'; ctx.lineWidth = 2; ctx.stroke() }
}

// hint line, with the live head count
ctx.fillStyle = '#8a8f98'; ctx.font = '14px sans-serif'; ctx.textAlign = 'left'
ctx.fillText('move to point · click to drop a dot · ' + Gamehoster.Players.size + ' here', 12, H - 12)
pointing/gamehoster-frontend/gamehoster-frontend-render.js

gamehoster-frontend/gamehoster-frontend-tick.js

Runs each client tick — logic paced to the game's tick rate rather than the display frame rate. While the pointer is over the canvas it streams its position to the server with the point command, which writes to your player.x / player.y so every other client sees your cursor move. It reads Gamehoster.Mouse, which is already in canvas coordinates, so it sends the position as-is; the settings file throttles the stream.

// Runs each client tick. While the pointer is over the canvas, stream its position to the server as a
// point command; the server writes it to our player.x / player.y, so every other client sees our
// cursor move. Our own cursor is drawn straight from the live pointer in the render body, so it needs
// no prediction here. Gamehoster.Mouse is already in canvas coordinates (the canvas matches the
// 960×600 world), so we send it as-is. Nothing to send while the pointer is away.
var mouse = Gamehoster.Mouse
if (!mouse.inside) return
Gamehoster.Command('point', { x: mouse.x, y: mouse.y })   // settings throttle it to 60/s
pointing/gamehoster-frontend/gamehoster-frontend-tick.js

gamehoster-frontend/gamehoster-frontend-settings.json

The front-end input settings. Pointing sets one rate: the point command is throttled to sixty sends a second, coalescing to the latest position, so the per-tick pointer stream never floods the socket.

{
  "rate": { "point": 60 }
}
pointing/gamehoster-frontend/gamehoster-frontend-settings.json

gamehoster-frontend/gamehoster-frontend-input-mouse-button-left-press.js

Runs when the left button goes down. It sends the click command, which carries no params — the server reads your live player.x / player.y (kept current by the point stream) and spawns a dot there. Shipping this one mouse input file is also what makes the abstraction track the pointer, so the render and tick bodies can read Gamehoster.Mouse.

// A left click drops a dot at the cursor. The click command carries no params — the server reads our
// current player.x / player.y (kept live by the point stream from the tick body) and spawns a dot
// there. Shipping this one mouse input file is also what makes the abstraction track the pointer at
// all, so the render and tick bodies can read Gamehoster.Mouse.
Gamehoster.Command('click', {})
pointing/gamehoster-frontend/gamehoster-frontend-input-mouse-button-left-press.js