构建砖块区域
这是使用纯 JavaScript 创建打砖块游戏教程 11 个步骤中的第 6 步。我们将探索如何创建一组砖块、使用循环将它们绘制到屏幕上,并在球击中砖块时将其移除。构建砖块区域比向屏幕添加单个对象稍微复杂一些。
绘制砖块
所有砖块都使用同一张图像,因此我们可以只创建并解码一次,然后供它们共用。向 GameObject 添加静态 assets 映射和 url 字段,并替换它的构造函数和 preload() 方法:
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;
}
}
// …
}
缓存会将每个 URL 映射到一个解析为已解码图像的 promise。首次为某个 URL 调用 preload() 时,会创建图像并开始解码;后续调用会等待同一个 promise,并获取同一张图像。每个对象仍有自己的位置和尺寸。
与 Ball 和 Paddle 一样,Brick 也由 GameObject 类提供支持。砖块没有默认位置或尺寸,必须在构造函数中显式指定。由于砖块有明确的尺寸,我们可以使用 ctx.drawImage() 的额外参数 dWidth 和 dHeight;如果图像尺寸不符合所需尺寸,它会自动缩放图像。
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);
}
}
你还需要获取砖块图像,并将它保存到 /img 目录中。
我们会将绘制砖块的所有代码放入 initBricks 函数中,使其与其余代码分开。在 colliders.push(paddle); 下方调用 initBricks:
// …
const paddle = new Paddle("img/paddle.png", ctx);
colliders.push(paddle);
const bricks = initBricks();
// …
还要确保游戏在开始前等待砖块完成预加载:在 Promise.all() 中的数组里添加 , ...bricks。这些调用会共用缓存的解码 promise,因此砖块图像只会被解码一次。
接下来编写这个函数。在 script.js 文件末尾添加 initBricks 函数。首先添加 bricksLayout 对象,它很快就会派上用场:
function initBricks() {
const bricksLayout = {
width: 50,
height: 20,
count: {
row: 3,
col: 7,
},
offset: {
top: 50,
left: 60,
},
padding: 10,
};
const bricks = [];
// 在这里继续添加代码……
return bricks;
}
bricksLayout 包含了我们需要的所有信息:单块砖的宽度和高度、屏幕上砖块的行数和列数、顶部和左侧偏移量(即在画布上开始绘制砖块的位置),以及砖块行列之间的间距。
现在开始创建砖块本身。我们可以遍历各行和各列,并在每次循环中创建一个新砖块——在上一段代码下方添加以下嵌套循环:
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);
}
}
每个 brickX 的位置由 bricksLayout.width 与 bricksLayout.padding 之和乘以列索引 c,再加上 bricksLayout.offset.left 得出。brickY 的计算方式相同,只是使用行索引 r、bricksLayout.height 和 bricksLayout.offset.top。这样,每块砖都能放在正确的位置,砖块之间留有间距,并从 Canvas 的左边缘和上边缘偏移后绘制。
最后,我们可以在 update() 函数中将这些砖块绘制到屏幕上。在 paddle.draw() 调用下方添加以下代码:
for (const brick of bricks) {
brick.draw();
}
此时重新加载 index.html,你应该会看到砖块以均匀的间距绘制在屏幕上。
砖块与球的碰撞检测
接下来要实现球与砖块之间的碰撞检测。幸运的是,我们已经实现了一个通用性很强的碰撞系统,因此只需将砖块接入该系统即可。
首先,在 initBricks() 调用下方,将每个砖块注册为碰撞体:
const bricks = initBricks();
for (const brick of bricks) {
colliders.push(brick);
}
为每个砖块添加 onCollide() 方法,将砖块自身从 bricks 和 colliders 集合中移除:
class Brick extends GameObject {
// …
onCollide() {
bricks.splice(bricks.indexOf(this), 1);
colliders.splice(colliders.indexOf(this), 1);
}
}
必须尽快移除砖块,以免球再次从它反弹。
大功告成!重新加载代码,你应该会看到新的碰撞检测按预期工作。
比较你的代码
下面是目前应当实现的效果。点击“运行”按钮即可查看源代码。
下一步
我们现在可以击中并移除砖块,这已经为游戏玩法增添了不少乐趣。如果在所有砖块都被摧毁时还能记录分数并获胜,那就更好了。