Skip to content

Instantly share code, notes, and snippets.

@straker
Last active February 7, 2024 11:59
  • Star 21 You must be signed in to star a gist
  • Fork 5 You must be signed in to fork a gist
Star You must be signed in to star a gist
Save straker/98a2aed6a7686d26c04810f08bfaf66b to your computer and use it in GitHub Desktop.
Basic Breakout HTML and JavaScript Game

Basic Breakout HTML and JavaScript Game

This is a basic implementation of the Atari Breakout game, but it's missing a few things intentionally and they're left as further exploration for the reader.

Further Exploration

  • Lives
    • The player should have 3 chances to remove all the bricks. Display how many lives the player currently has using context.fillText(). Remove a life when the ball goes below the screen
  • Score
  • Ball speed
  • Mobile and touchscreen support
  • Better paddle movement
    • Currently the paddle movement is sticky. If you press the opposite direction while releasing the other direction, the padddle doesn't move right away. And if you release a direction while holding the other, the paddle stops. Improve it so it doesn't do this.

Important note: I will answer questions about the code but will not add more features or answer questions about adding more features. This series is meant to give a basic outline of the game but nothing more.

License

(CC0 1.0 Universal) You're free to use this game and code in any project, personal or commercial. There's no need to ask permission before using these. Giving attribution is not required, but appreciated.

Other Basic Games

Support

Basic HTML Games are made possible by users like you. When you become a Patron, you get access to behind the scenes development logs, the ability to vote on which games I work on next, and early access to the next Basic HTML Game.

Top Patrons

  • Karar Al-Remahy
  • UnbrandedTech
  • Innkeeper Games
  • Nezteb
