Build the brick field
This is the 6th step out of 11 of the creating a Breakout game using pure JavaScript tutorial. Let's explore how to create a group of bricks, print them on the screen using a loop, and remove them when the ball hits them. Building the brick field is a little bit more complicated than adding a single object to the screen.
Drawing the bricks
All the bricks use the same image, so we can create and decode it once and share it among them. Add a static assets map and a url field to GameObject, and replace its constructor and preload() method:
class GameObject {
static assets = new Map();
url;
// …
constructor(url, ctx) {
this.url = url;
this.ctx = ctx;
}
async preload() {
if (!GameObject.assets.has(this.url)) {
const asset = new Image();
asset.src = this.url;
GameObject.assets.set(
this.url,
asset.decode().then(() => asset),
);
}
this.asset = await GameObject.assets.get(this.url);
if (this.size.w === undefined) {
this.size.w = this.asset.width;
this.size.h = this.asset.height;
}
}
// …
}
The cache maps each URL to a promise that resolves to the decoded image. The first preload() call for a URL creates the image and starts decoding it; later calls await the same promise and receive the same image. Each object still has its own position and size.
Like the Ball and Paddle, the Brick is also backed by the GameObject class. A brick has no default position or size and must be explicitly specified in the constructor. Because the bricks have explicit dimensions, we can use the extra dWidth and dHeight parameters of ctx.drawImage(), which automatically scales the image if it's not already of the desired dimensions.
class Brick extends GameObject {
constructor(url, ctx, x, y, w, h) {
super(url, ctx);
this.pos = { x, y };
this.size = { w, h };
}
draw() {
const { left, top } = this.hitbox;
this.ctx.drawImage(this.asset, left, top, this.size.w, this.size.h);
}
}
You also need to grab the brick image and save it in your /img directory.
We will place all the code for drawing the bricks inside an initBricks function to keep it separated from the rest of the code. Add a call to initBricks below colliders.push(paddle);:
// …
const paddle = new Paddle("img/paddle.png", ctx);
colliders.push(paddle);
const bricks = initBricks();
// …
And make sure that the game waits for the bricks to preload before starting the game, by adding , ...bricks to the array inside Promise.all(). These calls share the cached decode promise, so the brick image is only decoded once.
Now on to the function itself. Add the initBricks function at the end of the script.js file. To begin with, we add the bricksLayout object, as this will come in handy very soon:
function initBricks() {
const bricksLayout = {
width: 50,
height: 20,
count: {
row: 3,
col: 7,
},
offset: {
top: 50,
left: 60,
},
padding: 10,
};
const bricks = [];
// continue adding here...
return bricks;
}
This bricksLayout holds all the information we need: the width and height of a single brick, the number of rows and columns of bricks we will see on the screen, the top and left offset (the location on the canvas where we start to draw the bricks), and the padding between each row and column of bricks.
Now, let's start creating the bricks themselves. We can loop through the rows and columns to create a new brick on each iteration—add the following nested loop below the previous line of code:
for (let c = 0; c < bricksLayout.count.col; c++) {
for (let r = 0; r < bricksLayout.count.row; r++) {
const brickX =
c * (bricksLayout.width + bricksLayout.padding) +
bricksLayout.offset.left;
const brickY =
r * (bricksLayout.height + bricksLayout.padding) +
bricksLayout.offset.top;
const newBrick = new Brick(
"img/brick.png",
ctx,
brickX,
brickY,
bricksLayout.width,
bricksLayout.height,
);
bricks.push(newBrick);
}
}
Each brickX position is worked out as bricksLayout.width plus bricksLayout.padding, multiplied by the column number, c, plus the bricksLayout.offset.left; the logic for the brickY is identical except that it uses the values for row number, r, bricksLayout.height, and bricksLayout.offset.top. Now every single brick can be placed in its correct place, with padding between each brick, and drawn at an offset from the left and top Canvas edges.
Finally, we can draw these bricks to the screen inside the update() function. Add the following below the paddle.draw() call:
for (const brick of bricks) {
brick.draw();
}
If you reload index.html at this point, you should see the bricks printed on screen, at an even distance from one another.
Brick/Ball collision detection
Now onto the next challenge—the collision detection between the ball and the bricks. Luckily enough, we already implemented a very generic collision system, so we can simply wire our bricks to it.
First, register each brick as a collider, right below the initBricks() call:
const bricks = initBricks();
for (const brick of bricks) {
colliders.push(brick);
}
Add a onCollide() method to each brick, which removes itself from the bricks and colliders collections:
class Brick extends GameObject {
// …
onCollide() {
bricks.splice(bricks.indexOf(this), 1);
colliders.splice(colliders.indexOf(this), 1);
}
}
The brick needs to be gone as soon as possible, so that the ball doesn't bounce off it.
And that's it! Reload your code, and you should see the new collision detection working just as required.
Compare your code
Here's what you should have so far, running live. To view its source code, click the "Play" button.
Next steps
We can hit the bricks and remove them, which is a nice addition to the gameplay already. It would be even better to track the score and win when all the bricks are destroyed.