Skip to content
SlashClone
Made by@gameforge

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.

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