Frontend

Everything so far has been the backend, the schemas and scripts the server runs. The front end is the browser half, and Pong 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). Pong ships a handful of these snippets: setup caches the 2D context once, render draws the court each frame, a tick body steers your own paddle and sends moveY, and two per-event input bodies ready you up on Space or a click; a small settings file throttles the move command. The per-object predict body on the player and the smooth body on the ball run as before.

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

Contents

The folder holds a one-time setup, the per-frame render, a per-tick control loop, a settings file, and two per-event input bodies. The ball moves from its own server update, so there is no ball body here.

NameTypeDescription
gamehoster-frontend-setup.jsfileCache the 2D context once, on connect.
gamehoster-frontend-render.jsfileDraw the court, paddles, ball and score each frame.
gamehoster-frontend-tick.jsfileSteer your own paddle and send moveY each tick.
gamehoster-frontend-settings.jsonfileThrottle the moveY command.
gamehoster-frontend-input-keyboard-space-press.jsfileReady up on Space.
gamehoster-frontend-input-mouse-button-left-press.jsfileReady up on a click between matches.

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 + '/pong/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'))
pong/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 court is 800×480 and the canvas matches it, so it draws straight in court coordinates. Each paddle is a player view: your own (Gamehoster.Players.self) carries its predicted position, the rival is interpolated, and the ball is the one entity of type 'ball', its position eased between samples by the smooth body. Scores are read from each view's .current so they never flicker fractional between samples.

// The whole Pong draw, run automatically each frame by the abstraction. The court is 800×480 and the
// canvas matches it, so we draw straight in court coordinates. Every field read off the world is
// already interpolated a fixed buffer behind the server, so motion stays smooth: the ball and the
// rival paddle follow the authoritative positions, and our OWN paddle carries its predicted position
// (the predict body reconciled it toward authority), so it answers our input at once.
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.phase == null) {
  ctx.fillStyle = '#8a8f98'; ctx.font = '20px 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
}

// net
ctx.strokeStyle = '#2a2d34'; ctx.setLineDash([8, 12]); ctx.beginPath(); ctx.moveTo(W / 2, 0); ctx.lineTo(W / 2, H); ctx.stroke(); ctx.setLineDash([])

// paddles — our own is the predicted position, the rival interpolated
var pxOff = inst.paddleX, ph = inst.paddleH
for (var p of Gamehoster.Players.values()) {
  var px = p.side === 'left' ? pxOff : W - pxOff
  ctx.fillStyle = p.self ? '#7cc5ff' : '#e6e6e6'
  ctx.fillRect(px - 5, p.y - ph / 2, 10, ph)
}

// ball (its -smooth body snaps position across a post-point re-centre rather than sliding it)
var ball = Gamehoster.Entities.ofType('ball')[0]
if (ball) { ctx.fillStyle = '#fff'; ctx.beginPath(); ctx.arc(ball.x, ball.y, 7, 0, 7); ctx.fill() }

// scores — read authoritative (un-interpolated) so they never flicker fractional between samples
ctx.fillStyle = '#8a8f98'; ctx.font = 'bold 40px monospace'; ctx.textAlign = 'center'
var L = null, R = null
for (var q of Gamehoster.Players.values()) { if (q.side === 'left') L = q; else if (q.side === 'right') R = q }
ctx.fillText(L ? L.current.score : 0, W / 2 - 60, 52)
ctx.fillText(R ? R.current.score : 0, W / 2 + 60, 52)

// status overlay: waiting, ready-up prompt, or the result
if (inst.phase !== 'playing') {
  ctx.fillStyle = 'rgba(0,0,0,.55)'; ctx.fillRect(0, H / 2 - 44, W, 88)
  ctx.fillStyle = '#fff'; ctx.font = '22px sans-serif'
  var me = Gamehoster.Players.self
  var msg = 'Waiting for an opponent…'
  if (Gamehoster.Players.size >= 2) {
    msg = inst.phase === 'over'
      ? ((inst.winner === (me && me.side) ? 'You win!' : (inst.winner + ' wins')) + ' — SPACE to play again')
      : ((me && me.ready) ? 'Ready — waiting for opponent' : 'Press SPACE / click to ready up')
  }
  ctx.fillText(msg, W / 2, H / 2 + 8)
}
pong/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. It decides where you want the paddle (the top on , the bottom on , else the pointer read from Gamehoster.Mouse), feeds that target to the predict body with Gamehoster.Input.Set so your paddle answers at once, and sends an incremental moveY. The gap is measured from the authoritative paddle (self.current.y), not the interpolated draw, so the control loop never chases its own delayed rendering and oscillates.

// Runs each client tick. Steer our OWN paddle toward where we want it — the top on ↑, the bottom
// on ↓, else the pointer — and send an incremental moveY the server applies next tick. We measure
// the gap from the AUTHORITATIVE paddle position (self.current.y), not the interpolated draw, so the
// control loop never chases its own delayed rendering and oscillates. The same target is fed to the
// predict body with Gamehoster.Input.Set, so our paddle answers the instant we move; the ball and
// the rival paddle are just interpolated. Nothing to do until a match is actually in play.
var inst = Gamehoster.Instance
if (inst.phase !== 'playing') return
var me = Gamehoster.Players.self
if (!me || typeof me.current.y !== 'number') return

var court = inst.courtH, half = inst.paddleH / 2, speed = inst.paddleSpeed
var tgt = null
if (Gamehoster.Keys.down('up')) tgt = half
else if (Gamehoster.Keys.down('down')) tgt = court - half
else if (Gamehoster.Mouse.inside) tgt = Math.max(half, Math.min(court - half, Gamehoster.Mouse.fy * court))
if (tgt == null) return

Gamehoster.Input.Set('targetY', tgt)                             // feed the predict body
var axis = Math.max(-1, Math.min(1, (tgt - me.current.y) / speed))
if (Math.abs(axis) > 0.03) Gamehoster.Command('moveY', { axis: axis })   // settings throttle it to 30/s
pong/gamehoster-frontend/gamehoster-frontend-tick.js

gamehoster-frontend/gamehoster-frontend-settings.json

The front-end input settings. Pong sets one rate: the moveY command is throttled to thirty sends a second, coalescing to the latest value, so the per-tick control loop never floods the socket.

{
  "rate": { "moveY": 30 }
}
pong/gamehoster-frontend/gamehoster-frontend-settings.json

gamehoster-frontend/gamehoster-frontend-input-keyboard-space-press.js

Runs when Space goes down. It sends the ready command — to start the first match, or to play again after a result. The server's ready handler ignores it during play, so it is always safe to send.

// Space readies us up — to start the first match, or to play again after a result. The server's
// ready handler ignores it during play, so it is always safe to send.
Gamehoster.Command('ready', {})
pong/gamehoster-frontend/gamehoster-frontend-input-keyboard-space-press.js

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

Runs when the left button goes down. While you are between matches it readies you up; during play it does nothing, since the paddle is steered from the tick body.

// A click readies us up while we are between matches (waiting for an opponent, or on the results
// screen). During play the click does nothing here; the paddle is steered from the tick body.
if (Gamehoster.Instance.phase !== 'playing') Gamehoster.Command('ready', {})
pong/gamehoster-frontend/gamehoster-frontend-input-mouse-button-left-press.js