Breakout game in JavaScript (with requestAnimationFrame())

Muhammad Saqib Ilyas
29 min readMay 2, 2024

--

I wrote a blog earlier to build a clone of the classic Breakout game. Later, I discovered several things that could be improved. Rather than edit the original blog, I feel that a new blog is in order. The older one is useful in its own right.

Let’s create a clone of the classic console game Breakout. The game will have three rows with six blocks in each. When the ball hits a block, it pops and disappears.

A preview of the game

Here’s what we’ll need to do:

  • Create a container for the blocks
  • Create the blocks and render them inside the container
  • Maintain the coordinates of each block so that we can check for collision
  • Create and render a ball
  • Set the ball in motion
  • If the ball hits the floor, stop the game
  • Detect collisions and rebound the ball. Remove a block if necessary
  • Display a bat and enable it to move on keystrokes

The starter HTML and CSS

Let’s start with the basic HTML in a file named index.html as shown below:

<!DOCTYPE HTML>
<HTML>
<head>
<title>Breakout</title>
<link rel="stylesheet" href="style.css" />
</head>
<body>
<h2>Breakout</h2>
<div class="grid">
<div class="block"></div>
</div>
<script type="text/javascript" src="app.js"></script>
</body>
</HTML>

We start off with a container div element, with one div element inside it. We give each of these div elements different classes. The block type div element is just for getting our feet wet by displaying one block. We’ll render the blocks programatically later on.

Coming to the CSS, now. I’ll go with blocks 100 pixels wide and 20 pixels in height. If I lay out six of these in a row, with 10 pixels of spacing in between, and around the blocks, we’ll need the game canvas to be 100 x 6 + 7 x 10 = 670 pixels. I’ll set its height to 300 pixels. We’re not creating a responsive design with blocks inside the container element, so we wouldn’t use Flexbox or CSS grid. The blocks will be shot at and will disappear, so a responsive design isn’t what we’re looking for. Instead, we’ll use CSS positioning as follows:

.grid {
width: 670px;
height: 300px;
border: 1px solid black;
position: absolute;
}

.block {
width: 100px;
height: 20px;
background-color: #0303AA;
position: absolute;
left: 10px;
bottom: 10px;
}

We assign a black border to the div element with a class of grid. We set its height and width as mentioned above. We set position: aboslute; on it. We also style the block class with a width of 100 pixels and a height of 20 pixels, and a bluish background color. We set it’s position property to absolute, as well. We, then set it’s left and bottom properties to 10 pixels, each. What’s going on with that?

By saying position: absolute; for the element with block style, we are taking it out of the normal flow of the element placement, which would have placed the inner div near the left top corner of the outer div. The inner div will now be placed relative to the nearest ancestor with a position other than static. In our case, it is the outer div with a style of grid. The left: 10px directive tells the browser to place the block element 10 pixels from the left edge of the element with grid style. Similarly, bottom: 10px; tells the browser to place it 10 pixels from the bottom of the grid element. At this point, this is what you’ll see on the screen:

A block positioned inside the container

Getting cody

We can’t hard-code all the blocks in index.html. We’ll have to create the blocks using JavaScript. So, let’s remove the block element from index.html and try to insert it using JavaScript.

The following code will do the trick:

const grid = document.querySelector('.grid')
const block = document.createElement('div')
block.classList.add('block')
grid.appendChild(block)

We access the grid element using querySelector. We create a new div element using the document.createElement() method. We add the class of block to the new element, and add it as a child to the grid element.

At this point, the page should render the same as the previous illustration, even though we removed the inner div from index.html.

A touch of class

Since we need to render a number of blocks, each at a different position, we’ll create a class for the blocks. I’ll use the old-school way of creating classes in JavaScript.

Before we do that, I’ll get rid of the left and bottom properties from the style specification of the block class, because each block will be positioned differently, programmatically.

Here’s the modified JavaScript code:

const blockWidth = 100;
const blockHeight = 20;
const grid = document.querySelector('.grid')

function Block(left, bottom, width, height) {
this.bottomLeft = {x:left, y: bottom}
this.bottomRight = {x:left + width, y: bottom}
this.topLeft = {x: left, y: bottom + height}
this.topRight = {x: left + width, y: bottom + height}
}
const blocks = [
new Block(10, 270, blockWidth, blockHeight)
]
displayBlocks()
function displayBlocks() {
blocks.forEach( b => {
const block = document.createElement('div')
block.classList.add('block')
block.style.left = b.bottomLeft.x + 'px'
block.style.bottom = b.bottomLeft.y + 'px'
grid.appendChild(block)
})
}

We define variables to store the width and height of the blocks so that given the bottom left position of a block we can calculate the coordinates for all four of its corners.

We define a function named Block, which we’ll use as the constructor the objects. We pass in the bottom, and left positioning values, which we had previously hard-coded in CSS. The function also accepts the width and height of the block. Given these values, we store or calculate the coordinates for all four corners. Note that in our system, the origin is at the bottom left corner of the grid element, the x coordinate increases left to right, whereas the y coordinate increases bottom to top. For each corner, we create an object with x and y values for the coordinates. The following illustration might help wrap your head around the four coordinate calculations.

Coordinates calculation for a block of given width, and height, provided its bottom and left clearance

