弹跳

这是 Gamedev Canvas 教程第 3 步,共 10 步。您可以在 Gamedev-Canvas-workshop/lesson3.html 找到完成本课后代码应有的样子。

看到我们的球在移动很不错,但它很快就从屏幕上消失了,限制了我们的乐趣!为了克服这一点,我们将实现一些碰撞检测(稍后将在 更详细地解释),让球从 Canvas 的四条边反弹。

简单的碰撞检测

要检测碰撞,我们将检查球是否碰到(碰撞到)墙壁,如果碰到,我们将相应地改变其移动方向。

为了简化计算,让我们定义一个名为 ballRadius 的变量,它将保存绘制圆的半径并用于计算。将此添加到您的代码中,位于现有变量声明的某个位置下方。

js
const ballRadius = 10;

现在,将 drawBall() 函数中绘制球的代码更新为如下所示:

js
ctx.arc(x, y, ballRadius, 0, Math.PI * 2);

弹跳顶部和底部

球有四面墙可以反弹 — 让我们先关注顶部。我们需要在每一帧都检查球是否碰到 Canvas 的顶部边缘 — 如果是,我们将反转球的移动方向,使其开始向相反方向移动并保持在可见边界内。记住坐标系从左上角开始,我们可以得出这样的结论:

js
if (y + dy < 0) {
  dy = -dy;
}

如果球位置的 y 值小于零,则通过将其设置为自身的反值来改变 y 轴上的移动方向。如果球以每帧 2 像素的速度向上移动,现在它将以 -2 像素的速度“向上”移动,这实际上等于以每帧 2 像素的速度向下移动。

上面的代码处理了球从顶部边缘反弹的情况,现在让我们考虑底部边缘:

js
if (y + dy > canvas.height) {
  dy = -dy;
}

如果球的 y 位置大于 Canvas 的高度(记住我们从左上角开始计算 y 值,所以顶部边缘从 0 开始,底部边缘是 320 像素,即 Canvas 的高度),然后通过像以前一样反转 y 轴移动来使其从底部边缘反弹。

我们可以将这两个语句合并为一个,以节省代码冗余。

js
if (y + dy > canvas.height || y + dy < 0) {
  dy = -dy;
}

如果两个语句中的任何一个为 true,则反转球的移动。

弹跳左侧和右侧

我们已经处理了顶部和底部边缘,所以让我们考虑左侧和右侧。实际上非常相似,您所要做的就是为 x 重复 y 的语句。

js
if (x + dx > canvas.width || x + dx < 0) {
  dx = -dx;
}

if (y + dy > canvas.height || y + dy < 0) {
  dy = -dy;
}

此时,您应该将上面的代码块插入到 draw() 函数中,就在闭合花括号之前。

球仍然会消失到墙里!

此时测试您的代码,您会感到惊讶 — 现在我们有了一个能从画布四边反弹的球!然而,我们遇到了另一个问题 — 当球碰到每面墙时,它会在改变方向之前稍微沉入墙里。

skyblue ball disappearing into the top of the white wall.

这是因为我们正在计算墙壁和球心之间的碰撞点,而我们应该为球的周长进行计算。球应该在碰到墙壁后立即反弹,而不是在已经半个球体都在墙里之后,所以让我们稍微调整一下我们的语句以包含这一点。将您最后添加的代码更新为如下所示:

js
if (x + dx > canvas.width - ballRadius || x + dx < ballRadius) {
  dx = -dx;
}
if (y + dy > canvas.height - ballRadius || y + dy < ballRadius) {
  dy = -dy;
}

当球心与墙壁边缘之间的距离等于球的半径时,它将改变移动方向。从一个边缘的宽度中减去半径,并在另一个边缘加上半径,让我们对正确的碰撞检测有印象 — 球应该像预期的那样从墙壁上反弹。

Compare your code

让我们再次检查此部分的代码是否与您的代码相符,并进行一些尝试。

js
const canvas = document.getElementById("myCanvas");
const ctx = canvas.getContext("2d");
const ballRadius = 10;
let x = canvas.width / 2;
let y = canvas.height - 30;
let dx = 2;
let dy = -2;

function drawBall() {
  ctx.beginPath();
  ctx.arc(x, y, ballRadius, 0, Math.PI * 2);
  ctx.fillStyle = "#0095DD";
  ctx.fill();
  ctx.closePath();
}

function draw() {
  ctx.clearRect(0, 0, canvas.width, canvas.height);
  drawBall();

  if (x + dx > canvas.width - ballRadius || x + dx < ballRadius) {
    dx = -dx;
  }
  if (y + dy > canvas.height - ballRadius || y + dy < ballRadius) {
    dy = -dy;
  }

  x += dx;
  y += dy;
}

function startGame() {
  setInterval(draw, 10);
}

const runButton = document.getElementById("runButton");
runButton.addEventListener("click", () => {
  startGame();
  runButton.disabled = true;
});

注意: 尝试在球碰到墙壁时将其颜色更改为随机颜色。

后续步骤

现在我们已经到了球既能移动又能停留在游戏板上的阶段。在第四章中,我们将实现一个可控制的挡板 — 请参阅 挡板和键盘控制