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
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.
| Name | Type | Description |
|---|---|---|
gamehoster-frontend-setup.js | file | Cache the 2D context once, on connect. |
gamehoster-frontend-render.js | file | Draw the court, paddles, ball and score each frame. |
gamehoster-frontend-tick.js | file | Steer your own paddle and send moveY each tick. |
gamehoster-frontend-settings.json | file | Throttle the moveY command. |
gamehoster-frontend-input-keyboard-space-press.js | file | Ready up on Space. |
gamehoster-frontend-input-mouse-button-left-press.js | file | Ready 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) })
})
})()
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'))
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)
}
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
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 }
}
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', {})