Files
awesome-copilot/skills/game-engine/assets/paddle-game-template.md
2026-02-22 23:04:42 -05:00

38 KiB

Paddle Game Template (2D Breakout)

A complete step-by-step guide for building a 2D Breakout game with pure JavaScript and the HTML5 Canvas API. This template walks through every stage of development, from setting up the canvas to implementing a lives system and polished game loop.

What you will build: A classic breakout/paddle game where the player controls a paddle to bounce a ball and destroy a field of bricks, with score tracking, win/lose conditions, keyboard and mouse controls, and a lives system.

Prerequisites: Basic to intermediate JavaScript knowledge and familiarity with HTML.

Source: Based on the MDN 2D Breakout Game Tutorial.


Step 1: Create the Canvas and Draw on It

The first step is setting up the HTML document with a <canvas> element and learning to draw basic shapes using the 2D rendering context.

HTML Structure

Create your base HTML file with an embedded canvas element:

<!doctype html>
<html lang="en-US">
  <head>
    <meta charset="utf-8" />
    <title>Gamedev Canvas Workshop</title>
    <style>
      * {
        padding: 0;
        margin: 0;
      }
      canvas {
        background: #eeeeee;
        display: block;
        margin: 0 auto;
      }
    </style>
  </head>
  <body>
    <canvas id="myCanvas" width="480" height="320"></canvas>

    <script>
      // JavaScript code goes here
    </script>
  </body>
</html>

Getting the Canvas Reference and 2D Context

The canvas element provides a drawing surface. You access it through a 2D rendering context:

const canvas = document.getElementById("myCanvas");
const ctx = canvas.getContext("2d");
  • canvas is a reference to the HTML <canvas> element.
  • ctx is the 2D rendering context object, which provides all drawing methods.

Drawing a Filled Rectangle

Use rect() to define a rectangle and fill() to render it:

ctx.beginPath();
ctx.rect(20, 40, 50, 50);
ctx.fillStyle = "red";
ctx.fill();
ctx.closePath();
  • The first two parameters (20, 40) set the top-left corner coordinates.
  • The second two parameters (50, 50) set the width and height.
  • fillStyle sets the fill color.
  • fill() renders the shape as a solid fill.

Drawing a Circle

Use arc() to define a circle:

ctx.beginPath();
ctx.arc(240, 160, 20, 0, Math.PI * 2, false);
ctx.fillStyle = "green";
ctx.fill();
ctx.closePath();
  • 240, 160 -- center x, y coordinates.
  • 20 -- radius.
  • 0 -- start angle (radians).
  • Math.PI * 2 -- end angle (full circle).
  • false -- draw clockwise.

Drawing a Stroked Rectangle (Outline Only)

Use stroke() instead of fill() for outlines, and strokeStyle for outline color:

ctx.beginPath();
ctx.rect(160, 10, 100, 40);
ctx.strokeStyle = "rgb(0 0 255 / 50%)";
ctx.stroke();
ctx.closePath();
  • Uses an RGB color with 50% alpha transparency.
  • stroke() draws only the outline, not a solid fill.

Key Methods Reference

Method Purpose
beginPath() Start a new drawing path
closePath() Close the current path
rect(x, y, width, height) Define a rectangle
arc(x, y, radius, startAngle, endAngle, counterclockwise) Define a circle or arc
fillStyle Set the fill color
fill() Fill the shape with the fill color
strokeStyle Set the stroke (outline) color
stroke() Draw an outline of the shape

Complete Code for Step 1

<canvas id="myCanvas" width="480" height="320"></canvas>