<!DOCTYPE html>
<html>
<head>
<title>Basic Breakout HTML Game</title>
<meta charset="UTF-8">
<style>
html, body {
height: 100%;
margin: 0;
}
body {
background: black;
display: flex;
align-items: center;
justify-content: center;
}
</style>
</head>
<body>
<canvas width="400" height="500" id="game"></canvas>
<script>
const canvas = document.getElementById('game');
const context = canvas.getContext('2d');
// each row is 14 bricks long. the level consists of 6 blank rows then 8 rows
// of 4 colors: red, orange, green, and yellow
const level1 = [
[],
[],
[],
[],
[],
[],
['R','R','R','R','R','R','R','R','R','R','R','R','R','R'],
['R','R','R','R','R','R','R','R','R','R','R','R','R','R'],
['O','O','O','O','O','O','O','O','O','O','O','O','O','O'],
['O','O','O','O','O','O','O','O','O','O','O','O','O','O'],
['G','G','G','G','G','G','G','G','G','G','G','G','G','G'],
['G','G','G','G','G','G','G','G','G','G','G','G','G','G'],
['Y','Y','Y','Y','Y','Y','Y','Y','Y','Y','Y','Y','Y','Y'],
['Y','Y','Y','Y','Y','Y','Y','Y','Y','Y','Y','Y','Y','Y']
];
// create a mapping between color short code (R, O, G, Y) and color name
const colorMap = {
'R': 'red',
'O': 'orange',
'G': 'green',
'Y': 'yellow'
};
// use a 2px gap between each brick
const brickGap = 2;
const brickWidth = 25;
const brickHeight = 12;
// the wall width takes up the remaining space of the canvas width. with 14 bricks
// and 13 2px gaps between them, thats: 400 - (14 * 25 + 2 * 13) = 24px. so each
// wall will be 12px
const wallSize = 12;
const bricks = [];
// create the level by looping over each row and column in the level1 array
// and creating an object with the bricks position (x, y) and color
for (let row = 0; row < level1.length; row++) {
for (let col = 0; col < level1[row].length; col++) {
const colorCode = level1[row][col];
bricks.push({
x: wallSize + (brickWidth + brickGap) * col,
y: wallSize + (brickHeight + brickGap) * row,
color: colorMap[colorCode],
width: brickWidth,
height: brickHeight
});
}
}
const paddle = {
// place the paddle horizontally in the middle of the screen
x: canvas.width / 2 - brickWidth / 2,
y: 440,
width: brickWidth,
height: brickHeight,
// paddle x velocity
dx: 0
};
const ball = {
x: 130,
y: 260,
width: 5,
height: 5,
// how fast the ball should go in either the x or y direction
speed: 2,
// ball velocity
dx: 0,
dy: 0
};
// check for collision between two objects using axis-aligned bounding box (AABB)
// @see https://developer.mozilla.org/en-US/docs/Games/Techniques/2D_collision_detection
function collides(obj1, obj2) {
return obj1.x < obj2.x + obj2.width &&
obj1.x + obj1.width > obj2.x &&
obj1.y < obj2.y + obj2.height &&
obj1.y + obj1.height > obj2.y;
}
// game loop
function loop() {
requestAnimationFrame(loop);
context.clearRect(0,0,canvas.width,canvas.height);
// move paddle by it's velocity
paddle.x += paddle.dx;
// prevent paddle from going through walls
if (paddle.x < wallSize) {
paddle.x = wallSize
}
else if (paddle.x + brickWidth > canvas.width - wallSize) {
paddle.x = canvas.width - wallSize - brickWidth;
}
// move ball by it's velocity
ball.x += ball.dx;
ball.y += ball.dy;
// prevent ball from going through walls by changing its velocity
// left & right walls
if (ball.x < wallSize) {
ball.x = wallSize;
ball.dx *= -1;
}
else if (ball.x + ball.width > canvas.width - wallSize) {
ball.x = canvas.width - wallSize - ball.width;
ball.dx *= -1;
}
// top wall
if (ball.y < wallSize) {
ball.y = wallSize;
ball.dy *= -1;
}
// reset ball if it goes below the screen
if (ball.y > canvas.height) {
ball.x = 130;
ball.y = 260;
ball.dx = 0;
ball.dy = 0;
}
// check to see if ball collides with paddle. if they do change y velocity
if (collides(ball, paddle)) {
ball.dy *= -1;
// move ball above the paddle otherwise the collision will happen again
// in the next frame
ball.y = paddle.y - ball.height;
}
// check to see if ball collides with a brick. if it does, remove the brick
// and change the ball velocity based on the side the brick was hit on
for (let i = 0; i < bricks.length; i++) {
const brick = bricks[i];
if (collides(ball, brick)) {
// remove brick from the bricks array
bricks.splice(i, 1);
// ball is above or below the brick, change y velocity
// account for the balls speed since it will be inside the brick when it
// collides
if (ball.y + ball.height - ball.speed <= brick.y ||
ball.y >= brick.y + brick.height - ball.speed) {
ball.dy *= -1;
}
// ball is on either side of the brick, change x velocity
else {
ball.dx *= -1;
}
break;
}
}
// draw walls
context.fillStyle = 'lightgrey';
context.fillRect(0, 0, canvas.width, wallSize);
context.fillRect(0, 0, wallSize, canvas.height);
context.fillRect(canvas.width - wallSize, 0, wallSize, canvas.height);
// draw ball if it's moving
if (ball.dx || ball.dy) {
context.fillRect(ball.x, ball.y, ball.width, ball.height);
}
// draw bricks
bricks.forEach(function(brick) {
context.fillStyle = brick.color;
context.fillRect(brick.x, brick.y, brick.width, brick.height);
});
// draw paddle
context.fillStyle = 'cyan';
context.fillRect(paddle.x, paddle.y, paddle.width, paddle.height);
}
// listen to keyboard events to move the paddle
document.addEventListener('keydown', function(e) {
// left arrow key
if (e.which === 37) {
paddle.dx = -3;
}
// right arrow key
else if (e.which === 39) {
paddle.dx = 3;
}
// space key
// if they ball is not moving, we can launch the ball using the space key. ball
// will move towards the bottom right to start
if (ball.dx === 0 && ball.dy === 0 && e.which === 32) {
ball.dx = ball.speed;
ball.dy = ball.speed;
}
});
// listen to keyboard events to stop the paddle if key is released
document.addEventListener('keyup', function(e) {
if (e.which === 37 || e.which === 39) {
paddle.dx = 0;
}
});
// start the game
requestAnimationFrame(loop);
</script>
</body>
</html>
@straker
Copy link
Author

