Building a game

A game is one HTML file that runs on a phone. There is no build step, no framework and nothing to install — open an editor, write a file, upload it.

Start from this

A complete, working game. It is deliberately small, and it already does the four things people most often get wrong: a delta-time loop, laying out inside the safe area, stopping on pause, and calling Arcade.ready().

tap-the-dot.html
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width,initial-scale=1,viewport-fit=cover,user-scalable=no">
<title>Tap the dot</title>
<style>
  body { background:#101018; }
  #msg {
    position:fixed; inset:0; display:grid; place-items:center;
    font:600 15px system-ui,sans-serif; color:#fff; text-align:center;
    background:rgba(16,16,24,.9); pointer-events:none; transition:opacity .2s;
  }
  #msg.hidden { opacity:0; }
</style>
</head>
<body>
<canvas id="c"></canvas>
<div id="msg"><div>Tap the dot before it escapes<br><br>tap to start</div></div>

<script>
(function () {
  var c = document.getElementById('c'), g = c.getContext('2d');
  var msg = document.getElementById('msg');
  var W, H, playing = false, score = 0, best = 0, dot, left;

  function resize() {
    var dpr = Math.min(window.devicePixelRatio || 1, 2);
    W = window.innerWidth; H = window.innerHeight;
    c.style.width = W + 'px'; c.style.height = H + 'px';
    c.width = W * dpr; c.height = H * dpr;
    g.setTransform(dpr, 0, 0, dpr, 0, 0);
  }
  window.addEventListener('resize', resize);

  // The feed draws its controls over these edges, so the dot never goes there.
  function place() {
    var i = Arcade.insets;
    var r = 28;
    dot = {
      x: i.left + r + Math.random() * (W - i.left - i.right - r * 2),
      y: i.top + r + Math.random() * (H - i.top - i.bottom - r * 2),
      r: r
    };
    left = 1200;
  }
  Arcade.on('insets', function () { if (playing) place(); });

  function start() {
    score = 0; playing = true;
    Arcade.score(0);
    msg.classList.add('hidden');
    place();
  }

  function over() {
    playing = false;
    if (score > best) { best = score; Arcade.save('best', best); }
    msg.innerHTML = '<div>' + score + ' caught<br>best ' + best + '<br><br>tap to play again</div>';
    msg.classList.remove('hidden');
    Arcade.gameOver({ score: score });
  }

  c.addEventListener('pointerdown', function (e) {
    if (!playing) return start();
    if (Math.hypot(e.clientX - dot.x, e.clientY - dot.y) > dot.r) return;
    score++;
    Arcade.score(score);
    Arcade.haptic('light');
    place();
  });

  // One delta-time loop, everything in milliseconds.
  var last = 0, paused = false;
  function frame(now) {
    requestAnimationFrame(frame);
    if (paused) { last = now; return; }
    var dt = Math.min(now - last, 50);
    last = now;

    if (playing) {
      left -= dt;
      if (left <= 0) return over();
    }

    g.fillStyle = '#101018';
    g.fillRect(0, 0, W, H);
    if (playing) {
      g.fillStyle = '#4ade80';
      g.beginPath();
      g.arc(dot.x, dot.y, dot.r * (0.6 + 0.4 * (left / 1200)), 0, 6.2832);
      g.fill();
    }
  }

  Arcade.on('pause', function () { paused = true; });
  Arcade.on('resume', function () { paused = false; last = performance.now(); });

  resize();
  Arcade.load('best').then(function (v) { best = Number(v) || 0; });
  requestAnimationFrame(function (t) { last = t; frame(t); });
  Arcade.ready();
})();
</script>
</body>
</html>

The screen is not all yours

The feed draws its own controls on top of every game. Anything interactive, or anything the player has to read, needs to stay out of these edges:

Do not hard-code those numbers. Read Arcade.insets, or use the CSS variables --arcade-inset-top, --arcade-inset-right, --arcade-inset-bottom and --arcade-inset-left. They can change, so re-layout when Arcade.on('insets') fires.

The runtime

window.Arcade already exists when your code runs. There is nothing to import and no script tag to add.

Arcade.ready()call once when playable; clears the loading screen
Arcade.score(n)current score, shown in the chrome
Arcade.gameOver({score, won})a run ended
Arcade.progress(0..1)optional
await Arcade.save(key, value)persist a value
await Arcade.load(key)read it back, or null
Arcade.haptic('light' | 'success')vibrate
Arcade.on('pause', fn)player left: stop timers, sound and animation
Arcade.on('resume', fn)player came back
Arcade.on('insets', fn)safe area changed, re-layout
Arcade.insets{top, right, bottom, left}
Arcade.registerAudio(ctx)lets an AudioContext be suspended on pause
Arcade.next()ask the feed to move on

What is not available

Your game runs in a sandboxed iframe with an opaque origin and no network. That is what stops one upload reading another’s data or phoning home, and it means the following throw or silently do nothing. An upload using any of them is rejected automatically, naming the line.

localStorage, sessionStorage, IndexedDBuse Arcade.save and Arcade.load
fetch, XMLHttpRequest, WebSocketthere is no network at all
cookies and service workers
<iframe>, <object>, <embed>
eval and new Function
alert, confirm, prompt, window.openno-ops

Everything must be inline. Scripts may be loaded from cdn.jsdelivr.netcdnjs.cloudflare.com and nothing else.

Input

Every gesture is yours: tap, hold, drag in any direction, multi-touch, tilt. Nothing is reserved, because players move between games with a button rather than a swipe. Design for one thumb, and handle pointercancel so a drag that gets interrupted does not leave your game stuck mid-gesture.

Things that go wrong

Mixing per-frame and per-millisecond units
Run a delta-time loop and keep every speed and acceleration in units of milliseconds. Mixing the two is the single most common way one of these ends up unplayably fast or slow, and it looks completely fine in the code.
Forgetting Arcade.ready()
The feed covers your game with a loading screen until you call it. Leave it out and players stare at that screen for eight seconds before it gives up and marks the game broken.
Laying out to the full screen
The feed draws its own controls over the top, right and bottom edges. Anything you put there is partly unreachable. Lay out against Arcade.insets instead.
Carrying on while paused
When a player moves to the next game yours keeps existing. Stop timers, animation work and audio on pause, or it runs in the background eating their battery.
Reaching for localStorage
It throws. Your game runs in an opaque origin, which is what stops other games reading your data. Arcade.save and Arcade.load are namespaced to you and are async.

Using an AI to write it

Paste this in before you describe the game you want. It is the same information as this page, written for a machine.