Browser game starter
@gameforge/browser-game-starter
Turn a one-line game idea into a complete, playable single-file HTML5 canvas game: game loop, keyboard and touch input, score, best score, game over, and restart. Ships a template, a working Block Dodge example, and a script that copies either into your project.
- Activity
- New: be the first to try it
- Installs
- 0 installs
- Likes
- 0 likes
- Clones
- 0 clones
- 1<!doctype html>
- 2<html lang="en">
- 3<head>
- 4<meta charset="utf-8">
- 5<meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1, user-scalable=no">
- 6<title>{{TITLE}}</title>
- 7<style>
- 8 html, body { margin: 0; height: 100%; background: #10131a; color: #f2f4f8; font-family: system-ui, sans-serif; }
- 9 body { display: flex; align-items: center; justify-content: center; overflow: hidden; touch-action: none; }
- 10 canvas { background: #1b2030; border-radius: 8px; max-width: 100vw; max-height: 100vh; }
- 11</style>
- 12</head>
- 13<body>
- 14<!-- Idea: {{IDEA}} -->
- 15<canvas id="game" width="480" height="720"></canvas>
- 16<script>
- 17"use strict";
- 18// ---------------------------------------------------------------------------
- 19// {{TITLE}}: single-file HTML5 canvas game made with browser-game-starter.
- 20// Sections: CONFIG, STATE, INPUT, UPDATE, DRAW, LOOP. Change CONFIG first.
- 21// ---------------------------------------------------------------------------
- 23// CONFIG: tune the feel here.
- 24const CONFIG = {
- 25 playerSpeed: 320, // pixels per second
- 26 playerSize: 36,
- 27 itemSize: 22,
- 28 spawnEvery: 0.9, // seconds between items at the start
- 29 minSpawnEvery: 0.35, // fastest spawn rate
- 30 fallSpeed: 180, // starting fall speed, pixels per second
- 31 speedUpPerPoint: 4, // fall speed added per point
- 32 hazardChance: 0.25, // share of items that end the run
- 33};
- 35const canvas = document.getElementById("game");
- 36const ctx = canvas.getContext("2d");
- 37const W = canvas.width, H = canvas.height;
- 39// STATE
- 40let state;
- 41function reset() {
- 42 state = {
- 43 mode: "title", // title | playing | over
- 44 player: { x: W / 2, y: H - 70 },
- 45 items: [],
- 46 score: 0,
- 47 spawnTimer: 0,
- 48 best: loadBest(),
- 49 };
- 50}
- 51function loadBest() { try { return Number(localStorage.getItem("best:{{TITLE}}")) || 0; } catch { return 0; } }
- 52function saveBest(v) { try { localStorage.setItem("best:{{TITLE}}", String(v)); } catch { /* storage blocked */ } }
- 54// INPUT: keyboard, mouse, and touch all drive the same targetX.
- 55const keys = new Set();
- 56let pointerX = null;
- 57addEventListener("keydown", (e) => {
- 58 keys.add(e.key);
- 59 if (e.key === " " || e.key === "Enter") startOrRestart();
- 60});
- 61addEventListener("keyup", (e) => keys.delete(e.key));
- 62function toCanvasX(clientX) {
- 63 const r = canvas.getBoundingClientRect();
- 64 return ((clientX - r.left) / r.width) * W;
- 65}
- 66canvas.addEventListener("pointerdown", (e) => { pointerX = toCanvasX(e.clientX); startOrRestart(); });
- 67canvas.addEventListener("pointermove", (e) => { if (e.buttons || e.pointerType === "touch") pointerX = toCanvasX(e.clientX); });
- 68canvas.addEventListener("pointerup", () => { pointerX = null; });
- 70function startOrRestart() {
- 71 if (state.mode === "playing") return;
- 72 const best = state.best;
- 73 reset();
- 74 state.best = best;
- 75 state.mode = "playing";
- 76}
- 78// UPDATE
- 79function update(dt) {
- 80 if (state.mode !== "playing") return;
- 81 const p = state.player;
- 82 let dir = 0;
- 83 if (keys.has("ArrowLeft") || keys.has("a")) dir -= 1;
- 84 if (keys.has("ArrowRight") || keys.has("d")) dir += 1;
- 85 if (pointerX !== null) dir = Math.sign(pointerX - p.x) * Math.min(1, Math.abs(pointerX - p.x) / 20);
- 86 p.x = Math.max(CONFIG.playerSize / 2, Math.min(W - CONFIG.playerSize / 2, p.x + dir * CONFIG.playerSpeed * dt));
- 88 state.spawnTimer -= dt;
- 89 if (state.spawnTimer <= 0) {
- 90 state.spawnTimer = Math.max(CONFIG.minSpawnEvery, CONFIG.spawnEvery - state.score * 0.01);
- 91 state.items.push({
- 92 x: CONFIG.itemSize + Math.random() * (W - CONFIG.itemSize * 2),
- 93 y: -CONFIG.itemSize,
- 94 hazard: Math.random() < CONFIG.hazardChance,
- 95 });
- 96 }
- 97 const speed = CONFIG.fallSpeed + state.score * CONFIG.speedUpPerPoint;
- 98 for (const it of state.items) it.y += speed * dt;
- 100 const reach = (CONFIG.playerSize + CONFIG.itemSize) / 2;
- 101 for (const it of state.items) {
- 102 if (Math.abs(it.x - p.x) < reach && Math.abs(it.y - p.y) < reach) {
- 103 it.dead = true;
- 104 if (it.hazard) return gameOver();
- 105 state.score += 1;
- 106 }
- 107 }
- 108 state.items = state.items.filter((it) => !it.dead && it.y < H + CONFIG.itemSize);
- 109}
- 110function gameOver() {
- 111 state.mode = "over";
- 112 if (state.score > state.best) { state.best = state.score; saveBest(state.best); }
- 113}
- 115// DRAW
- 116function draw() {
- 117 ctx.clearRect(0, 0, W, H);
- 118 for (const it of state.items) {
- 119 ctx.fillStyle = it.hazard ? "#ff5d73" : "#ffd166";
- 120 ctx.beginPath();
- 121 ctx.arc(it.x, it.y, CONFIG.itemSize / 2, 0, Math.PI * 2);
- 122 ctx.fill();
- 123 }
- 124 const p = state.player, s = CONFIG.playerSize;
- 125 ctx.fillStyle = "#4cc9f0";
- 126 ctx.fillRect(p.x - s / 2, p.y - s / 2, s, s);
- 128 ctx.fillStyle = "#f2f4f8";
- 129 ctx.font = "bold 24px system-ui, sans-serif";
- 130 ctx.textAlign = "left";
- 131 ctx.fillText(`Score ${state.score}`, 16, 36);
- 132 ctx.textAlign = "right";
- 133 ctx.fillText(`Best ${state.best}`, W - 16, 36);
- 135 if (state.mode !== "playing") {
- 136 ctx.fillStyle = "rgba(16, 19, 26, 0.75)";
- 137 ctx.fillRect(0, 0, W, H);
- 138 ctx.fillStyle = "#f2f4f8";
- 139 ctx.textAlign = "center";
- 140 ctx.font = "bold 40px system-ui, sans-serif";
- 141 ctx.fillText(state.mode === "title" ? "{{TITLE}}" : "Game over", W / 2, H / 2 - 30);
- 142 ctx.font = "20px system-ui, sans-serif";
- 143 ctx.fillText("Catch yellow, avoid red", W / 2, H / 2 + 10);
- 144 ctx.fillText("Tap, click, or press Space to play", W / 2, H / 2 + 44);
- 145 }
- 146}
- 148// LOOP: fixed-ish timestep, capped so a background tab does not explode dt.
- 149let last = performance.now();
- 150function frame(now) {
- 151 const dt = Math.min(0.05, (now - last) / 1000);
- 152 last = now;
- 153 update(dt);
- 154 draw();
- 155 requestAnimationFrame(frame);
- 156}
- 157reset();
- 158requestAnimationFrame(frame);
- 159</script>
- 160</body>
- 161</html>