Having declared the constructor, we define an array to hold the blocks. For now, we create just one, with hard-coded bottom and left parameters. We, then, declare a function to display the blocks using forEach() on the blocks array. Inside the displayBlocks() function, for each block, we create a div element, assign it a class of block, set its left and bottom style values to the x and y values of the bottom left of the Block object. Notice that we concatenate the string “px” to the number stored in the object for these values, since CSS expects a unit specification. Of course, we call this function, otherwise, no blocks would be displayed on the page.

Opening the page at this point will show a single block inside the grid container near the top left corner of the grid.

A row of blocks

Let’s now try to create a row of blocks in JavaScript without hard-coding. Let’s start with the first row. With 10 pixels on either side of each block, a block width of 100 pixels, and block height of 20 pixels, the first row should have bottom left coordinates as shown in the following diagram:

Coordinates of the bottom left corner of the six blocks in the first row

The y-coordinate for all blocks is the same, since they are horizontally aligned. Why is it 270? Because the grid container is 300 pixel in height, we wanted to leave 10 pixels from the top, the block height is 10 pixels, so that takes away a total of 20 + 10 = 30 pixels from the 300 pixels, leaving us 300–30 = 270 pixels.

For the x-coordinates, the first block is 10 pixels apart from the left edge, so that’s 10. How much further do we need to go along the x-axis to get to the left edge of the second blocks? Well, we have to go across the first block’s width, so that’s 100 pixels, and we also have to leave 10 pixels to the right of the first block, so that’s a total of 110 pixels. Since the first block had an x-coordinate of 10, the second block has 10 + 110=120 pixels. Each subsequent block will have its bottom left x coordinate increment by 110 pixels.

If the block numbers go from 0 to 5, left to right, then a general formula for the x-coordinate of the i-th block is: 10 + i * (width + 10). The first 10 is the left margin we left before the first block. Each subsequent block, as we discussed is offset by 110 pixels, or width + 10 pixels, in general. A function to create the first row of blocks is given below:

function createRow() {
for (let i = 0 ; i < 6 ; i++) {
const block = new Block( 10 + i * (blockWidth + 10), 300 - (20 + 10), blockWidth, blockHeight)
blocks.push(block)
}
}

Let’s integrate this with the rest of the code so far and see what we get:

const boardWidth = 670
const boardHeight = 300

const blockWidth = 100;
const blockHeight = 20;
const blockSpace = 10

const columns = 6

const grid = document.querySelector('.grid')

function Block(left, bottom, width, height) {
this.bottomLeft = {x:left, y: bottom}
this.bottomRight = {x:left + width, y: bottom}
this.topLeft = {x: left, y: bottom + height}
this.topRight = {x: left + width, y: bottom + height}
}

const blocks = []

createRow()

displayBlocks()

function displayBlocks() {
blocks.forEach( b => {
const block = document.createElement('div')
block.classList.add('block')
block.style.left = b.bottomLeft.x + 'px'
block.style.bottom = b.bottomLeft.y + 'px'
grid.appendChild(block)
})
}

function creatRow() {
for (let i = 0 ; i < columns ; i++) {
const block = new Block( blockSpace + i * (blockWidth + blockSpace), boardHeight - (blockHeight + blockSpace), blockWidth, blockHeight)
blocks.push(block)
}
}

Rather than use hard-coded constants all over the program, we declared constants at the top, and used those in the loop in the createRow() function. Once you run the above code, you should see the first row of six blocks nicely set out on the screen.

Three rows of blocks

Let’s generalize the above to more than one row. We’ve already mastered creating and displaying one row. The six blocks in all rows are set apart exactly alike, as far as the x coordinate is concerned. The only difference is the y coordinate.

What’s the y coordinate for the bottom left corner of the first row? 300 — (10 + 20) = 270 pixels. What’s the y-coordinate for the bottom left corner of the second row? Well, you come down 10 pixels, since you’re leaving 10 pixels between rows. Then, you come down 20 pixels, which is the block height in the second row. So, you have 270 — (10 + 20). So, each subsequent row, we take away 30 from the y coordinate. If the row numbers go from 1 to 3, the y coordinate of the bottom left corner of the j-th row is 300 — j x 30 pixels.

We can call our createRow() function repeatedly in a loop to render all three rows:

const columns = 6
const rows = 3

function creatRow(row) {
for (let i = 0 ; i < columns ; i++) {
const block = new Block( blockSpace + i * (blockWidth + blockSpace), boardHeight - row * (blockHeight + blockSpace), blockWidth, blockHeight)
blocks.push(block)
}
}

function createRows() {
for (let j = 1 ; j <= rows; j++) {
createRow(j)
}
}

Since the createRow() function needs to display each row at a different offset from the top of the game board, we pass the row number as argument to it, and use it in the calculation of the y coordinate of each block. We define a createRows() function and call createRow() from it in a loop, while passing the current row number.

Here’s the complete JavaScript code so far. If you open the page, you’ll see three rows of six blocks on the screen:

const boardWidth = 670
const boardHeight = 300

const blockWidth = 100;
const blockHeight = 20;
const blockSpace = 10

const columns = 6
const rows = 3

const grid = document.querySelector('.grid')

function Block(left, bottom, width, height) {
this.bottomLeft = {x:left, y: bottom}
this.bottomRight = {x:left + width, y: bottom}
this.topLeft = {x: left, y: bottom + height}
this.topRight = {x: left + width, y: bottom + height}
}
const blocks = [
]