straker commented Mar 31, 2021

@BenjiG64 I cannot. I leave adding new features to the code as a further exploration.

@HanoiWarrior
Copy link

HanoiWarrior commented Aug 28, 2021

how can I change the ball color? @straker

@straker
Copy link
Author

straker commented Aug 28, 2021

@HanoiWarrior before calling context.fillRect on the ball, change the fill style using context.fillStyle = 'colorName';

@RagnowProductions
Copy link

The Ball Is Not Showing Up For Some Reason. How Do I Fix This?

@straker
Copy link
Author

straker commented Feb 8, 2022

@RagnowProductions I'm not sure. Are you getting any errors in the browser DevTools console?

@RagnowProductions
Copy link

@straker I'm Not Getting Any Errors. When I Run The HTML, It Doesn't Display The Ball.

Screenshot 2022-02-17 14 24 35

@straker
Copy link
Author

straker commented Feb 18, 2022

@RagnowProductions Hu, that's strange. What browser and version are you using? Does the ball appear when you hit the Space key?

@RagnowProductions
Copy link

using google chrome
also yes, it does pop up from pressing the space key

@straker
Copy link
Author

straker commented Feb 25, 2022

@RagnowProductions Ok, that's good then. The ball won't show up until you press space to launch the ball.

@max1444
Copy link

max1444 commented Apr 18, 2022

could you please tell me on how to make different levels

@straker
Copy link
Author

straker commented Apr 19, 2022

@max1444 Add more levels using the same structure as the level1 array and then when one level ends, load the next level.

@max1444
Copy link

max1444 commented Apr 20, 2022

@straker I dont really under stand how could i do it or could you show a code for that

@max1444
Copy link

max1444 commented Apr 20, 2022

how could i load the next level and what do i change in the code

@remediation-html
Copy link

Just letting you guys know that the score needs a link to the HTML code. you cannot just copy paste the code from the included help guide in the description of this project. :) hopefully that makes some sense to somebody

@YanLouQA
Copy link

YanLouQA commented Dec 20, 2022

@straker Hi) I still can't figure out what I need to do. I want that after all bricks are removed from the field, a new level will start. I tried different ways, but nothing happens. The ball just keeps flying across the empty field. I would be very grateful for your help)

@Lucarnosky
Copy link

I know that is an old topic, but I made this improvement. I was finding odd that the ball goes to the right corner at the start, with this change in the code, now it will always aim at the center of the paddle (if not moving)

if(paddle.x -170 < 0){
      ball.x = paddle.x + 180;
      ball.dx = -ball.speed;
      ball.dy = ball.speed;
    }else{
      ball.x = paddle.x - 165;
      ball.dx = ball.speed;
      ball.dy = ball.speed;
    }

@Fer-Win
Copy link

Fer-Win commented Dec 9, 2023

Have used the code of the game for a project and since we are using NextJS, have converted the code to a NextJs component and also added the implementation of scoring to this.
Thanks to @straker
Here's the code 👇👇

