We’ll begin by creating the game area using the HTML canvas element. The element provides access to the Canvas API, which allows us to draw graphics on a webpage.
In your HTML add the code below.
1 |
|
2 |
|
3 |
Bounce the ball with the paddle. Use your mouse to move the paddle and |
4 |
break the bricks. |
5 |
|
6 |
|
7 |
|
8 |
|
9 |
|
The canvas is the area where we will draw our game graphics; it has a width of 640 pixels and a height of 320 pixels. Setting the size in your HTML rather than in CSS ensures consistency when drawing. If you set the size with CSS only, it may lead to distortion when drawing graphics.
We will have a few styles to make the game more appealing. Add the following CSS styles.
1 |
* { |
2 |
padding: 0; |
3 |
margin: 0; |
4 |
}
|
5 |
@import url("https://fonts.googleapis.com/css2?family=DM+Mono:ital,wght@0,300;0,400;0,500;1,300;1,400;1,500&display=swap"); |
6 |
|
7 |
body { |
8 |
display: flex; |
9 |
align-items: center; |
10 |
justify-content: center; |
11 |
flex-direction: column; |
12 |
gap: 20px; |
13 |
height: 100vh; |
14 |
font-family: "DM Mono", monospace; |
15 |
background-color: #2c3e50; |
16 |
text-align: center; |
17 |
}
|
18 |
canvas { |
19 |
background: #fff; |
20 |
border: 2px solid #34495e; |
21 |
box-shadow: 0 4px 8px rgba(0, 0, 0, 0.2); |
22 |
}
|
23 |
|
24 |
.instructions { |
25 |
font-size: 1.3rem; |
26 |
color: #fff; |
27 |
max-width: 600px; |
28 |
margin-bottom: 20px; |
29 |
}
|
30 |
|
31 |
#start_btn { |
32 |
margin-top: 20px; |
33 |
padding: 10px 20px; |
34 |
font-size: 16px; |
35 |
color: #fff; |
36 |
background-color: #0c89dd; |
37 |
border: none; |
38 |
border-radius: 5px; |
39 |
cursor: pointer; |
40 |
font-weight: 500; |
41 |
font-family: "DM Mono", monospace; |
42 |
}
|
The first step is to get the reference to the canvas using the getElementById()
method. Add the code below.
1 |
const canvas = document.getElementById("myCanvas"); |
2 |
|
Next, get the context. The context (ctx
) is where our graphics will be rendered on the canvas.
1 |
const ctx = canvas.getContext("2d"); |
A paddle is the horizontal tool at the bottom of the canvas used to bounce off the ball.
Define the height and width of the paddle and its start position on the canvas.
1 |
const paddleHeight = 10; |
2 |
const paddleWidth = 80; |
3 |
|
4 |
let paddleStart = (canvas.width - paddleWidth) / 2; |
paddleStartStart
is the starting point on the X-axis of the canvas where the paddle will start. The position is calculated by half of the canvas width, but we have to account for the space occupied by the paddle, hence the formula paddleStart = (canvas.width - paddleWidth) / 2
;
The Canvas API uses the fillRect()
method to draw a rectangle using the following formula:
1 |
fillRect(x, y, width, height) |
where :
Create a function named drawPaddle()
and draw the paddle using the specified dimensions. The fillStyle
property sets the color of the rectangle.
1 |
function drawPaddle() { |
2 |
ctx.fillStyle = "#0095DD"; |
3 |
ctx.fillRect( |
4 |
paddleStart, |
5 |
canvas.height - paddleHeight, |
6 |
paddleWidth, |
7 |
paddleHeight
|
8 |
);
|
9 |
}
|
Call the drawPaddle()
function to draw the paddle on the canvas.
The next step is to add the ability to move the paddle when a user moves a mouse left and right. Create a mouse-event listener and hook it to a function called movePaddle()
.
1 |
document.addEventListener("mousemove", movePaddle, false); |
The movePaddle()
function will be triggered anytime the user moves the mouse on the canvas. The false argument means that the event will be handled during the bubbling phase, which is the default behavior.
Create the movePaddle()
function and add the code below.
1 |
function movePaddle(e) { |
2 |
let mouseX = e.clientX - canvas.offsetLeft; |
3 |
if (mouseX > 0 && mouseX < canvas.width) { |
4 |
paddleStart = mouseX - paddleWidth / 2; |
5 |
}
|
6 |
}
|
In the code above, we get the X position of the mouse relative to the canvas. The if statement ensures that the paddle doesn’t move beyond the canvas’s bounds. If the condition is met, we will update the paddle’s startX
position which will in turn redraw the paddle to the new position of the mouse.
At the moment, the paddle doesn’t move because we are only calling drawPaddle()
once. We need to continuously update the paddle’s position based on new mouse movements. To do this, we will implement an animation loop that repeatedly calls drawPaddle()
, which will then update the paddle’s position.
Since we will have the same scenario when we draw the ball, let’s create a function called gameLoop which will handle the animation loop.
1 |
function gameLoop() { |
2 |
drawPaddle(); |
3 |
requestAnimationFrame(gameLoop); |
4 |
}
|
requestAnimationFrame()
is a function that helps to create smooth animations. It takes a callback function and executes the callback before the next repaint of the screen. In our case, we are using it to draw the paddle on every repaint.
Call the gameLoop()
function so as to effect the movements:
Update the function to clear the canvas each time the paddle is redrawn in its new position. Add this line of code at the beginning of the function.
1 |
ctx.clearRect(0, 0, canvas.width, canvas.height); |
When you move the mouse over the canvas, the paddle doesn’t get drawn as expected, and you see something like this:
To solve this issue, we need to clear the canvas on every repaint. Update the code as follows:
1 |
function gameLoop() { |
2 |
ctx.clearRect(0, 0, canvas.width, canvas.height); |
3 |
drawPaddle(); |
4 |
requestAnimationFrame(gameLoop); |
5 |
}
|
The clearRect()
will clear the contents of the canvas. Clearing the canvas is essential to ensure that any previously drawn drawings are removed each time the animation loop runs; this creates an illusion of movement.
In a breakout game, the ball is used to hit bricks and the user ensures that it bounces back once it hits a brick. Since we don’t have any bricks, yet, we will bounce the ball off the walls of the canvas. Let’s start by drawing the ball.
The first step is to define some variables as shown below.
1 |
let startX = canvas.width / 2; |
2 |
let startY = canvas.height - 100; |
startX
and startY
are the x and y coordinates of the original ball position.
To draw the ball, create a function named drawBall()
which looks like this:
1 |
function drawBall() { |
2 |
ctx.beginPath(); |
3 |
ctx.rect(startX, startY, 6, 6); |
4 |
ctx.fillStyle = "blue"; |
5 |
ctx.fill(); |
6 |
ctx.closePath(); |
7 |
|
8 |
}
|
In the code above, we are drawing a small blue rectangle with a width and height of 6 pixels at the specified position. Call the drawBall()
function inside the gameLoop()
function to ensure the ball is also updated on every repaint.
1 |
function gameLoop() { |
2 |
drawPaddle(); |
3 |
requestAnimationFrame(gameLoop); |
4 |
}
|
5 |
|
The next step is to simulate ball movements by updating its position incrementally; this will create an illusion of movement.
Set the incremental values for both the x and y directions.
1 |
let deltaX = 2; |
2 |
let deltaY = 2; |
To ensure we understand how the ball moves, we have the following diagram which illustrates how the canvas is measured.
On each repaint, update the position of the ball by incrementing the startX
and startY
coordinates with the values of deltaX
and deltaY
; This will make the ball move in a diagonal position
1 |
function drawBall() { |
2 |
ctx.beginPath(); |
3 |
ctx.rect(startX, startY, 6, 6); |
4 |
ctx.fillStyle = "blue"; |
5 |
ctx.fill(); |
6 |
ctx.closePath(); |
7 |
|
8 |
startX += deltaX; |
9 |
startY += deltaY; |
10 |
}
|
Currently, the ball moves in only one direction and disappears out of the canvas. Let’s ensure it changes direction when it hits the bottom of the canvas. Update the code as shown below.
1 |
function drawBall() { |
2 |
ctx.beginPath(); |
3 |
ctx.rect(startX, startY, 6, 6); |
4 |
ctx.fillStyle = "blue"; |
5 |
ctx.fill(); |
6 |
ctx.closePath(); |
7 |
startX += deltaX; |
8 |
startY += deltaY; |
9 |
|
10 |
if (startY + 6 >= canvas.height) { |
11 |
deltaY = -deltaY; |
12 |
}
|
13 |
}
|
The code startY + 6 >= canvas.height
checks if the current position of the ball has reached or exceeded the bottom edge of the canvas. If this condition is true, deltaY = -deltaY
reverses the vertical movement of the ball, and it simulates a bouncing effect at the bottom of the canvas.
If the ball’s x position moves beyond 0 (the left edge of the canvas), reverse its horizontal direction. Similarly, if the ball moves beyond the right edge i.e., exceeds the canvas width, reverse its horizontal direction.
1 |
function drawBall() { |
2 |
ctx.beginPath(); |
3 |
ctx.rect(startX, startY, 6, 6); |
4 |
ctx.fillStyle = "blue"; |
5 |
ctx.fill(); |
6 |
ctx.closePath(); |
7 |
startX += deltaX; |
8 |
startY += deltaY; |
9 |
|
10 |
if (startY + 6 >= canvas.height) { |
11 |
deltaY = -deltaY; |
12 |
}
|
13 |
if (startX < 0 || startX + 6 > canvas.width) { |
14 |
deltaX = -deltaX; |
15 |
}
|
16 |
}
|
From the canvas illustration, we know that the top-left and top-right of the canvas are (0, 0) and (640, 0), respectively. To ensure the ball doesn’t move beyond the top edge, we use the condition startY < 0.
startY < 0
checks if the ball’s y position is above the top edge. If the condition is true, the ball’s vertical direction is reversed and the ball reverses its direction.
1 |
function drawBall() { |
2 |
ctx.beginPath(); |
3 |
ctx.rect(startX, startY, 6, 6); |
4 |
ctx.fillStyle = "blue"; |
5 |
ctx.fill(); |
6 |
ctx.closePath(); |
7 |
startX += deltaX; |
8 |
startY += deltaY; |
9 |
|
10 |
if (startY + 6 >= canvas.height) { |
11 |
deltaY = -deltaY; |
12 |
}
|
13 |
if (startX < 0 || startX + 6 > canvas.width) { |
14 |
deltaX = -deltaX; |
15 |
}
|
16 |
if (startY < 0) { |
17 |
deltaY = -deltaY; |
18 |
}
|
19 |
}
|
So far, we have implemented ball movement, collision detection, and paddle movement. The next step is to create the logic for detecting collisions between the ball and the paddle, ensuring that the ball bounces off the paddle rather than the bottom of the canvas
For this logic, we want to check if the ball has reached the paddle position; if it touches the paddle, the ball’s direction will be reversed. Create a function called checkBallPaddleCollision()
and add the code below.
1 |
function checkBallPaddleCollision() { |
2 |
if ( |
3 |
startY + 6 >= canvas.height - paddleHeight && |
4 |
startX + 6 > paddleStart && |
5 |
startX < paddleStart + paddleWidth |
6 |
) { |
7 |
deltaY = -deltaY; |
8 |
}
|
9 |
}
|
Call the function inside the gameLoop()
function.
1 |
function gameLoop() { |
2 |
ctx.clearRect(0, 0, canvas.width, canvas.height); |
3 |
drawBall(); |
4 |
drawPaddle(); |
5 |
checkBallPaddleCollision() |
6 |
requestAnimationFrame(gameLoop); |
7 |
}
|
Since the canvas is being cleared on every frame (when the paddle or ball is redrawn in a new position), we also need to redraw the bricks on each frame. To do this, we’ll store the brick dimensions in an array called bricks.
This array will store the dimensions of all the bricks needed. We will then create two functions named initializeBricks
and DrawBricks()
. The first function will store brick dimensions for all the rows and bricks, while the DrawBricks
function will be called on every repaint to draw the remaining blocks.
As you can see from the demo, we have 2 rows of bricks with each row containing 7 bricks. Set the following dimensions
1 |
const brickWidth = 75; |
2 |
const brickHeight = 20; |
3 |
const brickPadding = 10; |
4 |
const brickOffsetTop = 40; |
5 |
const brickOffsetLeft = 30; |
6 |
const numberOfBricks = 7 |
brickWidth
: is thewidth of each brick
brickWidth
: is the height of each brick
brickPadding
:This value ensures even spacing between the bricks.
brickOffsetTop
: distance between the top of the canvas and the first row of bricks
brickOffsetLeft
: the space between the bricks and the left side of the canvas
numberOfBricks
:defines how many bricks are in each row.
Suppose we are only interested in drawing a single row of bricks, we will create a for loop that runs for the number of bricks. For each brick we will calculate the start point by defining the x coordinate and y coordinates, these values will determine where each brick starts on the canvas.
Create the initializeBricks()
function and add a for loop as shown below
1 |
function initializeBricks() { |
2 |
for (let i = 0; i < numberOfBricks; i++) { |
3 |
const brickX = brickOffsetLeft + i * (brickWidth + brickPadding); |
4 |
const brickY = brickOffsetLeft |
5 |
|
6 |
}
|
7 |
}
|
brickX
will determine the horizontal position of each brick, for example, the brick at index 0 will be placed at 30 pixels on the x-axis and 30 pixels on the y-axis. For index 1, brickX accounts for the distance from the left of the canvas, plus the width of the previous brick and its padding.
For index 2, brickX accounts for the distance from the left of the canvas plus the width of the previous blocks (2 * blockWidth)
plus the padding of the previous blocks (2 * blockRowPadding).
brickY
is the vertical position of the bricks; this value doesn’t change since we are drawing a single row. brickX
will determine the position of the brick on the canvas , so for example the brick at index 0 will be placed at 30 on the x axis and 30 on the Y axis since the distance from the top doesn’t change.
The loop will continue until all the bricks have been calculated.
To add other row of bricks, we will adjust the brickY
coordinate to account for the height of the previous row and the padding between rows. To accomplish this, let’s set a custom number for the rows.
We will then create an outer loop that will loop through each row and calculate the value of brickY, (this is the distance from the top of the canvas to each brick).
1 |
function initializeBricks() { |
2 |
for (let row = 0; row < no0fRows; row++) { |
3 |
const brickY = brickOffsetLeft + row * (brickHeight + brickPadding); |
4 |
|
5 |
for (let i = 0; i < numberOfBricks; i++) { |
6 |
const brickX = brickOffsetLeft + i * (brickWidth + brickPadding); |
7 |
|
8 |
}
|
9 |
}
|
10 |
}
|
We now have the brick dimensions, create an object for each brick, the object will contain the data below.
x
: The x-coordinate of the upper-left corner of the brick
y
:The y-coordinate of the upper-left corner of the brick
width
:The width of the brick
height
: The height of the brick
color
: The color of the brick
In the inner loop statement, rather than drawing the bricks, we will push the dimensions of each brick to the array.
1 |
function initializeBricks() { |
2 |
|
3 |
for (let row = 0; row < no0fRows; row++) { |
4 |
const brickY = brickOffsetLeft + row * (brickHeight + brickPadding); |
5 |
|
6 |
for (let i = 0; i < numberOfBricks; i++) { |
7 |
const brickX = brickOffsetLeft + i * (brickWidth + brickPadding); |
8 |
bricks.push({ |
9 |
x: brickX, |
10 |
y: brickY, |
11 |
width: brickWidth, |
12 |
height: brickHeight, |
13 |
color: "green", |
14 |
});
|
15 |
}
|
16 |
}
|
17 |
}
|
18 |
initializeBricks(); |
If we want to assign each block a random color, we can also do so. Define an array of colors.
1 |
const colors = ["#0095DD", "#4CAF50", "#FF5733", "#FFC300"]; |
Update the fillStyle
property as follows:
1 |
bricks.push({ |
2 |
x: brickX, |
3 |
y: brickY, |
4 |
width: brickWidth, |
5 |
height: brickHeight, |
6 |
color: colors[i % colors.length], |
7 |
});
|
To see the data in table format, you can use console.table
which will display the data in tabular format.
The data looks like this:
We now have all the dimensions of the bricks, let’s draw them on the canvas. Create a function called drawBricks()
and add the code below:
1 |
function drawBricks() { |
2 |
for (let i = 0; i < bricks.length; i++) { |
3 |
const brick = bricks[i]; |
4 |
ctx.beginPath(); |
5 |
ctx.rect(brick.x, brick.y, brick.width, brick.height); |
6 |
ctx.fillStyle = brick.color; |
7 |
ctx.fill(); |
8 |
ctx.closePath(); |
9 |
}
|
10 |
}
|
In the drawBricks()
function, we loop through the bricks array, and for each brick object, we draw a rectangle representing the brick with the specified color.
1 |
function gameLoop() { |
2 |
ctx.clearRect(0, 0, canvas.width, canvas.height); |
3 |
drawBall(); |
4 |
drawPaddle(); |
5 |
checkBallPaddleCollision(); |
6 |
drawBricks(); |
7 |
|
8 |
requestAnimationFrame(gameLoop); |
9 |
}
|
Our app now looks like this:
As you can see from the diagram above, the ball can move past the bricks, which is not ideal! We need to ensure that the ball bounces back (reverses direction) when it hits any brick.
The last step in the collision functionality is to check for brick and ball collision detection. Create a function called checkBrickBallCollision()
.
1 |
function checkBrickBallCollision() { |
2 |
|
3 |
}
|
In this function, we want to loop through the bricks array and check if the ball position is touching any brick. If any collision is detected, the ball’s direction is reversed and the collided brick is removed from the array.
1 |
function checkBrickBallCollision() { |
2 |
for (let i = 0; i < bricks.length; i++) { |
3 |
const brick = bricks[i]; |
4 |
if ( |
5 |
startX < brick.x + brick.width && |
6 |
startX + 6 > brick.x && |
7 |
startY < brick.y + brick.height && |
8 |
startY + 6 > brick.y |
9 |
) { |
10 |
deltaY = -deltaY; |
11 |
bricks.splice(i, 1); |
12 |
break; |
13 |
}
|
14 |
}
|
15 |
}
|
The Breakout game isn’t much fun to play without a score. We need to implement a scoring system in which a player will be awarded points every time they hit a brick by bouncing the ball off the paddle. Create a variable called score
and initialize it to 0.
Create a function called UpdateSCore()
and add the code below that adds the score value at the top left corner of the canvas.
1 |
function updateScore() { |
2 |
ctx.font = "16px Arial"; |
3 |
ctx.fillText("Score: " + score, 10, 20); |
4 |
}
|
Call the function in the gameLoop()
function.
1 |
function gameLoop() { |
2 |
ctx.clearRect(0, 0, canvas.width, canvas.height); |
3 |
updateScore(); |
4 |
drawBricks(); |
5 |
drawPaddle(); |
6 |
drawBall(); |
7 |
|
8 |
checkBallPaddleCollision(); |
9 |
checkBrickBallCollision(); |
10 |
|
11 |
}
|
The score should be updated every time the ball hits a brick. Update the checkBrickBallCollision()
to include the functionality.
1 |
function checkBrickBallCollision() { |
2 |
for (let i = 0; i < bricks.length; i++) { |
3 |
const brick = bricks[i]; |
4 |
|
5 |
if ( |
6 |
startX < brick.x + brick.width && |
7 |
startX + 6 > brick.x && |
8 |
startY < brick.y + brick.height && |
9 |
startY + 6 > brick.y |
10 |
) { |
11 |
deltaY = -deltaY; |
12 |
bricks.splice(i, 1); |
13 |
//update points
|
14 |
score += 10; |
15 |
break; |
16 |
}
|
17 |
}
|
18 |
}
|
The final step is to add win logic. To win the game, the player must hit all the bricks without missing the paddle. If the ball misses the paddle, the game should reset, and a notification should be displayed. Update the if statement in the drawBall()
function where we check if the ball hits the bottom of the canvas.
1 |
function drawBall() { |
2 |
// the rest of the code
|
3 |
}
|
4 |
if (startY + 6 >= canvas.height) { |
5 |
deltaY = -deltaY; |
6 |
alert("Try Again"); |
7 |
|
8 |
}
|
9 |
}
|
Create a variable gameWon
that will keep track of whether the player has won the game or not.
In the gameLoop()
function, add an if statement to check if all the bricks have been hit by the ball. If so, set the gameWon
variable to true and display a notification. Additionally, update the requestAnimationFrame(gameLoop)
call to ensure it continues running as long as the game has not been won.
1 |
function gameLoop() { |
2 |
if (bricks.length === 0 && !gameWon) { |
3 |
gameWon = true; |
4 |
alert("You Won: Play Again"); |
5 |
resetGame(); |
6 |
}
|
7 |
ctx.clearRect(0, 0, canvas.width, canvas.height); |
8 |
updateScore(); |
9 |
drawBricks(); |
10 |
drawPaddle(); |
11 |
drawBall(); |
12 |
|
13 |
checkBallPaddleCollision(); |
14 |
checkBrickBallCollision(); |
15 |
if (!gameWon) { |
16 |
requestAnimationFrame(gameLoop); |
17 |
}
|
18 |
}
|
The resetGame()
function looks like this;
1 |
function resetGame() { |
2 |
score = 0; |
3 |
startX = canvas.width / 2; |
4 |
startY = canvas.height - 100; |
5 |
deltaX = -2; |
6 |
deltaY = -2; |
7 |
initializeBricks(); |
8 |
|
9 |
}
|
In this function, we are resetting the score to 0, repositioning the ball to its original starting position, initializing the brick layout, and reversing the ball’s direction; this ensures the game is reset to allow the player to try again.
Finally, in the initializeBricks()
function, reset the brick array’s length to 0 to ensure no leftover bricks from the previous game state.
1 |
function initializeBricks() { |
2 |
bricks.length = 0; |
3 |
// the rest of the code
|
4 |
}
|
The final step is to use a button to start the game so that the game doesn’t automatically start when you open the app. Get a reference to the start game button and add a click event listener that calls the gameLoop()
function.
1 |
document
|
2 |
.getElementById("start_btn") |
3 |
.addEventListener("click", function () { |
4 |
gameLoop(); |
5 |
});
|
Here is a reminder of what we’ve built!
That was quite a journey! We’ve tackled a range of concepts in this game, from drawing a simple canvas to implementing moving objects, collision detection, and managing game states. Now you have a fully functioning Breakout game. To take it to the next level, consider adding features like high score tracking and sound effects to make the game even more engaging.