<style>
  * { padding: 0; margin: 0; }
  canvas { background: #eeeeee; display: block; margin: 0 auto; }
</style>

<script>
  const canvas = document.getElementById("myCanvas");
  const ctx = canvas.getContext("2d");

  // Filled red square
  ctx.beginPath();
  ctx.rect(20, 40, 50, 50);
  ctx.fillStyle = "red";
  ctx.fill();
  ctx.closePath();

  // Filled green circle
  ctx.beginPath();
  ctx.arc(240, 160, 20, 0, Math.PI * 2, false);
  ctx.fillStyle = "green";
  ctx.fill();
  ctx.closePath();

  // Stroked blue rectangle (semi-transparent)
  ctx.beginPath();
  ctx.rect(160, 10, 100, 40);
  ctx.strokeStyle = "rgb(0 0 255 / 50%)";
  ctx.stroke();
  ctx.closePath();
</script>

Step 2: Move the Ball

Now we animate the ball by creating a game loop that redraws the canvas on each frame and updates the ball position using velocity variables.

Creating the Draw Loop

Define a draw() function that executes repeatedly using setInterval:

function draw() {
  // drawing code
}
setInterval(draw, 10);

setInterval(draw, 10) calls the draw function every 10 milliseconds, creating approximately 100 frames per second.

Drawing the Ball

Inside the draw() function, draw a ball (circle) at a fixed position:

ctx.beginPath();
ctx.arc(50, 50, 10, 0, Math.PI * 2);
ctx.fillStyle = "#0095DD";
ctx.fill();
ctx.closePath();

Adding Position Variables

Instead of hardcoded positions, use variables so we can update them each frame. Place these above the draw() function:

let x = canvas.width / 2;
let y = canvas.height - 30;

This starts the ball at the horizontal center, near the bottom of the canvas.

Adding Velocity Variables

Define speed and direction for horizontal (dx) and vertical (dy) movement:

let dx = 2;
let dy = -2;
  • dx = 2 moves the ball 2 pixels right per frame.
  • dy = -2 moves the ball 2 pixels up per frame (negative y is upward on canvas).

Updating Position Each Frame

Add position updates at the end of the draw() function:

x += dx;
y += dy;

Clearing the Canvas

Without clearing, the ball leaves a trail. Add clearRect() at the start of each frame:

ctx.clearRect(0, 0, canvas.width, canvas.height);

Refactoring Into a Separate drawBall() Function

For clean, maintainable code, separate the ball-drawing logic:

function drawBall() {
  ctx.beginPath();
  ctx.arc(x, y, 10, 0, Math.PI * 2);
  ctx.fillStyle = "#0095DD";
  ctx.fill();
  ctx.closePath();
}

Complete Code for Step 2

const canvas = document.getElementById("myCanvas");
const ctx = canvas.getContext("2d");

let x = canvas.width / 2;
let y = canvas.height - 30;
let dx = 2;
let dy = -2;

function drawBall() {
  ctx.beginPath();
  ctx.arc(x, y, 10, 0, Math.PI * 2);
  ctx.fillStyle = "#0095DD";
  ctx.fill();
  ctx.closePath();
}

function draw() {
  ctx.clearRect(0, 0, canvas.width, canvas.height);
  drawBall();
  x += dx;
  y += dy;
}

setInterval(draw, 10);

Key concepts:

  • Animation loop: setInterval(draw, 10) continuously redraws the scene.
  • Position variables: x and y track the ball's current location.
  • Velocity variables: dx and dy determine movement per frame.
  • Canvas clearing: clearRect() removes the previous frame before drawing the new one.

Step 3: Bounce Off the Walls

We add collision detection so the ball bounces off the canvas edges instead of disappearing.

Defining the Ball Radius

Extract the ball radius into a named constant for reuse in collision calculations:

const ballRadius = 10;

Update drawBall() to use this variable:

function drawBall() {
  ctx.beginPath();
  ctx.arc(x, y, ballRadius, 0, Math.PI * 2);
  ctx.fillStyle = "#0095DD";
  ctx.fill();
  ctx.closePath();
}

Basic Wall Collision (Without Radius Adjustment)

The simplest approach checks if the next ball position goes beyond the canvas boundaries:

// Left and right walls
if (x + dx > canvas.width || x + dx < 0) {
  dx = -dx;
}

// Top and bottom walls
if (y + dy > canvas.height || y + dy < 0) {
  dy = -dy;
}

Reversing dx or dy (multiplying by -1) changes the ball's direction.

Improved Collision (Accounting for Ball Radius)

The basic version lets the ball sink halfway into the wall before bouncing. To fix this, account for the ball's radius:

// Left and right walls
if (x + dx > canvas.width - ballRadius || x + dx < ballRadius) {
  dx = -dx;
}

// Top and bottom walls
if (y + dy > canvas.height - ballRadius || y + dy < ballRadius) {
  dy = -dy;
}

Collision Detection Conditions

Wall Condition Action
Left x + dx < ballRadius dx = -dx
Right x + dx > canvas.width - ballRadius dx = -dx
Top y + dy < ballRadius dy = -dy
Bottom y + dy > canvas.height - ballRadius dy = -dy

Complete Code for Step 3

const canvas = document.getElementById("myCanvas");
const ctx = canvas.getContext("2d");
const ballRadius = 10;

let x = canvas.width / 2;
let y = canvas.height - 30;
let dx = 2;
let dy = -2;

function drawBall() {
  ctx.beginPath();
  ctx.arc(x, y, ballRadius, 0, Math.PI * 2);
  ctx.fillStyle = "#0095DD";
  ctx.fill();
  ctx.closePath();
}

function draw() {
  ctx.clearRect(0, 0, canvas.width, canvas.height);
  drawBall();

  // Collision detection - left and right walls
  if (x + dx > canvas.width - ballRadius || x + dx < ballRadius) {
    dx = -dx;
  }

  // Collision detection - top and bottom walls
  if (y + dy > canvas.height - ballRadius || y + dy < ballRadius) {
    dy = -dy;
  }

  x += dx;
  y += dy;
}

setInterval(draw, 10);

Step 4: Paddle and Keyboard Controls

Now we add a player-controlled paddle at the bottom of the screen and wire up keyboard input (left/right arrow keys).

Defining Paddle Variables

const paddleHeight = 10;
const paddleWidth = 75;
let paddleX = (canvas.width - paddleWidth) / 2;
  • paddleHeight and paddleWidth define the paddle dimensions.
  • paddleX starts the paddle centered horizontally. It is a let because it will change as the player moves it.

Drawing the Paddle

Create a drawPaddle() function. The paddle sits at the very bottom of the canvas:

function drawPaddle() {
  ctx.beginPath();
  ctx.rect(paddleX, canvas.height - paddleHeight, paddleWidth, paddleHeight);
  ctx.fillStyle = "#0095DD";
  ctx.fill();
  ctx.closePath();
}
  • The y-position is canvas.height - paddleHeight, placing it flush with the bottom edge.

Keyboard State Variables

Track whether arrow keys are currently pressed:

let rightPressed = false;
let leftPressed = false;

Event Listeners for Key Presses

Register handlers for keydown (key pressed) and keyup (key released):

document.addEventListener("keydown", keyDownHandler);
document.addEventListener("keyup", keyUpHandler);

Key Handler Functions

Set the boolean flags based on which key is pressed or released:

function keyDownHandler(e) {
  if (e.key === "Right" || e.key === "ArrowRight") {
    rightPressed = true;
  } else if (e.key === "Left" || e.key === "ArrowLeft") {
    leftPressed = true;
  }
}

function keyUpHandler(e) {
  if (e.key === "Right" || e.key === "ArrowRight") {
    rightPressed = false;
  } else if (e.key === "Left" || e.key === "ArrowLeft") {
    leftPressed = false;
  }
}

Both "ArrowRight" (modern browsers) and "Right" (legacy IE/Edge) are checked for compatibility.

Paddle Movement Logic (With Boundary Checking)

Add this inside the draw() function to move the paddle based on key state, while keeping it within canvas bounds:

if (rightPressed) {
  paddleX = Math.min(paddleX + 7, canvas.width - paddleWidth);
} else if (leftPressed) {
  paddleX = Math.max(paddleX - 7, 0);
}
  • The paddle moves 7 pixels per frame.
  • Math.min prevents the paddle from going past the right edge.
  • Math.max prevents it from going past the left edge.

Complete Code for Step 4

const canvas = document.getElementById("myCanvas");
const ctx = canvas.getContext("2d");
const ballRadius = 10;

let x = canvas.width / 2;
let y = canvas.height - 30;
let dx = 2;
let dy = -2;

const paddleHeight = 10;
const paddleWidth = 75;
let paddleX = (canvas.width - paddleWidth) / 2;

let rightPressed = false;
let leftPressed = false;

document.addEventListener("keydown", keyDownHandler);
document.addEventListener("keyup", keyUpHandler);

function keyDownHandler(e) {
  if (e.key === "Right" || e.key === "ArrowRight") {
    rightPressed = true;
  } else if (e.key === "Left" || e.key === "ArrowLeft") {
    leftPressed = true;
  }
}

function keyUpHandler(e) {
  if (e.key === "Right" || e.key === "ArrowRight") {
    rightPressed = false;
  } else if (e.key === "Left" || e.key === "ArrowLeft") {
    leftPressed = false;
  }
}

function drawBall() {
  ctx.beginPath();
  ctx.arc(x, y, ballRadius, 0, Math.PI * 2);
  ctx.fillStyle = "#0095DD";
  ctx.fill();
  ctx.closePath();
}

function drawPaddle() {
  ctx.beginPath();
  ctx.rect(paddleX, canvas.height - paddleHeight, paddleWidth, paddleHeight);
  ctx.fillStyle = "#0095DD";
  ctx.fill();
  ctx.closePath();
}

function draw() {
  ctx.clearRect(0, 0, canvas.width, canvas.height);
  drawBall();
  drawPaddle();

  if (x + dx > canvas.width - ballRadius || x + dx < ballRadius) {
    dx = -dx;
  }
  if (y + dy > canvas.height - ballRadius || y + dy < ballRadius) {
    dy = -dy;
  }

  if (rightPressed) {
    paddleX = Math.min(paddleX + 7, canvas.width - paddleWidth);
  } else if (leftPressed) {
    paddleX = Math.max(paddleX - 7, 0);
  }

  x += dx;
  y += dy;
}

setInterval(draw, 10);

Step 5: Game Over

We replace the bottom-wall bounce with actual game logic: the ball should bounce off the paddle, but if it misses, it is game over.

Storing the Interval Reference

To stop the game loop on game over, store the interval ID:

let interval = 0;

Then assign the return value of setInterval:

interval = setInterval(draw, 10);

Implementing Game Over and Paddle Collision

Replace the bottom-wall collision check. Instead of bouncing off the bottom edge, we now check whether the ball hits the paddle or misses it:

if (y + dy < ballRadius) {
  // Ball hits top wall -- bounce
  dy = -dy;
} else if (y + dy > canvas.height - ballRadius) {
  // Ball reaches bottom edge
  if (x > paddleX && x < paddleX + paddleWidth) {
    // Ball hits paddle -- bounce
    dy = -dy;
  } else {
    // Ball missed the paddle -- game over
    alert("GAME OVER");
    document.location.reload();
    clearInterval(interval);
  }
}

How paddle collision works:

  • x > paddleX -- the ball is past the paddle's left edge.
  • x < paddleX + paddleWidth -- the ball is before the paddle's right edge.
  • If both are true, the ball is above the paddle, so it bounces.
  • If the ball reaches the bottom without hitting the paddle, the game ends.

Complete Code for Step 5

const canvas = document.getElementById("myCanvas");
const ctx = canvas.getContext("2d");
const ballRadius = 10;

let x = canvas.width / 2;
let y = canvas.height - 30;
let dx = 2;
let dy = -2;

const paddleHeight = 10;
const paddleWidth = 75;
let paddleX = (canvas.width - paddleWidth) / 2;

let rightPressed = false;
let leftPressed = false;
let interval = 0;

document.addEventListener("keydown", keyDownHandler);
document.addEventListener("keyup", keyUpHandler);

function keyDownHandler(e) {
  if (e.key === "Right" || e.key === "ArrowRight") {
    rightPressed = true;
  } else if (e.key === "Left" || e.key === "ArrowLeft") {
    leftPressed = true;
  }
}

function keyUpHandler(e) {
  if (e.key === "Right" || e.key === "ArrowRight") {
    rightPressed = false;
  } else if (e.key === "Left" || e.key === "ArrowLeft") {
    leftPressed = false;
  }
}

function drawBall() {
  ctx.beginPath();
  ctx.arc(x, y, ballRadius, 0, Math.PI * 2);
  ctx.fillStyle = "#0095DD";
  ctx.fill();
  ctx.closePath();
}

function drawPaddle() {
  ctx.beginPath();
  ctx.rect(paddleX, canvas.height - paddleHeight, paddleWidth, paddleHeight);
  ctx.fillStyle = "#0095DD";
  ctx.fill();
  ctx.closePath();
}

function draw() {
  ctx.clearRect(0, 0, canvas.width, canvas.height);
  drawBall();
  drawPaddle();

  // Left and right wall collision
  if (x + dx > canvas.width - ballRadius || x + dx < ballRadius) {
    dx = -dx;
  }

  // Top wall collision
  if (y + dy < ballRadius) {
    dy = -dy;
  } else if (y + dy > canvas.height - ballRadius) {
    // Bottom edge: paddle collision or game over
    if (x > paddleX && x < paddleX + paddleWidth) {
      dy = -dy;
    } else {
      alert("GAME OVER");
      document.location.reload();
      clearInterval(interval);
    }
  }

  // Paddle movement
  if (rightPressed) {
    paddleX = Math.min(paddleX + 7, canvas.width - paddleWidth);
  } else if (leftPressed) {
    paddleX = Math.max(paddleX - 7, 0);
  }

  x += dx;
  y += dy;
}

interval = setInterval(draw, 10);

Step 6: Build the Brick Field

Now we create the grid of bricks that the ball will destroy. The bricks are stored in a 2D array and drawn in rows and columns.

Brick Configuration Variables

Define constants that control the layout of the brick field:

const brickRowCount = 3;
const brickColumnCount = 5;
const brickWidth = 75;
const brickHeight = 20;
const brickPadding = 10;
const brickOffsetTop = 30;
const brickOffsetLeft = 30;
  • brickRowCount / brickColumnCount -- how many rows and columns of bricks.
  • brickWidth / brickHeight -- dimensions of each individual brick.
  • brickPadding -- space between bricks.
  • brickOffsetTop / brickOffsetLeft -- distance from the top and left canvas edges to the first brick.

Creating the Bricks 2D Array

Use nested loops to create a 2D array. Each brick stores its x and y position (initially 0, calculated during drawing):

const bricks = [];
for (let c = 0; c < brickColumnCount; c++) {
  bricks[c] = [];
  for (let r = 0; r < brickRowCount; r++) {
    bricks[c][r] = { x: 0, y: 0 };
  }
}

The drawBricks() Function

Loop through every brick, calculate its position, store it, and draw it:

function drawBricks() {
  for (let c = 0; c < brickColumnCount; c++) {
    for (let r = 0; r < brickRowCount; r++) {
      const brickX = c * (brickWidth + brickPadding) + brickOffsetLeft;
      const brickY = r * (brickHeight + brickPadding) + brickOffsetTop;
      bricks[c][r].x = brickX;
      bricks[c][r].y = brickY;
      ctx.beginPath();
      ctx.rect(brickX, brickY, brickWidth, brickHeight);
      ctx.fillStyle = "#0095DD";
      ctx.fill();
      ctx.closePath();
    }
  }
}

Position calculation formula:

  • brickX = column * (brickWidth + brickPadding) + brickOffsetLeft
  • brickY = row * (brickHeight + brickPadding) + brickOffsetTop

This creates an evenly-spaced grid with consistent padding and margins.

Calling drawBricks() in the Game Loop

Add the call at the beginning of your draw() function, after clearing the canvas:

function draw() {
  ctx.clearRect(0, 0, canvas.width, canvas.height);
  drawBricks();
  drawBall();
  drawPaddle();
  // ... rest of draw function
}

Complete Code for Step 6

const canvas = document.getElementById("myCanvas");
const ctx = canvas.getContext("2d");
const ballRadius = 10;

let x = canvas.width / 2;
let y = canvas.height - 30;
let dx = 2;
let dy = -2;

const paddleHeight = 10;
const paddleWidth = 75;
let paddleX = (canvas.width - paddleWidth) / 2;

let rightPressed = false;
let leftPressed = false;
let interval = 0;

const brickRowCount = 3;
const brickColumnCount = 5;
const brickWidth = 75;
const brickHeight = 20;
const brickPadding = 10;
const brickOffsetTop = 30;
const brickOffsetLeft = 30;

const bricks = [];
for (let c = 0; c < brickColumnCount; c++) {
  bricks[c] = [];
  for (let r = 0; r < brickRowCount; r++) {
    bricks[c][r] = { x: 0, y: 0 };
  }
}

document.addEventListener("keydown", keyDownHandler);
document.addEventListener("keyup", keyUpHandler);

function keyDownHandler(e) {
  if (e.key === "Right" || e.key === "ArrowRight") {
    rightPressed = true;
  } else if (e.key === "Left" || e.key === "ArrowLeft") {
    leftPressed = true;
  }
}

function keyUpHandler(e) {
  if (e.key === "Right" || e.key === "ArrowRight") {
    rightPressed = false;
  } else if (e.key === "Left" || e.key === "ArrowLeft") {
    leftPressed = false;
  }
}

function drawBall() {
  ctx.beginPath();
  ctx.arc(x, y, ballRadius, 0, Math.PI * 2);
  ctx.fillStyle = "#0095DD";
  ctx.fill();
  ctx.closePath();
}

function drawPaddle() {
  ctx.beginPath();
  ctx.rect(paddleX, canvas.height - paddleHeight, paddleWidth, paddleHeight);
  ctx.fillStyle = "#0095DD";
  ctx.fill();
  ctx.closePath();
}

function drawBricks() {
  for (let c = 0; c < brickColumnCount; c++) {
    for (let r = 0; r < brickRowCount; r++) {
      const brickX = c * (brickWidth + brickPadding) + brickOffsetLeft;
      const brickY = r * (brickHeight + brickPadding) + brickOffsetTop;
      bricks[c][r].x = brickX;
      bricks[c][r].y = brickY;
      ctx.beginPath();
      ctx.rect(brickX, brickY, brickWidth, brickHeight);
      ctx.fillStyle = "#0095DD";
      ctx.fill();
      ctx.closePath();
    }
  }
}

function draw() {
  ctx.clearRect(0, 0, canvas.width, canvas.height);
  drawBricks();
  drawBall();
  drawPaddle();

  if (x + dx > canvas.width - ballRadius || x + dx < ballRadius) {
    dx = -dx;
  }
  if (y + dy < ballRadius) {
    dy = -dy;
  } else if (y + dy > canvas.height - ballRadius) {
    if (x > paddleX && x < paddleX + paddleWidth) {
      dy = -dy;
    } else {
      alert("GAME OVER");
      document.location.reload();
      clearInterval(interval);
    }
  }

  if (rightPressed) {
    paddleX = Math.min(paddleX + 7, canvas.width - paddleWidth);
  } else if (leftPressed) {
    paddleX = Math.max(paddleX - 7, 0);
  }

  x += dx;
  y += dy;
}

interval = setInterval(draw, 10);

Step 7: Collision Detection

With bricks on screen, we need to detect when the ball hits one and make it disappear. Each brick gets a status property: 1 means visible, 0 means destroyed.

Adding the Status Property to Bricks

Update the brick initialization to include a status flag:

const bricks = [];
for (let c = 0; c < brickColumnCount; c++) {
  bricks[c] = [];
  for (let r = 0; r < brickRowCount; r++) {
    bricks[c][r] = { x: 0, y: 0, status: 1 };
  }
}

The collisionDetection() Function

Loop through every brick and check if the ball's center is within the brick's bounding box:

function collisionDetection() {
  for (let c = 0; c < brickColumnCount; c++) {
    for (let r = 0; r < brickRowCount; r++) {
      const b = bricks[c][r];
      if (b.status === 1) {
        if (
          x > b.x &&
          x < b.x + brickWidth &&
          y > b.y &&
          y < b.y + brickHeight
        ) {
          dy = -dy;
          b.status = 0;
        }
      }
    }
  }
}

Collision conditions (all four must be true simultaneously):

  • x > b.x -- ball center is to the right of the brick's left edge.
  • x < b.x + brickWidth -- ball center is to the left of the brick's right edge.
  • y > b.y -- ball center is below the brick's top edge.
  • y < b.y + brickHeight -- ball center is above the brick's bottom edge.

When a collision is detected:

  • dy = -dy reverses the ball's vertical direction (bounce).
  • b.status = 0 marks the brick as destroyed.

Updating drawBricks() to Respect Status

Only draw bricks that are still active (status === 1):

function drawBricks() {
  for (let c = 0; c < brickColumnCount; c++) {
    for (let r = 0; r < brickRowCount; r++) {
      if (bricks[c][r].status === 1) {
        const brickX = c * (brickWidth + brickPadding) + brickOffsetLeft;
        const brickY = r * (brickHeight + brickPadding) + brickOffsetTop;
        bricks[c][r].x = brickX;
        bricks[c][r].y = brickY;
        ctx.beginPath();
        ctx.rect(brickX, brickY, brickWidth, brickHeight);
        ctx.fillStyle = "#0095DD";
        ctx.fill();
        ctx.closePath();
      }
    }
  }
}

Calling collisionDetection() in the Game Loop

Add the call in your draw() function, after drawing all elements:

function draw() {
  ctx.clearRect(0, 0, canvas.width, canvas.height);
  drawBricks();
  drawBall();
  drawPaddle();
  collisionDetection();
  // ... rest of draw function
}

Step 8: Track the Score and Win

We add a score counter that increments each time a brick is destroyed, and a win condition that triggers when all bricks are gone.

Initializing the Score

let score = 0;

The drawScore() Function

Display the current score on the canvas using text rendering:

function drawScore() {
  ctx.font = "16px Arial";
  ctx.fillStyle = "#0095DD";
  ctx.fillText(`Score: ${score}`, 8, 20);
}
  • ctx.font sets the font size and family (like CSS).
  • ctx.fillText(text, x, y) renders text at the given coordinates.
  • Position (8, 20) places the score in the top-left corner.

Incrementing the Score

In the collisionDetection() function, increment the score when a brick is hit:

dy = -dy;
b.status = 0;
score++;

Adding the Win Condition

After incrementing the score, check if the player has destroyed all bricks:

score++;
if (score === brickRowCount * brickColumnCount) {
  alert("YOU WIN, CONGRATULATIONS!");
  document.location.reload();
  clearInterval(interval);
}

The total number of bricks is brickRowCount * brickColumnCount. When the score reaches that number, every brick has been destroyed.

Complete collisionDetection() with Score and Win

function collisionDetection() {
  for (let c = 0; c < brickColumnCount; c++) {
    for (let r = 0; r < brickRowCount; r++) {
      const b = bricks[c][r];
      if (b.status === 1) {
        if (
          x > b.x &&
          x < b.x + brickWidth &&
          y > b.y &&
          y < b.y + brickHeight
        ) {
          dy = -dy;
          b.status = 0;
          score++;
          if (score === brickRowCount * brickColumnCount) {
            alert("YOU WIN, CONGRATULATIONS!");
            document.location.reload();
            clearInterval(interval);
          }
        }
      }
    }
  }
}

Calling drawScore() in the Game Loop

Add the call in your draw() function:

function draw() {
  ctx.clearRect(0, 0, canvas.width, canvas.height);
  drawBricks();
  drawBall();
  drawPaddle();
  drawScore();
  collisionDetection();
  // ... rest of draw function
}

Canvas Text Methods Reference

Method/Property Purpose
ctx.font Set font size and family
ctx.fillStyle Set text color
ctx.fillText(text, x, y) Draw filled text at coordinates

Step 9: Mouse Controls

In addition to keyboard controls, we add mouse support so the player can move the paddle by moving the mouse.

Adding the mousemove Event Listener

Register the handler alongside the existing keyboard listeners:

document.addEventListener("mousemove", mouseMoveHandler);

The mouseMoveHandler Function

Calculate the mouse's horizontal position relative to the canvas and update the paddle position:

function mouseMoveHandler(e) {
  const relativeX = e.clientX - canvas.offsetLeft;
  if (relativeX > 0 && relativeX < canvas.width) {
    paddleX = relativeX - paddleWidth / 2;
  }
}

How it works:

  • e.clientX -- the mouse's horizontal position in the browser viewport.
  • canvas.offsetLeft -- the distance from the canvas's left edge to the viewport's left edge.
  • relativeX -- the mouse position relative to the canvas (not the viewport).
  • The boundary check (relativeX > 0 && relativeX < canvas.width) ensures the paddle only moves when the mouse is over the canvas.
  • paddleX = relativeX - paddleWidth / 2 centers the paddle under the mouse cursor by subtracting half the paddle width.

Complete Event Listener Setup (Keyboard + Mouse)

document.addEventListener("keydown", keyDownHandler);
document.addEventListener("keyup", keyUpHandler);
document.addEventListener("mousemove", mouseMoveHandler);

Both control methods work simultaneously. The player can use arrow keys or mouse -- or switch between them at any time.


Step 10: Finishing Up

The final step adds a lives system (so the player gets multiple chances) and upgrades the game loop from setInterval to requestAnimationFrame for smoother rendering.

Adding the Lives Variable

let lives = 3;

The drawLives() Function

Display the remaining lives in the top-right corner:

function drawLives() {
  ctx.font = "16px Arial";
  ctx.fillStyle = "#0095DD";
  ctx.fillText(`Lives: ${lives}`, canvas.width - 65, 20);
}

Implementing the Lives System

Replace the immediate game-over logic with a lives-based system. When the ball misses the paddle:

if (y + dy < ballRadius) {
  dy = -dy;
} else if (y + dy > canvas.height - ballRadius) {
  if (x > paddleX && x < paddleX + paddleWidth) {
    dy = -dy;
  } else {
    lives--;
    if (!lives) {
      alert("GAME OVER");
      document.location.reload();
    } else {
      // Reset ball and paddle positions
      x = canvas.width / 2;
      y = canvas.height - 30;
      dx = 2;
      dy = -2;
      paddleX = (canvas.width - paddleWidth) / 2;
    }
  }
}

What happens when a life is lost:

  • lives-- decrements the lives counter.
  • If lives reaches 0, the game ends with an alert and page reload.
  • Otherwise, the ball resets to center-bottom, velocity resets, and the paddle resets to center.

Upgrading to requestAnimationFrame

Replace setInterval with requestAnimationFrame for a smoother, browser-optimized game loop:

Old approach (remove):

interval = setInterval(draw, 10);

New approach: Add requestAnimationFrame(draw) at the end of the draw() function:

function draw() {
  // ... all drawing and logic ...
  requestAnimationFrame(draw);
}

// Start the game by calling draw() once:
draw();

requestAnimationFrame lets the browser schedule rendering at the optimal frame rate (typically 60fps), which is more efficient than a fixed 10ms interval.

Calling drawLives() in the Game Loop

function draw() {
  ctx.clearRect(0, 0, canvas.width, canvas.height);
  drawBricks();
  drawBall();
  drawPaddle();
  drawScore();
  drawLives();
  collisionDetection();
  // ... rest of logic ...
  requestAnimationFrame(draw);
}

Complete Final Game Code

Below is the entire game in a single, self-contained HTML file. This is the final product of all 10 steps combined.

<!doctype html>
<html lang="en-US">
  <head>
    <meta charset="utf-8" />
    <title>2D Breakout Game</title>
    <style>
      * {
        padding: 0;
        margin: 0;
      }
      canvas {
        background: #eeeeee;
        display: block;
        margin: 0 auto;
      }
    </style>
  </head>
  <body>
    <canvas id="myCanvas" width="480" height="320"></canvas>

    <script>
      const canvas = document.getElementById("myCanvas");
      const ctx = canvas.getContext("2d");

      // --- Ball ---
      const ballRadius = 10;
      let x = canvas.width / 2;
      let y = canvas.height - 30;
      let dx = 2;
      let dy = -2;

      // --- Paddle ---
      const paddleHeight = 10;
      const paddleWidth = 75;
      let paddleX = (canvas.width - paddleWidth) / 2;

      // --- Controls ---
      let rightPressed = false;
      let leftPressed = false;

      // --- Bricks ---
      const brickRowCount = 3;
      const brickColumnCount = 5;
      const brickWidth = 75;
      const brickHeight = 20;
      const brickPadding = 10;
      const brickOffsetTop = 30;
      const brickOffsetLeft = 30;

      const bricks = [];
      for (let c = 0; c < brickColumnCount; c++) {
        bricks[c] = [];
        for (let r = 0; r < brickRowCount; r++) {
          bricks[c][r] = { x: 0, y: 0, status: 1 };
        }
      }

      // --- Score and Lives ---
      let score = 0;
      let lives = 3;

      // =====================
      // Event Listeners
      // =====================
      document.addEventListener("keydown", keyDownHandler);
      document.addEventListener("keyup", keyUpHandler);
      document.addEventListener("mousemove", mouseMoveHandler);

      function keyDownHandler(e) {
        if (e.key === "Right" || e.key === "ArrowRight") {
          rightPressed = true;
        } else if (e.key === "Left" || e.key === "ArrowLeft") {
          leftPressed = true;
        }
      }

      function keyUpHandler(e) {
        if (e.key === "Right" || e.key === "ArrowRight") {
          rightPressed = false;
        } else if (e.key === "Left" || e.key === "ArrowLeft") {
          leftPressed = false;
        }
      }

      function mouseMoveHandler(e) {
        const relativeX = e.clientX - canvas.offsetLeft;
        if (relativeX > 0 && relativeX < canvas.width) {
          paddleX = relativeX - paddleWidth / 2;
        }
      }

      // =====================
      // Collision Detection
      // =====================
      function collisionDetection() {
        for (let c = 0; c < brickColumnCount; c++) {
          for (let r = 0; r < brickRowCount; r++) {
            const b = bricks[c][r];
            if (b.status === 1) {
              if (
                x > b.x &&
                x < b.x + brickWidth &&
                y > b.y &&
                y < b.y + brickHeight
              ) {
                dy = -dy;
                b.status = 0;
                score++;
                if (score === brickRowCount * brickColumnCount) {
                  alert("YOU WIN, CONGRATULATIONS!");
                  document.location.reload();
                }
              }
            }
          }
        }
      }

      // =====================
      // Drawing Functions
      // =====================
      function drawBall() {
        ctx.beginPath();
        ctx.arc(x, y, ballRadius, 0, Math.PI * 2);
        ctx.fillStyle = "#0095DD";
        ctx.fill();
        ctx.closePath();
      }

      function drawPaddle() {
        ctx.beginPath();
        ctx.rect(
          paddleX,
          canvas.height - paddleHeight,
          paddleWidth,
          paddleHeight
        );
        ctx.fillStyle = "#0095DD";
        ctx.fill();
        ctx.closePath();
      }

      function drawBricks() {
        for (let c = 0; c < brickColumnCount; c++) {
          for (let r = 0; r < brickRowCount; r++) {
            if (bricks[c][r].status === 1) {
              const brickX =
                c * (brickWidth + brickPadding) + brickOffsetLeft;
              const brickY =
                r * (brickHeight + brickPadding) + brickOffsetTop;
              bricks[c][r].x = brickX;
              bricks[c][r].y = brickY;
              ctx.beginPath();
              ctx.rect(brickX, brickY, brickWidth, brickHeight);
              ctx.fillStyle = "#0095DD";
              ctx.fill();
              ctx.closePath();
            }
          }
        }
      }

      function drawScore() {
        ctx.font = "16px Arial";
        ctx.fillStyle = "#0095DD";
        ctx.fillText(`Score: ${score}`, 8, 20);
      }

      function drawLives() {
        ctx.font = "16px Arial";
        ctx.fillStyle = "#0095DD";
        ctx.fillText(`Lives: ${lives}`, canvas.width - 65, 20);
      }

      // =====================
      // Main Game Loop
      // =====================
      function draw() {
        ctx.clearRect(0, 0, canvas.width, canvas.height);
        drawBricks();
        drawBall();
        drawPaddle();
        drawScore();
        drawLives();
        collisionDetection();

        // Left and right wall collision
        if (
          x + dx > canvas.width - ballRadius ||
          x + dx < ballRadius
        ) {
          dx = -dx;
        }

        // Top wall collision
        if (y + dy < ballRadius) {
          dy = -dy;
        } else if (y + dy > canvas.height - ballRadius) {
          // Bottom edge: paddle collision or lose a life
          if (x > paddleX && x < paddleX + paddleWidth) {
            dy = -dy;
          } else {
            lives--;
            if (!lives) {
              alert("GAME OVER");
              document.location.reload();
            } else {
              x = canvas.width / 2;
              y = canvas.height - 30;
              dx = 2;
              dy = -2;
              paddleX = (canvas.width - paddleWidth) / 2;
            }
          }
        }

        // Paddle movement (keyboard)
        if (rightPressed) {
          paddleX = Math.min(
            paddleX + 7,
            canvas.width - paddleWidth
          );
        } else if (leftPressed) {
          paddleX = Math.max(paddleX - 7, 0);
        }

        x += dx;
        y += dy;
        requestAnimationFrame(draw);
      }

      draw();
    </script>
  </body>
</html>

Quick Reference: All Game Variables

Variable Type Purpose
canvas const Reference to the HTML canvas element
ctx const 2D rendering context
ballRadius const Radius of the ball (10)
x, y let Current ball position
dx, dy let Ball velocity (pixels per frame)
paddleHeight const Height of the paddle (10)
paddleWidth const Width of the paddle (75)
paddleX let Current horizontal position of the paddle
rightPressed let Whether the right arrow key is held down
leftPressed let Whether the left arrow key is held down
brickRowCount const Number of brick rows (3)
brickColumnCount const Number of brick columns (5)
brickWidth const Width of each brick (75)
brickHeight const Height of each brick (20)
brickPadding const Space between bricks (10)
brickOffsetTop const Distance from top of canvas to first brick row (30)
brickOffsetLeft const Distance from left of canvas to first brick column (30)
bricks const 2D array holding all brick objects
score let Current player score
lives let Remaining lives (starts at 3)

Quick Reference: All Functions

Function Purpose
keyDownHandler(e) Sets rightPressed or leftPressed to true on key press
keyUpHandler(e) Sets rightPressed or leftPressed to false on key release
mouseMoveHandler(e) Moves paddle to follow mouse horizontal position
collisionDetection() Checks ball against all active bricks; destroys hit bricks, increments score, checks win
drawBall() Renders the ball at current (x, y) position
drawPaddle() Renders the paddle at current paddleX position
drawBricks() Renders all bricks with status === 1
drawScore() Renders the score text in the top-left corner
drawLives() Renders the lives text in the top-right corner
draw() Main game loop: clears canvas, draws everything, handles collisions, updates positions