createRows()

displayBlocks()

function creatRow(row) {
for (let i = 0 ; i < columns ; i++) {
const block = new Block( blockSpace + i * (blockWidth + blockSpace), boardHeight - row * (blockHeight + blockSpace), blockWidth, blockHeight)
blocks.push(block)
}
}

function createRows() {
for (let j = 1 ; j <= rows; j++) {
createRow(j)
}
}

The bat

Now, we are going to display the bat that the player can move around. We’ll create it with a div that has the same dimensions as a block. We’ll use a different color. We’ll then position it at the middle of the canvas near the bottom.

Let’s draw out where the bat should be positioned, so that we can calculate its left and bottom offsets. As shown in the following diagram, there are two and a half blocks, and a total of three inter-block spaces to the bat’s left. In case you’re having difficulty understanding why there are three inter-block spacing, here’s how. First, there’s the 10 pixels to the left of the leftmost block, then there’s the 10 pixels between the first and the second block, and finally, the 10 pixels between the second and the third blocks.

Estimating the left spacing of the bat

Since each block is 100px wide, that gives us a total of 250 + 30 = 280px as the left offset/margin. We’ll keep a bottom offset of 10 pixels. Now, on to rendering the bat.

We can reuse the Block class, since the bat appears similar to the blocks, expect for its color. So, we can do a const bat = new Block(280, 10, blockWidth, blockHeight). Let’s now display it on the page. When you start writing const bat = document.createElement(‘div’), followed by bat.classList.add('bat'), you should have a “wait a minute!” moment. Haven’t you written that code somewhere already? Sure, enough, it resides inside the displayBlocks() function.

Can we call that function to display the bat? A better idea: let’s refactor the code to create a displayBlock() function, which you can call for any block, whether it’s the pedal or otherwise. Here’s the refactored code for displayBlocks().

function displayBlocks() {
blocks.forEach( b => displayBlock(b, 'block'))
}

function displayBlock(b, style) {
const block = document.createElement('div')
block.classList.add(style)
block.style.left = b.bottomLeft.x + 'px'
block.style.bottom = b.bottomLeft.y + 'px'
grid.appendChild(block)
}

We pass in the Block object to displayBlock() as well as the style. From the forEach in displayBlocks(), we can call this function. Here’s the complete code so far:

const boardWidth = 670
const boardHeight = 300

const blockWidth = 100;
const blockHeight = 20;
const blockSpace = 10

const columns = 6
const rows = 3

const grid = document.querySelector('.grid')

displayBlock(bat, 'bat')

function Block(left, bottom, width, height) {
this.bottomLeft = {x:left, y: bottom}
this.bottomRight = {x:left + width, y: bottom}
this.topLeft = {x: left, y: bottom + height}
this.topRight = {x: left + width, y: bottom + height}
}

const blocks = []

createRows()

displayBlocks()

function displayBlocks() {
blocks.forEach( b => displayBlock(b, 'block'))
}

function displayBlock(b, style) {
const block = document.createElement('div')
block.classList.add(style)
block.style.left = b.bottomLeft.x + 'px'
block.style.bottom = b.bottomLeft.y + 'px'
grid.appendChild(block)
}

function creatRow(row) {
for (let i = 0 ; i < columns ; i++) {
const block = new Block( blockSpace + i * (blockWidth + blockSpace), boardHeight - row * (blockHeight + blockSpace), blockWidth, blockHeight)
blocks.push(block)
}
}

function createRows() {
for (let j = 1 ; j <= rows; j++) {
createRow(j)
}
}

We also need to define styles for a class named bat as follows:

.block, .bat {
width: 100px;
height: 20px;
background-color: #0303AA;
position: absolute;
left: 10px;
bottom: 10px;
}

.bat {
background-color: #AA0303;
}

We piggy-back most of the style with the specification for the block class because the bat is very similar to the rest of the blocks, except for the background color. We set the background distinctively in the .bat specification. The later specified background color takes precedence, so our problem is solved.

Bat movement

Now, let’s try to implement the bat movement with the arrow keys. As long as the right arrow key is pressed, the bat should keep moving right until, of course, it hits the wall. The same goes for the left movement. So, we’ll need to handle the keydown as well as the keyup events. Let’s start with the following:

document.addEventListener('keydown', handleKeys)
document.addEventListener('keyup', stopBat)

function handleKeys(event) {
const key = event.key
switch(key) {
case 'ArrowLeft':
// Handle left movement
break
case 'ArrowRight':
// Handle right movement
break;
}
}

function stopBat(event) {
switch(event.key) {
case 'ArrowLeft':
case 'ArrowRight':
// Stop bat movement
break;
}
}

We add event listeners to the document object for key down and key up events. In the keydown event listener, we obtain the key pressed, and use a switch-case statement to handle the arrow keys. We have access to the bat thanks to the bat variable, but how do we update its left position?

To achieve bat motion, we must update its left CSS property. As long as the right arrow key is pressed, we must keep increasing the value of the left property. We must do similarly, for the left arrow key. We could use the setInterval() method to invoke a callback every few milliseconds and in that callback, update the left property. At the same time, we should update the coordinates of the corners of the bat object. To do this, we add a setX() method to the Block class. Here’s the code that we might add.

document.addEventListener('keydown', handleKeys)
document.addEventListener('keyup', stopBat)

let batMovement