'use client'
import React, { useEffect, useRef, useState } from 'react';
interface BreakoutGameProps {
  scoreUpdate: (newValue: number) => void;
}
const BreakoutGame: React.FC<BreakoutGameProps> = ({scoreUpdate}) => {
  const canvasRef = useRef<HTMLCanvasElement>(null);
  const scoreRef = useRef(0);
  const [gameOver,setGameOver] = useState(false);
  const [hitSpace,setHitSpace] = useState(false);

  useEffect(() => {
    
    const canvas = canvasRef.current;
    const context = canvas?.getContext('2d');

    if (!canvas || !context) return;

    const level1 = [
      [],
      [],
      [],
      [],
      [],
      [],
      ['R','R','R','R','R','R','R','R','R','R','R','R','R','R'],
      ['R','R','R','R','R','R','R','R','R','R','R','R','R','R'],
      ['O','O','O','O','O','O','O','O','O','O','O','O','O','O'],
      ['O','O','O','O','O','O','O','O','O','O','O','O','O','O'],
      ['G','G','G','G','G','G','G','G','G','G','G','G','G','G'],
      ['G','G','G','G','G','G','G','G','G','G','G','G','G','G'],
      ['Y','Y','Y','Y','Y','Y','Y','Y','Y','Y','Y','Y','Y','Y'],
      ['Y','Y','Y','Y','Y','Y','Y','Y','Y','Y','Y','Y','Y','Y']
    ];
    
    // create a mapping between color short code (R, O, G, Y) and color name
    const colorMap = {
      'R': 'red',
      'O': 'orange',
      'G': 'green',
      'Y': 'yellow'
    };
    
    // use a 2px gap between each brick
    const brickGap = 2;
    const brickWidth = 25;
    const brickHeight = 12;
    
    // the wall width takes up the remaining space of the canvas width. with 14 bricks
    // and 13 2px gaps between them, that's: 400 - (14 * 25 + 2 * 13) = 24px. so each
    // wall will be 12px
    const wallSize = 12;
    const bricks:any[] = [];
    
    // create the level by looping over each row and column in the level1 array
    // and creating an object with the bricks position (x, y) and color
    for (let row = 0; row < level1.length; row++) {
      for (let col = 0; col < level1[row].length; col++) {
        const colorCode = level1[row][col];
    
        bricks.push({
          x: wallSize + (brickWidth + brickGap) * col,
          y: wallSize + (brickHeight + brickGap) * row,
          color: colorMap[colorCode],
          width: brickWidth,
          height: brickHeight
        });
      }
    }
    
    const paddle = {
      // place the paddle horizontally in the middle of the screen
      x: canvas.width / 2 - brickWidth / 2,
      y: 440,
      width: brickWidth,
      height: brickHeight,
    
      // paddle x velocity
      dx: 0
    };
    
    const ball = {
      x: 130,
      y: 260,
      width: 5,
      height: 5,
      color: 'black',
      // how fast the ball should go in either the x or y direction
      speed: 2,
    
      // ball velocity
      dx: 0,
      dy: 0
    };
    
    // check for collision between two objects using axis-aligned bounding box (AABB)
    // @see https://developer.mozilla.org/en-US/docs/Games/Techniques/2D_collision_detection
    function collides(obj1, obj2) {
      return obj1.x < obj2.x + obj2.width &&
             obj1.x + obj1.width > obj2.x &&
             obj1.y < obj2.y + obj2.height &&
             obj1.y + obj1.height > obj2.y;
    }

  const loop=()=> {
      requestAnimationFrame(loop);
      context.clearRect(0, 0, canvas.width, canvas.height);
      // move paddle by its velocity
      paddle.x += paddle.dx;

      // prevent paddle from going through walls
      if (paddle.x < wallSize) {
        paddle.x = wallSize;
      } else if (paddle.x + brickWidth > canvas.width - wallSize) {
        paddle.x = canvas.width - wallSize - brickWidth;
      }

      // move ball by its velocity
      ball.x += ball.dx;
      ball.y += ball.dy;

      // prevent ball from going through walls by changing its velocity
      // left & right walls
      if (ball.x < wallSize) {
        ball.x = wallSize;
        ball.dx *= -1;
      } else if (ball.x + ball.width > canvas.width - wallSize) {
        ball.x = canvas.width - wallSize - ball.width;
        ball.dx *= -1;
      }
      // top wall
      if (ball.y < wallSize) {
        ball.y = wallSize;
        ball.dy *= -1;
      }

      // reset ball if it goes below the screen
      if (ball.y > canvas.height) {
        ball.x = 130;
        ball.y = 260;
        ball.dx = 0;
        ball.dy = 0;
        setGameOver(true);
      }

      // check to see if ball collides with paddle. if they do change y velocity
      if (collides(ball, paddle)) {
        ball.dy *= -1;

        // move ball above the paddle otherwise the collision will happen again
        // in the next frame
        ball.y = paddle.y - ball.height;
      }

      // check to see if ball collides with a brick. if it does, remove the brick
      // and change the ball velocity based on the side the brick was hit on
      for (let i = 0; i < bricks.length; i++) {
        const brick = bricks[i];

        if (collides(ball, brick)) {
          // remove brick from the bricks array
          bricks.splice(i, 1);
          console.log("Score ++");
          scoreRef.current = scoreRef.current + 1;
          scoreUpdate(scoreRef.current);
         // setScore(count+1);

          // ball is above or below the brick, change y velocity
          // account for the ball's speed since it will be inside the brick when it
          // collides
          if (ball.y + ball.height - ball.speed <= brick.y ||
              ball.y >= brick.y + brick.height - ball.speed) {
            ball.dy *= -1;
          }
          // ball is on either side of the brick, change x velocity
          else {
            ball.dx *= -1;
          }

          break;
        }
      }

      // draw walls
      context.fillStyle = 'lightgrey';
      context.fillRect(0, 0, canvas.width, wallSize);
      context.fillRect(0, 0, wallSize, canvas.height);
      context.fillRect(canvas.width - wallSize, 0, wallSize, canvas.height);

      // draw ball if it's moving
      if (ball.dx || ball.dy) {
        context.fillStyle = ball.color;
        context.fillRect(ball.x, ball.y, ball.width, ball.height);
      }

      // draw bricks
      bricks.forEach(function(brick) {
        context.fillStyle = brick.color;
        context.fillRect(brick.x, brick.y, brick.width, brick.height);
      });

      // draw paddle
      context.fillStyle = 'cyan';
      context.fillRect(paddle.x, paddle.y, paddle.width, paddle.height);
    }

    // Listen to keyboard events...
    const handleKeyDown = (e) => {
      // left arrow key
      if (e.which === 37) {
        paddle.dx = -3;
      }
      // right arrow key
      else if (e.which === 39) {
        paddle.dx = 3;
      }

      // space key
      // if the ball is not moving, we can launch the ball using the space key. ball
      // will move towards the bottom right to start
      if (ball.dx === 0 && ball.dy === 0 && e.which === 32) {
        setHitSpace(true);
        ball.dx = ball.speed;
        ball.dy = ball.speed;
      }
    };

    const handleKeyUp = (e) => {
      if (e.which === 37 || e.which === 39) {
        paddle.dx = 0;
      }
    };

    document.addEventListener('keydown', handleKeyDown);
    document.addEventListener('keyup', handleKeyUp);

    // Start the game
    requestAnimationFrame(loop);
   

    return () => {
      
      document.removeEventListener('keydown', handleKeyDown);
      document.removeEventListener('keyup', handleKeyUp);
    };
  }, [gameOver]); 
   
  const handleRestart =()=>{
    setGameOver(false);
    scoreRef.current = 0;
    scoreUpdate(scoreRef.current);
    setHitSpace(false);
    
  }

  return (
    <>
    <div className={`text-white ${hitSpace? 'hidden':'' }`}>Press Space to Start</div>
    <canvas
      className={`mx-auto mt-4 border-white bg-[#f8f8f8] ${gameOver ? 'hidden' : ''}`}

      width="400"
      height="500"
      ref={canvasRef}

    ></canvas>
{gameOver && (
                <div className={`flex flex-col items-center justify-center w-full h-full text-black text-3xl `}>
                    <div className='p-5 text-white'>Game Over. </div>
                    <button onClick={handleRestart} className='bg-blue-600 rounded-md  p-2 text-xl text-white'>Restart</button>
                </div>
            )}
    </>
  );
};

export default BreakoutGame;

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment