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().
<!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:
top 44the score readoutright 68the like, report, liked-games and next-game buttonsbottom 104the game’s title and author
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, IndexedDB | use Arcade.save and Arcade.load |
fetch, XMLHttpRequest, WebSocket | there is no network at all |
cookies and service workers | |
<iframe>, <object>, <embed> | |
eval and new Function | |
alert, confirm, prompt, window.open | no-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.