Breakout Game with JavaScript, HTML and CSS

In this comprehensive tutorial, we're diving into creating a Breakout game using plain JavaScript, HTML5, and CSS. This classic arcade game involves a paddle controlled by the player to bounce a ball upwards, breaking bricks. We'll leverage the HTML5 <canvas> element and its API for drawing game elements, handling animations, and implementing game logic.

Project Key Features

  1. Drawing game elements (paddle, ball, bricks) on the canvas.
  2. Animating the ball and paddle using requestAnimationFrame.
  3. Moving the paddle with the keyboard arrow keys.
  4. Implementing collision detection for game dynamics.
  5. Tracking and displaying the score.
  6. Adding a rules modal with game instructions.

Setting Up Your Project

  • Create a Project Directory: Name it JavaScript-Breakout.
  • Project Structure: Inside, create index.html, style.css, and script.js.

Building the Game

1. HTML Structure (index.html)

  • Define the HTML5 structure, linking style.css and script.js.
  • Include a <canvas> element where the game will render.
  • Add a button to show game rules and a modal for displaying them.
Let's create index.html and add the following code to it:
<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <meta http-equiv="X-UA-Compatible" content="ie=edge" />
    <link rel="stylesheet" href="style.css" />
    <title>Breakout!</title>
  </head>
  <body>
    <h1>Breakout!</h1>
    <button id="rules-btn" class="btn rules-btn">Show Rules</button>
    <div id="rules" class="rules">
      <h2>How To Play:</h2>
      <p>
        Use your right and left keys to move the paddle to bounce the ball up
        and break the blocks.
      </p>
      <p>If you miss the ball, your score and the blocks will reset.</p>
      <button id="close-btn" class="btn">Close</button>
    </div>

    <canvas id="canvas" width="800" height="600"></canvas>

    <script src="script.js"></script>
  </body>
</html>

2. Styling (style.css) 

Apply styles to center the game canvas and design the UI components (buttons, modal). Use CSS transitions for modal animations.

Let's create a CSS file named style.css and add the following CSS code to it:
* {
  box-sizing: border-box;
}

body {
  background-color: #0095dd;
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  font-family: Arial, Helvetica, sans-serif;
  min-height: 100vh;
  margin: 0;
}

h1 {
  font-size: 45px;
  color: #fff;
}

canvas {
  background: #f0f0f0;
  display: block;
  border-radius: 5px;
}

.btn {
  cursor: pointer;
  border: 0;
  padding: 10px 20px;
  background: #000;
  color: #fff;
  border-radius: 5px;
}

.btn:focus {
  outline: 0;
}

.btn:hover {
  background: #222;
}

.btn:active {
  transform: scale(0.98);
}

.rules-btn {
  position: absolute;
  top: 30px;
  left: 30px;
}

.rules {
  position: absolute;
  top: 0;
  left: 0;
  background: #333;
  color: #fff;
  min-height: 100vh;
  width: 400px;
  padding: 20px;
  line-height: 1.5;
  transform: translateX(-400px);
  transition: transform 1s ease-in-out;
}

.rules.show {
  transform: translateX(0);
}

3. Game Logic (script.js) 

Let's create a JavaScript file named script.js and add the following JavaScript code to it:
const rulesBtn = document.getElementById('rules-btn');
const closeBtn = document.getElementById('close-btn');
const rules = document.getElementById('rules');
const canvas = document.getElementById('canvas');
const ctx = canvas.getContext('2d');

let score = 0;

const brickRowCount = 9;
const brickColumnCount = 5;
const delay = 500; //delay to reset the game

// Create ball props
const ball = {
  x: canvas.width / 2,
  y: canvas.height / 2,
  size: 10,
  speed: 4,
  dx: 4,
  dy: -4,
  visible: true
};

// Create paddle props
const paddle = {
  x: canvas.width / 2 - 40,
  y: canvas.height - 20,
  w: 80,
  h: 10,
  speed: 8,
  dx: 0,
  visible: true
};

// Create brick props
const brickInfo = {
  w: 70,
  h: 20,
  padding: 10,
  offsetX: 45,
  offsetY: 60,
  visible: true
};

// Create bricks
const bricks = [];
for (let i = 0; i < brickRowCount; i++) {
  bricks[i] = [];
  for (let j = 0; j < brickColumnCount; j++) {
    const x = i * (brickInfo.w + brickInfo.padding) + brickInfo.offsetX;
    const y = j * (brickInfo.h + brickInfo.padding) + brickInfo.offsetY;
    bricks[i][j] = { x, y, ...brickInfo };
  }
}

// Draw ball on canvas
function drawBall() {
  ctx.beginPath();
  ctx.arc(ball.x, ball.y, ball.size, 0, Math.PI * 2);
  ctx.fillStyle = ball.visible ? '#0095dd' : 'transparent';
  ctx.fill();
  ctx.closePath();
}

// Draw paddle on canvas
function drawPaddle() {
  ctx.beginPath();
  ctx.rect(paddle.x, paddle.y, paddle.w, paddle.h);
  ctx.fillStyle = paddle.visible ? '#0095dd' : 'transparent';
  ctx.fill();
  ctx.closePath();
}

