此页面由社区从英文翻译而来。了解更多并加入 MDN Web Docs 社区。

View in English Always switch to English

构建砖块区域

这是使用纯 JavaScript 创建打砖块游戏教程 11 个步骤中的第 6 步。我们将探索如何创建一组砖块、使用循环将它们绘制到屏幕上,并在球击中砖块时将其移除。构建砖块区域比向屏幕添加单个对象稍微复杂一些。

绘制砖块

所有砖块都使用同一张图像,因此我们可以只创建并解码一次,然后供它们共用。向 GameObject 添加静态 assets 映射和 url 字段,并替换它的构造函数和 preload() 方法:

js
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;如果图像尺寸不符合所需尺寸,它会自动缩放图像。

js
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:

js
// …
const paddle = new Paddle("img/paddle.png", ctx);
colliders.push(paddle);
const bricks = initBricks();
// …

还要确保游戏在开始前等待砖块完成预加载:在 Promise.all() 中的数组里添加 , ...bricks。这些调用会共用缓存的解码 promise,因此砖块图像只会被解码一次。

接下来编写这个函数。在 script.js 文件末尾添加 initBricks 函数。首先添加 bricksLayout 对象,它很快就会派上用场:

js
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 包含了我们需要的所有信息:单块砖的宽度和高度、屏幕上砖块的行数和列数、顶部和左侧偏移量(即在画布上开始绘制砖块的位置),以及砖块行列之间的间距。

现在开始创建砖块本身。我们可以遍历各行和各列,并在每次循环中创建一个新砖块——在上一段代码下方添加以下嵌套循环:

js
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() 调用下方添加以下代码:

js
for (const brick of bricks) {
  brick.draw();
}

此时重新加载 index.html,你应该会看到砖块以均匀的间距绘制在屏幕上。

砖块与球的碰撞检测

接下来要实现球与砖块之间的碰撞检测。幸运的是,我们已经实现了一个通用性很强的碰撞系统,因此只需将砖块接入该系统即可。

首先,在 initBricks() 调用下方,将每个砖块注册为碰撞体:

js
const bricks = initBricks();
for (const brick of bricks) {
  colliders.push(brick);
}

为每个砖块添加 onCollide() 方法,将砖块自身从 bricks 和 colliders 集合中移除:

js
class Brick extends GameObject {
  // …
  onCollide() {
    bricks.splice(bricks.indexOf(this), 1);
    colliders.splice(colliders.indexOf(this), 1);
  }
}

必须尽快移除砖块,以免球再次从它反弹。

大功告成!重新加载代码,你应该会看到新的碰撞检测按预期工作。

比较你的代码

下面是目前应当实现的效果。点击“运行”按钮即可查看源代码。

下一步

我们现在可以击中并移除砖块,这已经为游戏玩法增添了不少乐趣。如果在所有砖块都被摧毁时还能记录分数并获胜,那就更好了。