function handleKeys(event) {
const key = event.key
switch(key) {
case 'ArrowLeft':
batMovement = setInterval(moveBatLeft, 200)
break
case 'ArrowRight':
// Handle right movement
break;
}
}

function stopBat(event) {
switch(event.key) {
case 'ArrowLeft':
case 'ArrowRight':
clearInterval(batMovement)
break;
}
}

Block.prototype.setX = function(x) {
this.bottomLeft.x = x;
this.bottomRight.x = x + this.width
this.topRight.x = x + this.width
this.topLeft.x = x
}

const stepSize = 10

function moveBatLeft() {
bat.setX(bat.bottomLeft.x - stepSize)
const bElement = document.querySelector('.bat');
bElement.style.left = bat.bottomLeft.x + 'px'
}

We define a variable named batMovement to hold the timer object. In the keydown event handler for the left arrow key, we acquire a timer using the setInterval() method and store it in batMovement. This timer is set to invoke a moveBatLeft() function every 200 milliseconds. We add a setX() method to the Block class. In this method, we update the x coordinates for all four corners of the object. We declare a variable named stepSize to hold the number of pixels that the bat should move every 200 milliseconds. We call the setX() method from our timer callback function moveBatLeft(), decrementing the left property’s value by stepSize. But updating the bat object doesn’t visually move the bat on the screen. To do that, we modify the left property of the bat object.

Of course, we need to do some bound checking so that the bat does not go outside of the game board. But, this is a start. We can similarly handle the right key movement. However, using setInterval() for this sort of “repainting” is not a good practice. The recommended approach is to use the requestAnimationFrame() method.

A browser tab repaints its contents approximately 60 times a second. We can request it to run some of our code on each repaint cycle. That’s what the requestAnimationFrame() method does. You invoke it with a callback function and it will call the callback function at the next repaint cycle, passing in the current timestamp. If we want our callback to the called on every repaint cycle, we can call requestAnimationFrame() inside our callback. Based on the timestamp, we can calculate how much time has elapsed since the last repaint. Using this, and a constant bat speed, we can determine how many pixels the bat should move, instead of using a fixed stepSize, and in which direction. That’s why we define the batSpeed variable to hold the bat’s speed of motion, and batvx to hold the bat’s current velocity, which could be equal to batSpeed for motion towards the right, or -batSpeed for motion towards the left. Initially, the bat should be stationary, so it is initialized to 0. Also, when the right or left arrow key is released, it should be reset to 0.

Let’s figure out that callback function, now.

const lastTime = 0
let batvx = 0
const batSpeed = 100

requestAnimationFrame(gameLoop)

function gameLoop(time) {
if(time) {
const deltams = time - lastTime
moveBat(deltams)
lastTime = time
requestAnimationFrame(gameLoop)
}
}

function handleKeys(event) {
const key = event.key
switch(key) {
case 'ArrowLeft':
batvx = -1 * batSpeed
break
case 'ArrowRight':
batvx = batSpeed
break;
}
}

function stopBat(event) {
switch(event.key) {
case 'ArrowLeft':
case 'ArrowRight':
batvx = 0
break;
}
}

Block.prototype.setX = function(x) {
this.bottomLeft.x = x;
this.bottomRight.x = x + this.width
this.topRight.x = x + this.width
this.topLeft.x = x
}

function moveBat(deltams) {
if (batvx === -1 * batSpeed || batvx === batSpeed) {
const distance = batvx / deltams
bat.setX(bat.bottomLeft.x + distance)
const bElement = document.querySelector('.bat');
bElement.style.left = bat.bottomLeft.x + 'px'
}
}

We declare a variable named lastTime to hold the last timestamp, a variable named batvx to hold the bat’s velocity and set it to 0, and a constant named batSpeed and set it to 100. In the keydown handler, we set batvx equal to 100 if the right key is pressed, or to -100 if the left key is pressed. In the keyup event handler, we set batvx to 0.

We declare a gameLoop() function and pass it to the requestAnimationFrame() method as a callback. Now, the browser will call gameLoop() every repaint cycle. The first time it is invoked, lastTime is 0, and the time elapsed since the last repaint equals the value of the argument time. We store the current timestamp (the value of time) in lastTimeStamp so that we can compute the time elapsed till the next repaint. We call a moveBat() function with deltams as the argument, and call requestAnimationFrame() again so that it keeps calling our gameLoop() over and over. The if (time) in gameLoop() ensures that if you directly invoke this function, without passing the time argument, we wouldn’t have any issues.

In moveBat(), we check if the bat needs to move either left or right. We calculate the number of pixels that the bat should move from its previous position, using its previous position and the time elapsed since the last movement. We call setX() to updates the block’s position and update its left property to visually move it on the screen. Now, you should be able to move the bat around left and right on the screen. Adjust the value of batSpeed to tune how fast the bat moves.

The bat overshoots the right and left edges of the board. To control that, we just need to add some more conditions to the bat movement code in moveBat().

const blockSpace = 10

function moveBat(deltams) {
if ((batvx === -1 * batSpeed && bat.bottomLeft.x >= blockSpace) ||
(batvx === batSpeed && bat.bottomLeft.x <= boardWidth - blockWidth - blockSpace)) {
const distance = batvx / deltams
bat.setX(bat.bottomLeft.x + distance)
const bElement = document.querySelector('.bat');
bElement.style.left = bat.bottomLeft.x + 'px'
}
}

