Unit 7 ยท JavaScript

7.10.6Complete example: Coin Collector

Save as game.html. Use any sprite sheet as images/hero.png with 4 frames per row (64 by 64). If you have no image yet, the game draws a coloured box until the image loads.

HTML
<!DOCTYPE html>
<html lang='en'>
<head>
<meta charset='UTF-8'>
<title>Coin Collector</title>
<style>
  body { text-align: center; font-family: Arial, sans-serif; background: #222; color: #fff; }
  canvas { background: #87ceeb; border: 3px solid #fff; }
</style>
</head>
<body>
<h1>Coin Collector</h1>
<p>Arrow keys or WASD to move. Collect coins, avoid the red enemies.</p>
<canvas id='game' width='640' height='360'></canvas>

<script>
(function () {
  const canvas = document.getElementById('game');
  const ctx = canvas.getContext('2d');

  // ---------- Setup ----------
  const sheet = new Image();
  sheet.src = 'images/hero.png';
  let sheetReady = false;
  sheet.onload = () => { sheetReady = true; };

  const FW = 64, FH = 64, FRAMES = 4, DELAY = 8;
  let frame = 0, tick = 0;

  const player = { x: 300, y: 160, w: 40, h: 40, speed: 4, row: 0 };
  let coins = [], enemies = [];
  let score = 0, lives = 3, over = false;
  const keys = {};

  // ---------- Input ----------
  window.addEventListener('keydown', e => {
    keys[e.code] = true;
    if (e.code.startsWith('Arrow') || e.code === 'Space') e.preventDefault();
    if (over && e.code === 'Enter') restart();
  });
  window.addEventListener('keyup', e => { keys[e.code] = false; });

  // ---------- Helpers ----------
  const rand = (min, max) => Math.random() * (max - min) + min;
  const hit = (a, b) =>
    a.x < b.x + b.w && a.x + a.w > b.x && a.y < b.y + b.h && a.y + a.h > b.y;

  function spawnCoin() {
    coins.push({ x: rand(0, canvas.width - 20), y: rand(0, canvas.height - 20), w: 20, h: 20 });
  }
  function spawnEnemy() {
    enemies.push({ x: canvas.width, y: rand(0, canvas.height - 30), w: 30, h: 30, speed: rand(1.5, 3.5) });
  }

  // ---------- Update ----------
  function update() {
    if (over) return;
    let moving = false;
    if (keys['ArrowLeft']  || keys['KeyA']) { player.x -= player.speed; player.row = 1; moving = true; }
    if (keys['ArrowRight'] || keys['KeyD']) { player.x += player.speed; player.row = 0; moving = true; }
    if (keys['ArrowUp']    || keys['KeyW']) { player.y -= player.speed; moving = true; }
    if (keys['ArrowDown']  || keys['KeyS']) { player.y += player.speed; moving = true; }

    player.x = Math.max(0, Math.min(canvas.width - player.w, player.x));
    player.y = Math.max(0, Math.min(canvas.height - player.h, player.y));

    // sprite animation
    if (moving) {
      if (++tick >= DELAY) { tick = 0; frame = (frame + 1) % FRAMES; }
    } else { frame = 0; }

    // coins: collect on collision
    coins = coins.filter(c => {
      if (hit(player, c)) { score += 10; return false; }   // remove collected coin
      return true;
    });

    // enemies: move left, hurt on collision, remove if off-screen
    enemies.forEach(en => { en.x -= en.speed; });
    enemies = enemies.filter(en => {
      if (hit(player, en)) { lives--; if (lives <= 0) over = true; return false; }
      return en.x + en.w > 0;
    });
  }

  // ---------- Draw ----------
  function draw() {
    ctx.clearRect(0, 0, canvas.width, canvas.height);

    ctx.fillStyle = 'gold';
    coins.forEach(c => {
      ctx.beginPath();
      ctx.arc(c.x + 10, c.y + 10, 10, 0, Math.PI * 2);
      ctx.fill();
    });

    ctx.fillStyle = 'crimson';
    enemies.forEach(en => ctx.fillRect(en.x, en.y, en.w, en.h));

    if (sheetReady) {
      ctx.drawImage(sheet, frame * FW, player.row * FH, FW, FH, player.x, player.y, player.w, player.h);
    } else {
      ctx.fillStyle = 'navy';
      ctx.fillRect(player.x, player.y, player.w, player.h);
    }

    ctx.fillStyle = '#000';
    ctx.font = '18px Arial';
    ctx.fillText('Score: ' + score + '   Lives: ' + lives, 10, 24);

    if (over) {
      ctx.fillStyle = 'rgba(0,0,0,0.6)';
      ctx.fillRect(0, 0, canvas.width, canvas.height);
      ctx.fillStyle = '#fff';
      ctx.font = '36px Arial';
      ctx.fillText('GAME OVER  Score ' + score, 150, 170);
      ctx.font = '18px Arial';
      ctx.fillText('Press Enter to play again', 220, 205);
    }
  }

  // ---------- Loop and timers ----------
  function loop() { update(); draw(); requestAnimationFrame(loop); }

  setInterval(function () { if (!over && coins.length < 6) spawnCoin(); }, 1000);
  setInterval(function () { if (!over) spawnEnemy(); }, 1500);

  function restart() {
    score = 0; lives = 3; over = false; coins = []; enemies = [];
    player.x = 300; player.y = 160;
  }

  requestAnimationFrame(loop);
})();
</script>
</body>
</html>

Understand the code: setup (variables and image), input (keys object), helpers (rand, hit), update (movement, animation, collisions), draw, the loop, and timers. Notice how the whole game is wrapped in an anonymous IIFE and uses anonymous callbacks in filter, forEach and setInterval.

Game practical: assessment criteria

FeatureMarks guide
Scenario implemented as specified (theme, goal, win/lose)20%
Sprite sheet animation (at least walk cycle with direction)20%
Keyboard control (smooth, multiple keys)15%
Collision detection for at least two object types20%
Score, lives or levels, game-over and restart10%
Code quality (functions, comments, naming)10%
Polish (sound, background, difficulty increase, high score)5%

Extensions: shooting with a bullets array, background scrolling, levels with increasing speed, sound with new Audio('coin.wav').play(), saving a high score in localStorage (localStorage.setItem('best', score)), a start screen, and touch controls.

Check yourself. The questions for this part are on the 7.11 Review Questions page, with answers to check your work.