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'))
    })
  })
})()
examples/pointing/ — booting the demo

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() } }
  }
})()
src/games/pointing/render.js