We compare the bat’s bottom left corner’s x coordinate against the two extreme values. We want to stop the bat 10 pixels before the left or right edge of the board. We declare a blockSpace variable and initialize it to 10. We extend the if conditions with the logical AND operator to make sure that the left property remains between two extremes such that there is at least 10 pixels of spacing to the right or left of the bat. Setting the condition for the minimum spacing on the left is straightforward. We compare bat.bottomLeft.x against blockSpace. For the extreme right position, there must be at least blockSpace pixels to the right of the bat. Since the board width is boardWidth pixels wide, and the bat is batWidth pixels wide, at this extreme, the left edge of the bat must be at boardWidth — blockSpace-blockWidth pixels. The rest of the code is the same. Now, that bat will move left and right, but not go beyond the game board.

While we’re here, let’s also add a setY() function to the Block class.

Block.prototype.setY = function(y) {
this.bottomLeft.y = y;
this.topLeft.y = y + this.height;
this.bottomRight.y = y;
this.topRight.y = y + this.height;
}

Show me the ball

Now, let’s add the ball to mix. We’ll reuse the Block class and create a new CSS class for the ball. Here’s the JavaScript:

const ballX = 320
const ballY = 102
const ballWidth = 10
const ballHeight = 10

const ball = new Block(ballX, ballY, ballWidth, ballHeight)
displayBlock(ball, 'ball')

and here’s the CSS:

.ball {
width: 10px;
height: 10px;
background-color: #03AA03;
position: absolute;
}

Now, you’ll see the ball on the screen, like the following:

If you want to make the ball appear round, add a border-radius: 50%; to the ball style in CSS.

Let’s get the ball rolling

Let’s get the ball to move diagonally towards the top right. Here’s the JavaScript code, which should be obviously mostly by now:

const ballSpeed = 50
let ballvx = 10
let ballvy = 10

function gameLoop(time) {
if(time) {
const deltams = time - lastTime
moveBall(deltams)
moveBat(deltams)
lastTime = time
requestAnimationFrame(gameLoop)
}
}

function moveBall(deltams) {
const dx = ballvx / deltams
const dy = ballvy / deltams
ball.setX(ball.bottomLeft.x + dx)
ball.setY(ball.bottomLeft.y + dy)
const ballElement = document.querySelector('.ball')
ballElement.style.left = ball.bottomLeft.x + 'px'
ballElement.style.bottom = ball.bottomLeft.y + 'px'
}

We declare a constant for the ball speed, and two variables to hold the ball’s velocity along the x and y axes. We invoke a movelBall() function from the gameLoop() function. Inside moveBall(), we update the ball object bottom left coordinates. We query the ball DOM element, and update its position.

Since ballvx and ballvy are both positive, if you run this program, you should see the ball move in a northeast direction and eventually disappear.

Detecting a collision

Now, on to detecting the collision between the ball and a wall, which should result in either a rebound, or in case of collision with the bottom wall (or floor), game over.

The ball has collided with the left wall if its left bottom (or top) corner’s x coordinate is equal to the wall’s x coordinate. After the collision, it will change its direction. The following illustration shows two ways in which the ball may collide with the left wall, and the corresponding reactions. Similar cases exist for the other two walls.

Collisions with the left wall

If the ball’s y coordinate was increasing before the collision, it continues to increase. Likewise, if the ball’s y coordinate was decreasing before the collision, it continues to decrease. The x coordinate on the other hand changes direction. If the x coordinate were increases (when the ball was moving upwards towards the right), it starts to decrease, after the collision, and vice versa.

let gameOver = false
const gameover = document.getElementById('gameover')

function moveBall(deltams) {
checkCollision()
const dx = ballvx / deltams
const dy = ballvy / deltams
ball.setX(ball.bottomLeft.x + dx)
ball.setY(ball.bottomLeft.y + dy)
const ballElement = document.querySelector('.ball')
ballElement.style.left = ball.bottomLeft.x + 'px'
ballElement.style.bottom = ball.bottomLeft.y + 'px'
}

function checkCollision() {
reboundFromBat()
reboundFromFrame()
reboundFromBlock()
}

function reboundFromFrame() {
// Check bounds of the play board
// Did we hit the right or left wall
if (ball.topRight.x >= boardWidth || ball.topLeft.x <= 0) {
ballvx = -1 * ballvx
}
// Did we hit the top wall
if (ball.topRight.y >= boardHeight) {
ballvy = -1 * ballvy
}
// Did we hit the bottom wall
else if (ball.bottomRight.y <= 0) {
stop()
gameover.innerHTML = " (GAME OVER)"
}
}

function stop() {
gameOver = true
document.removeEventListener('keydown', handleKeys)
document.removeEventListener('keyup', stopBat)
}

function reboundFromBat() {

}

function reboundFromBlock(){

}

We define a checkCollision() function to check for and respond to all sorts of collisions. Ultimately, we want to check for collisions with the wall, the blocks, and the bat. So, we define functions for each of these. We start with the implementation of reboundFromFrame(). We first check if the ball has hit the right or left wall. If so, we reverse the ball’s x velocity. Next, we check if the ball has hit the top wall. If so, we reverse the y velocity. Finally, we check if the ball hit the bottom of the game board. If so, we need to end the game. We call a stop() function which sets a variable named gameOver to true. We initialize this variable to false near the top of the program. Also, in stop(), we remove the keyup and keydown event listeners. Back in reboundFromFrame() we also set the text “Game Over” in a DOM element which we query near the top of the program. Of course, we need to add this element in HTML.

