起始点
要开始这个挑战,请在本地计算机的新目录中,复制上一篇文章中的 index-finished.html、style.css 和 main-finished.js 到本地。
或者,你可以使用在线编辑器,如 CodePen 或 JSFiddle。如果你使用的在线编辑器没有独立的 JavaScript 面板,你可以将 JavaScript 代码放在 HTML 页面中的 <script> 元素内。
注意:如果你遇到困难,可以通过我们的 交流渠道 与我们联系。
提示和技巧
开始之前有几点提示。
- 这个挑战相当困难。在开始编码之前请阅读所有说明,并缓慢而仔细地执行每个步骤。
- 在每个阶段完成后,最好保存演示的单独副本,以便将来遇到麻烦时可以参考。
项目简介
我们的弹跳球演示很有趣,但现在我们想通过添加一个用户控制的邪恶圆圈来使其更具交互性,如果它抓住球,它就会吃掉球。我们还想通过创建一个通用的 Shape() 对象来测试你的对象构建技能,我们的球和邪恶圆圈都可以继承该对象。最后,我们想添加一个分数计数器来跟踪剩余的球的数量。
以下截图让你了解完成后的程序应该是什么样子。

为了让你有更多的了解,请查看 完成的示例 (不要偷看源代码!)
完成步骤
以下部分描述了你需要做的事情。
创建一个 Shape 类
首先,创建一个新的 Shape 类。它只有一个构造函数。Shape 构造函数应该以与最初的 Ball() 构造函数相同的方式定义 x、y、velX 和 velY 属性,但不定义 color 和 size 属性。
Ball 类应该使用 extends 继承自 Shape。Ball 的构造函数应该
- 接受与以前相同的参数:
x、y、velX、velY、size和color - 使用
super()调用Shape构造函数,并传入x、y、velX和velY参数 - 根据给定的参数初始化其自身的
color和size属性。
注意:确保在现有 Ball 类上方创建 Shape 类,否则你会收到类似“Uncaught ReferenceError: Cannot access 'Shape' before initialization”的错误。
Ball 构造函数应该定义一个名为 exists 的新属性,用于跟踪球是否存在于程序中(是否未被邪恶圆圈吃掉)。这应该是一个布尔值(true/false),在构造函数中初始化为 true。
Ball 类的 collisionDetect() 方法需要进行小幅更新。只有当 exists 属性为 true 时,才需要考虑球的碰撞检测。因此,将现有的 collisionDetect() 代码替换为以下代码:
class Ball {
// …
collisionDetect() {
for (const ball of balls) {
if (!(this === ball) && ball.exists) {
const dx = this.x - ball.x;
const dy = this.y - ball.y;
const distance = Math.sqrt(dx * dx + dy * dy);
if (distance < this.size + ball.size) {
ball.color = this.color = randomRGB();
}
}
}
}
// …
}
如上所述,唯一的添加是检查球是否存在——通过在 if 条件中使用 ball.exists。
球的 draw() 和 update() 方法定义应该与以前完全相同。
此时,尝试重新加载代码——它应该与以前一样工作,只是我们的对象经过了重新设计。
定义 EvilCircle
现在是时候迎接坏人了——EvilCircle()!我们的游戏只涉及一个邪恶圆圈,但我们仍然会使用继承自 Shape() 的构造函数来定义它,以便给你一些练习。你可能希望稍后在应用程序中添加另一个可以由其他玩家控制的圆圈,或者有几个由计算机控制的邪恶圆圈。你可能不会用一个邪恶圆圈征服世界,但它足以应对这个挑战。
为 EvilCircle 类创建一个定义。它应该使用 extends 继承自 Shape。
EvilCircle 构造函数
EvilCircle 的构造函数应该
- 只传递
x,y参数 - 将
x,y参数以及硬编码为 20 的velX和velY值传递给Shape超类。你应该使用super(x, y, 20, 20);这样的代码来完成。 - 将
color设置为white,将size设置为10。
最后,构造函数应该设置代码,使用户能够移动屏幕上的邪恶圆圈。
window.addEventListener("keydown", (e) => {
switch (e.key) {
case "a":
this.x -= this.velX;
break;
case "d":
this.x += this.velX;
break;
case "w":
this.y -= this.velY;
break;
case "s":
this.y += this.velY;
break;
}
});
这将向 window 对象添加一个 keydown 事件监听器,以便当按下按键时,检查事件对象的 key 属性以查看按下了哪个键。如果它是四个指定键之一,则邪恶圆圈将向左/右/上/下移动。
为 EvilCircle 定义方法
EvilCircle 类应该有三个方法,如下所述。
draw()
此方法与 Ball 的 draw() 方法具有相同的目的:它在画布上绘制对象实例。EvilCircle 的 draw() 方法的工作方式将非常相似,因此你可以从复制 Ball 的 draw() 方法开始。然后你应该进行以下更改:
- 我们希望邪恶圆圈不被填充,而只有外线(描边)。你可以通过将
fillStyle和fill()分别更新为strokeStyle和stroke()来实现这一点。 - 我们还希望使描边更粗一些,这样你可以更容易地看到邪恶圆圈。这可以通过在
beginPath()调用之后的某个位置设置lineWidth的值(3 即可)。
checkBounds()
此方法将执行与 Ball 的 update() 方法的第一部分相同的事情——查看邪恶圆圈是否会超出屏幕边缘,并阻止它这样做。同样,你基本上可以只复制 Ball 的 update() 方法,但你需要进行一些更改:
- 删除最后两行——我们不希望在每一帧自动更新邪恶圆圈的位置,因为我们将以其他方式移动它,如下所示。
- 在
if ()语句中,如果测试返回 true,我们不希望更新velX/velY;我们希望改为更改x/y的值,以便邪恶圆圈稍微反弹回屏幕上。适当地添加或减去邪恶圆圈的size属性是有意义的。
collisionDetect()
此方法的行为方式将与 Ball 方法的 collisionDetect() 方法非常相似,因此你可以将其副本作为此新方法的基础。但有几个区别:
- 在外部
if语句中,你不再需要检查迭代中的当前球是否与正在检查的球相同——因为它不再是球,它是邪恶圆圈!相反,你需要进行测试以查看正在检查的球是否存在(你可以用哪个属性来做到这一点?)。如果它不存在,它已经被邪恶圆圈吃掉了,因此无需再次检查。 - 在内部
if语句中,你不再希望在检测到碰撞时使对象改变颜色——相反,你希望将与邪恶圆圈碰撞的任何球设置为不再存在(再次,你认为你会怎么做?)。
将邪恶圆圈引入程序
现在我们已经定义了邪恶圆圈,我们需要让它出现在我们的场景中。为此,你需要对 loop() 函数进行一些更改。
- 首先,创建一个新的邪恶圆圈对象实例(指定必要的参数)。你只需执行一次,而不是在循环的每次迭代中都执行。
- 在你遍历每个球并为每个球调用
draw()、update()和collisionDetect()函数时,确保只有在当前球存在时才调用这些函数。 - 在循环的每次迭代中调用邪恶圆圈实例的
draw()、checkBounds()和collisionDetect()方法。
实现分数计数器
要实现分数计数器,请遵循以下步骤:
-
在你的 CSS 文件中,在底部添加以下规则:
cssp { position: absolute; margin: 0; top: 35px; right: 5px; color: #aaaaaa; } -
在你的 JavaScript 中,进行以下更新:
- 创建一个变量来存储对段落的引用。
- 以某种方式记录屏幕上球的数量。
- 每次向场景中添加一个球时,增加计数并显示更新后的球的数量。
- 每次邪恶圆圈吃掉一个球(使其不再存在)时,减少计数并显示更新后的球的数量。