// Draw score on canvas
function drawScore() {
  ctx.font = '20px Arial';
  ctx.fillText(`Score: ${score}`, canvas.width - 100, 30);
}

// Draw bricks on canvas
function drawBricks() {
  bricks.forEach(column => {
    column.forEach(brick => {
      ctx.beginPath();
      ctx.rect(brick.x, brick.y, brick.w, brick.h);
      ctx.fillStyle = brick.visible ? '#0095dd' : 'transparent';
      ctx.fill();
      ctx.closePath();
    });
  });
}

// Move paddle on canvas
function movePaddle() {
  paddle.x += paddle.dx;

  // Wall detection
  if (paddle.x + paddle.w > canvas.width) {
    paddle.x = canvas.width - paddle.w;
  }

  if (paddle.x < 0) {
    paddle.x = 0;
    }
}

// Move ball on canvas
function moveBall() {
  ball.x += ball.dx;
  ball.y += ball.dy;

  // Wall collision (right/left)
  if (ball.x + ball.size > canvas.width || ball.x - ball.size < 0) {
    ball.dx *= -1; // ball.dx = ball.dx * -1
  }

  // Wall collision (top/bottom)
  if (ball.y + ball.size > canvas.height || ball.y - ball.size < 0) {
    ball.dy *= -1;
  }

  // console.log(ball.x, ball.y);

  // Paddle collision
  if (
    ball.x - ball.size > paddle.x &&
    ball.x + ball.size < paddle.x + paddle.w &&
    ball.y + ball.size > paddle.y
  ) {
    ball.dy = -ball.speed;
  }

  // Brick collision
  bricks.forEach(column => {
    column.forEach(brick => {
      if (brick.visible) {
        if (
          ball.x - ball.size > brick.x && // left brick side check
          ball.x + ball.size < brick.x + brick.w && // right brick side check
          ball.y + ball.size > brick.y && // top brick side check
          ball.y - ball.size < brick.y + brick.h // bottom brick side check
        ) {
          ball.dy *= -1;
          brick.visible = false;

          increaseScore();
        }
      }
    });
  });

  // Hit bottom wall - Lose
  if (ball.y + ball.size > canvas.height) {
    showAllBricks();
    score = 0;
  }
}

// Increase score
function increaseScore() {
  score++;

  if (score % (brickRowCount * brickColumnCount) === 0) {

      ball.visible = false;
      paddle.visible = false;

      //After 0.5 sec restart the game
      setTimeout(function () {
          showAllBricks();
          score = 0;
          paddle.x = canvas.width / 2 - 40;
          paddle.y = canvas.height - 20;
          ball.x = canvas.width / 2;
          ball.y = canvas.height / 2;
          ball.visible = true;
          paddle.visible = true;
      },delay)
  }
}

// Make all bricks appear
function showAllBricks() {
  bricks.forEach(column => {
    column.forEach(brick => (brick.visible = true));
  });
}

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

  drawBall();
  drawPaddle();
  drawScore();
  drawBricks();
}

// Update canvas drawing and animation
function update() {
  movePaddle();
  moveBall();

  // Draw everything
  draw();

  requestAnimationFrame(update);
}

update();

// Keydown event
function keyDown(e) {
  if (e.key === 'Right' || e.key === 'ArrowRight') {
    paddle.dx = paddle.speed;
  } else if (e.key === 'Left' || e.key === 'ArrowLeft') {
    paddle.dx = -paddle.speed;
  }
}

// Keyup event
function keyUp(e) {
  if (
    e.key === 'Right' ||
    e.key === 'ArrowRight' ||
    e.key === 'Left' ||
    e.key === 'ArrowLeft'
  ) {
    paddle.dx = 0;
  }
}

// Keyboard event handlers
document.addEventListener('keydown', keyDown);
document.addEventListener('keyup', keyUp);

// Rules and close event handlers
rulesBtn.addEventListener('click', () => rules.classList.add('show'));
closeBtn.addEventListener('click', () => rules.classList.remove('show'));

Initializing the Canvas and Game Elements: 

  • Get the canvas context (ctx) for drawing. 
  • Define properties for the ball, paddle, and bricks (size, position, visibility). 

Drawing Functions: 

Implement functions to draw the ball, paddle, bricks, and score on the canvas. 

Game Mechanics: 

Movement: Use arrow keys to move the paddle horizontally. 

Animation: Continuously update game elements' positions and redraw them using requestAnimationFrame. 

Collision Detection: Implement logic to detect collisions between the ball and walls, paddle, and bricks. 

Score Tracking: Increase the score when bricks are broken and reset the game when the ball misses the paddle. 

Event Handling: Add keyboard event listeners for paddle movement. Bind click events to the rules button and modal close button. 

Detailed Explanation of Key JavaScript Concepts 

Canvas API: Learn how to use canvas paths to draw shapes and how requestAnimationFrame creates smooth animations. 

Collision Detection: Understand the mathematics and logic behind detecting collisions between the ball, paddle, and bricks. 

Game State Management: Manage the game state (start, pause, end) and implement game resets.

Open index.html in Browser

Let's open the index.html file in the browser, and you will be able to see the following screen:

Comments