<!DOCTYPE HTML>
<HTML>
<head>
<title>Breakout</title>
<link rel="stylesheet" href="style.css" />
</head>
<body>
<h2>Breakout</h2>
<div><span id="gameover"></span></div>
<div class="grid">
</div>

<script type="text/javascript" src="app.js"></script>
</body>
</HTML>

Now, onto detecting collisions with a block. We fill in the empty reboundFromBlock() function.

const expectedInterval = 16

function reboundFromBlock(){
const blkIndex = blocks.findIndex(checkBounds)
if (blkIndex >= 0) {
console.log('hit')
}
}

function checkBounds(b) {
const dx = ballvx / expectedInterval
const dy = ballvy / expectedInterval
return ball.topRight.x + dx> b.topLeft.x &&
ball.topLeft.x + dx < b.topRight.x &&
ball.topLeft.y + dy > b.bottomLeft.y &&
ball.bottomLeft.y + dy < b.topLeft.y
}

We use Array.findIndex(), because we want to find an element from theblocks array with which we had a collision, if at all. We’ll either have a collision with one block, or with none at all. Why didn’t we use Array.find()? Because we’ll want to remove the block from the array, which requires using the Array.splice() function with the element’s index.

We pass the comparison function checkBounds to the findIndex call. For every array element, this function will be called. For any element that this function returns true, the blkIndex variable will be set to the element’s index. If this function doesn’t return true for any element, then blkIndex will be set to -1.

In checkBounds(), we estimate where the ball is going to be in the next repaint. If it will be inside the block in the next cycle, then it is about to hit the block. To estimate where the ball will be on the next repaint cycle, we use its velocity and the duration until the next repaint to calculate the distance it will cover along the x and y axes one by one. Since velocity is distance divided by time, distance equals velocity divided by time. But, how long do we have until the next repaint? Repaints happen approximately 60 times per second, which results in 16 milliseconds per repaint. So, we define a variable named expectedInterval set to 16, and use it to calculate dx and dy, the distances that the ball will cover until the next repaint along the x and y axis, respectively. We add these values to the ball’s current coordinates and check if the resulting point lies within the bounds of the block. If so, we return true, otherwise we return false. With the above code, you should see a bunch of messages in the JavaScript console, whenever the ball hits the bottom edge of a block. The catch is, that when the ball comes back down, it triggers the console.log once again when it reaches the bottom edge of the block. This will get fixed once we start rebounding the ball after a collision.

Great! Now, you should see the console messages when the ball hits any edge of a block. What do we do when that happens? We’ll go ahead and remove that div element from the DOM, and from the blocks array. The former removes the element from the page, while the latter helps future processing focus only on blocks that remain. Here’s the code with those changes.

function reboundFromBlock(){
const blkIndex = blocks.findIndex(checkBounds)
if (blkIndex >= 0) {
removeBlock(blkIndex)
}
}

function removeBlock(blkIndex) {
const allBlocks = document.querySelectorAll('.block')
grid.removeChild(allBlocks[blkIndex])
blocks.splice(blkIndex, 1)
}

We add a call to a removeBlock() function and pass it the index of the block that was hit. This function queries all the blocks, and removes the block at the given index from the DOM as well as from the blocks array using the Array.splice() method.

Now, let’s add the functionality to make the ball rebound after hitting a block. The following illustration shows collisions of the ball on all four sides of a block and how it is expected to rebound.

The expected ball rebound if it hits any of the four sides of a block

If the the ball hits the block on its left or right side, it will continue in the same vertical direction, but will change its x direction. On the other hand, if the ball hits either the top or bottom edge of a block, it will continue along the same x direction, but will reverse its vertical direction. Althought we’ve only shown a few directions in which the ball was initially travelling, you may verify that this is the general expected behavior. Here’s the implementation:

function reboundFromBlock(){
const blkIndex = blocks.findIndex(checkBounds)
if (blkIndex >= 0) {
if (hitSide(blocks[blkIndex])) {
ballvx = -1 * ballvx
}
else {
ballvy = -1 * ballvy
}
removeBlock(blkIndex)
}
}

function hitSide(b) {
const yDirection = ballvy / expectedInterval
const x1 = ball.bottomLeft.y - yDirection > b.topLeft.y
const x2 = ball.topLeft.y - yDirection < b.bottomLeft.y
if ( x1 || x2) {
return false
}
return true
}

We implement a hitSide() function that returns true if the ball hit a block on its left or right side. Here’s an illustration that shows the range of positions in which we’ll consider the ball to have hit the left side wall of a block. The ball is shown with a dashed line:

Left side hit

So, at one extreme, the ball’s bottom bottom left y coordinate just before the collision is greater than the block’s top left y coordinate. The variable x1 stores the value of this condition. At the other extreme, the ball’s top right y coordinate just before the collision is less than the block’s bottom left y coordinate. The variable x2 holds the value of this condition. If either of these conditions is true, the ball must have come and hit the block’s bottom from below or the top from above. In such a case, we return false. Otherwise, we return true as the ball must have hit the block on one of its sides.

