Frontend
Everything so far has been the backend: the schemas and scripts the server
runs. The front end is the browser half, and for a game it is small. It builds on the game's
baked client (gamehoster.client.js, generated from the definition and served
with the game), which gives you Gamehoster.connect, command sending, client-side
prediction and reconciliation, and an interpolated view each
frame. On top of that sits one hand-written file, render.js, that wires input to commands and
draws the frame.
Loading the client
The Play page loads the game's baked client from the game server (so it
carries this game's config, its predicted fields, and the command handlers the client replays), then the
renderer, then starts it on a <canvas>.
(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 () {
load('/src/games/pointing/render.js', function () {
GamehosterGames.pointing(document.getElementById('game'))
})
})
})()
render.js
The whole renderer. It connects, turns pointer input into point and
click commands (each predicted locally the moment it is sent), and every animation frame it
reads conn.frame() and draws it. The frame gives your own cursor from the reconciled
prediction (instant) and everyone else's interpolated between the sparse server snapshots, so a 5/s send
rate still looks smooth.
// Pointing renderer — draws every cursor and every dropped dot from the frame, and
// sends point (move) + click (drop a dot) commands. Registered as GamehosterGames.pointing.
(function () {
'use strict'
window.GamehosterGames = window.GamehosterGames || {}
window.GamehosterGames.pointing = function (canvas, opts) {
const conn = Gamehoster.connect('pointing', {})
const ctx = canvas.getContext('2d')
let last = 0
function toCourt(e) {
const r = canvas.getBoundingClientRect()
return { x: (e.clientX - r.left) * (canvas.width / r.width), y: (e.clientY - r.top) * (canvas.height / r.height) }
}
canvas.addEventListener('mousemove', (e) => {
const now = performance.now(); if (now - last < 16) return; last = now // ~60 commands/s
const p = toCourt(e)
conn.send('point', { x: p.x, y: p.y }) // predicted + reconciled inside the client, so own is instant
})
canvas.addEventListener('click', () => conn.send('click'))
function draw() {
const v = conn.frame()
const W = canvas.width, H = canvas.height
ctx.fillStyle = '#0b0c0f'; ctx.fillRect(0, 0, W, H)
if (!v) { ctx.fillStyle = '#8a8f98'; ctx.font = '18px sans-serif'; ctx.textAlign = 'center'; ctx.fillText('connecting…', W / 2, H / 2); return requestAnimationFrame(draw) }
// dots (entities)
for (const d of v.entities) { if (d.type !== 'dot') continue; ctx.fillStyle = d.color; ctx.beginPath(); ctx.arc(d.x, d.y, 6, 0, 7); ctx.fill() }
// cursors (players)
for (const p of v.players) {
ctx.fillStyle = p.color || '#fff'
ctx.beginPath(); ctx.arc(p.x, p.y, p.id === conn.self ? 9 : 7, 0, 7); ctx.fill()
if (p.id === conn.self) { ctx.strokeStyle = '#fff'; ctx.lineWidth = 2; ctx.stroke() }
}
ctx.fillStyle = '#8a8f98'; ctx.font = '14px sans-serif'; ctx.textAlign = 'left'
ctx.fillText('move to point · click to drop a dot · ' + v.players.length + ' here', 12, H - 12)
requestAnimationFrame(draw)
}
requestAnimationFrame(draw)
return { close() { conn.close() } }
}
})()