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
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.
| Name | Type | Description |
|---|---|---|
gamehoster-frontend-setup.js | file | Cache the 2D context once, on connect. |
gamehoster-frontend-render.js | file | Draw the dots and every cursor each frame. |
gamehoster-frontend-tick.js | file | Stream the pointer to the server as point each tick. |
gamehoster-frontend-settings.json | file | Throttle the point command. |
gamehoster-frontend-input-mouse-button-left-press.js | file | Drop 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) })
})
})()
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 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)
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
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 }
}