Till now, the ball doesn’t bounce off the bat. The collision detection with the bat is similar to that for collision detection with the blocks. But, the rebound is slightly different. If the ball hits the bat to the right of its centre, we want it to rebound towards the right, if it hits the bat to the left of its centre, we want it to rebound towards the left. If it hits the block exactly in the middle, well, it can retain its x direction.

function reboundFromBat() {
if (checkBounds(bat)) {
ballvy = -1 * ballvy
// if we hit the bat on the right half it should bounce off to the right
// otherwise to the left
if (ball.bottomLeft.x - bat.topLeft.x < (blockWidth / 2)) {
ballvx = -1 * ballSpeed
}
else {
ballvx = ballSpeed
}
}
}

We fill in our reboundFromBat() function that was previously emmpty. We reuse the checkBounds() function to check if the ball hit the bat. If so, we reverse the velocity of the ball along the y axis. We check where the ball hit the bat and reverse the velocity of the ball along the x axis, if needed.

In checkCollision(), notice that I put the bat collision function call first. Why is that? If I didn’t do it that way, and the ball comes to hit the wall and the bat at the same time, as shown in the following illustration, it will appear to slide over the bat and game over.

A corner case about collision with the bat

Score and finishing touches

Finally, let’s put the finishing touches of showing the score as we play the game and to show a message when game is over.

Here’s the HTML file with another span element for the game over and win message display:

<!DOCTYPE HTML>
<HTML>
<head>
<title>Breakout</title>
<link rel="stylesheet" href="style.css" />
</head>
<body>
<h2>Breakout</h2>
<div id="splashScreen" class="splash">
<div id="countdown" class="countdown">3</div>
</div>
<div>Score: <span id="score"></span><span id="gameover"></span></div>
<div class="grid">
</div>

<script type="text/javascript" src="app.js"></script>
</body>
</HTML>

The CSS file is as follows:

.grid {
width: 670px;
height: 300px;
border: 1px solid black;
background-color: #444;
position: absolute;
left: 50%;
top: 50%;
transform: translate(-50%, -50%);
}

.block {
width: 100px;
height: 20px;
background-color: #777777;
position: absolute;
}

.bat {
width: 100px;
height: 20px;
background-color: #AA0303;
position: absolute;
}

.ball {
width: 10px;
height: 10px;
background-color: #03AA03;
position: absolute;
border-radius: 50%;
}

.splash {
display: flex;
justify-content: center;
align-items: center;
width: 50%;
height: 50%;
position: absolute;
left: 50%;
top: 50%;
transform: translate(-50%, -50%);
background-color: rgba(0, 0, 0, 0.7);
z-index: 9999;
}

.countdown {
font-size: 48px;
color: white;
}

Here’s the JavaScript:

const boardWidth = 670
const boardHeight = 300

const blockWidth = 100
const blockHeight = 20
const blockScore = 10
const blockSpace = 10

const rows = 3
const columns = 6

const ballX = 320
const ballY = 102
const ballWidth = 10
const ballHeight = 10
const ballSpeed = 10

const maxScore = columns * rows * blockScore

const batX = 280
const batY = 10
const batSpeed = 100

const expectedInterval = 16

let gameOver = false

let ballvx = 10
let ballvy = 10

let batvx = 0

let lastTime = 0

const grid = document.querySelector('.grid')

const bat = new Block(batX, batY, blockWidth, blockHeight)
displayBlock(bat, 'bat')

const ball = new Block(ballX, ballY, ballWidth, ballHeight)
displayBlock(ball, 'ball')

const scoreSpan = document.getElementById('score')
const gameover = document.getElementById('gameover')

let score = 0

const splashScreen = document.getElementById('splashScreen');
const countdownElement = document.getElementById('countdown');

// Show the splash screen initially
splashScreen.style.display = 'flex';

// Countdown and start the game
let countdown = 3;
countdownElement.textContent = countdown;

function getStar() {
const minRadius = 0.5
const maxRadius = 2
const radius = Math.random() * (maxRadius - minRadius)
const x = Math.floor(Math.random() * boardWidth)
const y = Math.floor(Math.random() * boardHeight)
const star = document.createElement('div')
star.style.width = `${radius}px`
star.style.height = `${radius}px`
star.style.position = 'absolute'
console.log('x: ', x, ', y: ', y)
star.style.left = `${x}px`
star.style.top = `${y}px`
star.style.zIndex = -1
star.style.backgroundColor = '#FFF'
return star
}

function background() {
for (let i = 1; i < 100 ; i++) {
grid.appendChild(getStar())
}
}

function gameLoop(time) {
if(time) {
const deltams = time - lastTime
moveBall(deltams)
moveBat(deltams)
lastTime = time
if (!gameOver)
requestAnimationFrame(gameLoop)
}
}

function moveBat(deltams) {
if ((batvx === -1 * batSpeed && bat.bottomLeft.x >= blockSpace) ||
(batvx === batSpeed && bat.bottomLeft.x <= boardWidth - blockWidth - blockSpace)) {
const distance = batvx / deltams
bat.setX(bat.bottomLeft.x + distance)
const bElement = document.querySelector('.bat');
bElement.style.left = bat.bottomLeft.x + 'px'
}
}

function handleKeys(event) {
const key = event.key
switch(key) {
case 'ArrowLeft':
batvx = -1 * batSpeed
break
case 'ArrowRight':
batvx = batSpeed
break;
}
}

function stopBat(event) {
switch(event.key) {
case 'ArrowLeft':
case 'ArrowRight':
batvx = 0
break;
}
}

function Block(left, bottom, width, height) {
this.bottomLeft = {x:left, y: bottom}
this.bottomRight = {x:left + width, y: bottom}
this.topLeft = {x: left, y: bottom + height}
this.topRight = {x: left + width, y: bottom + height}
this.width = width
this.height = height
}

Block.prototype.setX = function(x) {
this.bottomLeft.x = x;
this.bottomRight.x = x + this.width
this.topRight.x = x + this.width
this.topLeft.x = x
}

Block.prototype.setY = function(y) {
this.bottomLeft.y = y;
this.topLeft.y = y + this.height;
this.bottomRight.y = y;
this.topRight.y = y + this.height;
}

const blocks = []

createRows()
displayBlocks()

function displayBlocks() {
blocks.forEach( b => displayBlock(b, 'block'))
}

function displayBlock(b, style) {
const block = document.createElement('div')
block.classList.add(style)
block.style.left = b.bottomLeft.x + 'px'
block.style.bottom = b.bottomLeft.y + 'px'
grid.appendChild(block)
}

function creatRow(row) {
for (let i = 0 ; i < columns ; i++) {
const block = new Block( blockSpace + i * (blockWidth + blockSpace), boardHeight - row * (blockHeight + blockSpace), blockWidth, blockHeight)
blocks.push(block)
}
}

function createRows() {
for (let j = 1 ; j <= rows ; j++) {
creatRow(j)
}
}

function moveBall(deltams) {
checkCollision()
scoreSpan.innerHTML = score
if (score === maxScore) {
gameOver = true
gameover.innerHTML = " (YOU WON!)"
stop()
}

const dx = ballvx / deltams
const dy = ballvy / deltams
ball.setX(ball.bottomLeft.x + dx)
ball.setY(ball.bottomLeft.y + dy)
const ballElement = document.querySelector('.ball')
ballElement.style.left = ball.bottomLeft.x + 'px'
ballElement.style.bottom = ball.bottomLeft.y + 'px'
}

function reboundFromBat() {
if (checkBounds(bat)) {
ballvy = -1 * ballvy
// if we hit the bat on the right half it should bounce off to the right
// otherwise to the left
if (ball.bottomLeft.x - bat.topLeft.x < (blockWidth / 2)) {
ballvx = -1 * ballSpeed
}
else {
ballvx = ballSpeed
}
}
}

function reboundFromFrame() {
// Check bounds of the play board
// Did we hit the right or left wall
if (ball.topRight.x >= boardWidth || ball.topLeft.x <= 0) {
ballvx = -1 * ballvx
}
// Did we hit the top wall
if (ball.topRight.y >= boardHeight) {
ballvy = -1 * ballvy
}
// Did we hit the bottom wall
else if (ball.bottomRight.y <= 0) {
stop()
gameover.innerHTML = " (GAME OVER)"
}
}

function removeBlock(blkIndex) {
const allBlocks = document.querySelectorAll('.block')
grid.removeChild(allBlocks[blkIndex])
blocks.splice(blkIndex, 1)
}

function reboundFromBlock(){
const blkIndex = blocks.findIndex(checkBounds)
if (blkIndex >= 0) {
// If we hit a side wall, we need to reverse the x direction,
// otherwise we need to reverse the y direction
if (hitSide(blocks[blkIndex])) {
ballvx = -1 * ballvx
}
else {
ballvy = -1 * ballvy
}
removeBlock(blkIndex)
score = score + blockScore
}
}

function checkCollision() {
// Did we hit the bat
reboundFromBat()
reboundFromFrame()
reboundFromBlock()
}

function checkBounds(b) {
const dx = ballvx / expectedInterval
const dy = ballvy / expectedInterval
return ball.topRight.x + dx> b.topLeft.x &&
ball.topLeft.x + dx < b.topRight.x &&
ball.topLeft.y + dy > b.bottomLeft.y &&
ball.bottomLeft.y + dy < b.topLeft.y
}

function hitSide(b) {
const yDirection = ballvy / expectedInterval
const x1 = ball.bottomLeft.y - yDirection > b.topLeft.y
const x2 = ball.topLeft.y - yDirection < b.bottomLeft.y
if ( x1 || x2) {
return false
}
return true
}

function stop() {
gameOver = true
document.removeEventListener('keydown', handleKeys)
document.removeEventListener('keyup', stopBat)
}

const countdownInterval = setInterval(() => {
countdown--;
if (countdown === 0) {
countdownElement.textContent = 'GO!'
} else if (countdown < 0) {
clearInterval(countdownInterval)
splashScreen.style.display = 'none' // Hide the splash screen
document.addEventListener('keydown', handleKeys)
document.addEventListener('keyup', stopBat)
background()
requestAnimationFrame(gameLoop)
} else {
countdownElement.textContent = countdown;
}
}, 1000);

I hope you liked the tutorial. Enjoy the game. You may download the source code from this repository. Try adding features to the game. For example, add a “Start game” button. Try implementing multiple “lives” feature, where the player gets multiple turns before the game is over. Try implementing a feature whereby the speed of the ball increases after every 50 points or so.

--

--

Muhammad Saqib Ilyas

A computer science teacher by profession. I love teaching and learning programming. I like to write about frontend development, and coding interview preparation