对象构建实践
在之前的文章中,我们学习了所有重要的 JavaScript 对象理论和语法细节,为你打下了坚实的基础。在本文中,我们将深入到一个实践练习,通过一个有趣且多彩的结果,为你提供更多构建自定义 JavaScript 对象的实践。
预备知识 | 熟悉 JavaScript 基础(尤其是对象基础)和本模块先前课程中涵盖的面向对象 JavaScript 概念。 |
---|---|
学习成果 | 在实际情境中练习使用对象和面向对象技术。 |
让我们来弹跳一些小球
在本文中,我们将编写一个经典的“弹跳小球”演示,向你展示对象在 JavaScript 中是多么有用。我们的小球将在屏幕上弹跳,并在相互接触时改变颜色。最终的示例看起来会是这样:
此示例将利用 Canvas API 在屏幕上绘制小球,并利用 requestAnimationFrame
API 为整个显示添加动画 — 你不需要有这些 API 的任何先验知识,我们希望在你完成本文时,你会对它们有兴趣进一步探索。在此过程中,我们将使用一些巧妙的对象,并向你展示一些不错的技术,例如小球从墙壁弹开,以及检查它们是否相互撞击(也称为碰撞检测)。
入门
首先,复制我们的 index.html
、style.css
和 main.js
文件到本地。它们分别包含以下内容:
- 一个非常简单的 HTML 文档,包含一个 h1 元素,一个
<canvas>
元素用于绘制我们的小球,以及用于将 CSS 和 JavaScript 应用到 HTML 的元素。 - 一些非常简单的样式,主要用于设置
<h1>
的样式和位置,并去除页面边缘的任何滚动条或边距(使其看起来整洁)。 - 一些 JavaScript,用于设置
<canvas>
元素并提供我们将要使用的通用函数。
脚本的第一部分如下所示:
const canvas = document.querySelector("canvas");
const ctx = canvas.getContext("2d");
const width = (canvas.width = window.innerWidth);
const height = (canvas.height = window.innerHeight);
这个脚本获取对 <canvas>
元素的引用,然后在其上调用 getContext()
方法,为我们提供一个可以开始绘制的上下文。生成的常量 (ctx
) 是直接表示画布绘图区域的对象,允许我们在其上绘制 2D 图形。
接下来,我们设置了名为 width
和 height
的常量,并将 canvas 元素的宽度和高度(由 canvas.width
和 canvas.height
属性表示)设置为等于浏览器视口(网页显示的区域 — 这可以通过 Window.innerWidth
和 Window.innerHeight
属性获取)的宽度和高度。
请注意,我们正在将多个赋值操作链接在一起,以更快地设置所有变量 — 这是完全可以的。
然后我们有两个辅助函数
function random(min, max) {
return Math.floor(Math.random() * (max - min + 1)) + min;
}
function randomRGB() {
return `rgb(${random(0, 255)} ${random(0, 255)} ${random(0, 255)})`;
}
random()
函数接受两个数字作为参数,并返回介于两者之间的一个随机数。randomRGB()
函数生成一个表示为 rgb()
字符串的随机颜色。
在程序中建模小球
我们的程序将在屏幕上显示许多弹跳的球。由于这些球都以相同的方式运动,因此用一个对象来表示它们是合理的。让我们从在代码底部添加以下类定义开始。
class Ball {
constructor(x, y, velX, velY, color, size) {
this.x = x;
this.y = y;
this.velX = velX;
this.velY = velY;
this.color = color;
this.size = size;
}
}
到目前为止,这个类只包含一个构造函数,我们可以在其中初始化每个球在程序中运行所需的属性:
x
和y
坐标 — 球在屏幕上开始的水平和垂直坐标。这可以从 0(左上角)到浏览器视口(右下角)的宽度和高度范围。- 水平和垂直速度(
velX
和velY
)— 每个球都被赋予水平和垂直速度;实际上,当我们给球添加动画时,这些值会定期添加到x
/y
坐标值中,以便在每一帧中移动它们这么多。 color
— 每个球都有一个颜色。size
— 每个球都有一个大小 — 这是它的半径,以像素为单位。
这处理了属性,但是方法呢?我们希望让球在程序中实际做些什么。
绘制小球
首先,将以下 draw()
方法添加到 Ball
类中:
class Ball {
// …
draw() {
ctx.beginPath();
ctx.fillStyle = this.color;
ctx.arc(this.x, this.y, this.size, 0, 2 * Math.PI);
ctx.fill();
}
}
使用此函数,我们可以通过调用我们之前定义的 2D canvas 上下文 (ctx
) 的一系列成员来告诉小球在屏幕上绘制自己。上下文就像纸张,现在我们想命令我们的笔在上面绘制一些东西。
-
首先,我们使用
beginPath()
来声明我们想要在纸上绘制一个形状。 -
接下来,我们使用
fillStyle
定义我们希望形状呈现的颜色 — 我们将其设置为小球的color
属性。 -
接下来,我们使用
arc()
方法在纸上描绘一个弧形。它的参数是:- 圆弧中心的
x
和y
位置 — 我们指定了小球的x
和y
属性。 - 圆弧的半径 — 在这种情况下,是球的
size
属性。 - 最后两个参数指定了圆弧绘制的起始和结束度数。这里我们指定 0 度和
2 * PI
,这相当于 360 度(以弧度表示,令人恼火的是你必须以弧度指定)。这给我们一个完整的圆。如果你只指定了1 * PI
,你将得到一个半圆(180 度)。
- 圆弧中心的
-
最后,我们使用
fill()
方法,它基本上表示“完成我们用beginPath()
开始绘制的路径,并用我们之前在fillStyle
中指定的颜色填充它所占据的区域。”
你现在就可以开始测试你的对象了。
-
保存目前的 L 代码,并在浏览器中加载 HTML 文件。
-
打开浏览器的 JavaScript 控制台,然后刷新页面,以便画布大小更改为控制台打开后剩余的较小可见视口。
-
输入以下内容以创建一个新的球实例:
jsconst testBall = new Ball(50, 100, 4, 4, "blue", 10);
-
尝试调用其成员:
jstestBall.x; testBall.size; testBall.color; testBall.draw();
-
当你输入最后一行时,你应该会看到小球在画布的某个位置绘制出来。
更新小球数据
我们可以在指定位置绘制小球,但要实际移动小球,我们需要某种更新函数。将以下代码添加到 Ball
的类定义中:
class Ball {
// …
update() {
if (this.x + this.size >= width) {
this.velX = -this.velX;
}
if (this.x - this.size <= 0) {
this.velX = -this.velX;
}
if (this.y + this.size >= height) {
this.velY = -this.velY;
}
if (this.y - this.size <= 0) {
this.velY = -this.velY;
}
this.x += this.velX;
this.y += this.velY;
}
}
函数的前四个部分检查球是否已到达画布边缘。如果已到达,我们会反转相关速度的方向,使球沿相反方向运动。例如,如果球向上运动(负 velY
),则垂直速度会改变,使其改为向下运动(正 velY
)。
在四种情况下,我们正在检查:
- 如果
x
坐标大于画布宽度(球正超出右边缘)。 - 如果
x
坐标小于 0(球正超出左边缘)。 - 如果
y
坐标大于画布高度(球正超出下边缘)。 - 如果
y
坐标小于 0(球正超出上边缘)。
在每种情况下,我们都将球的 size
包含在计算中,因为 x
/y
坐标位于球的中心,但我们希望球的边缘从边界弹开——我们不希望球在开始反弹之前就超出屏幕一半。
最后两行将 velX
值添加到 x
坐标,并将 velY
值添加到 y
坐标——每次调用此方法时,球实际上都会移动。
暂时就这些了;让我们继续做动画吧!
给小球添加动画
现在让我们来点乐趣。我们现在要开始将小球添加到画布上,并对其进行动画处理。
首先,我们需要创建一个地方来存储我们所有的小球,然后填充它。以下代码将完成这项工作——现在将其添加到代码底部:
const balls = [];
while (balls.length < 25) {
const size = random(10, 20);
const ball = new Ball(
// ball position always drawn at least one ball width
// away from the edge of the canvas, to avoid drawing errors
random(0 + size, width - size),
random(0 + size, height - size),
random(-7, 7),
random(-7, 7),
randomRGB(),
size,
);
balls.push(ball);
}
while
循环使用我们的 random()
和 randomRGB()
函数生成的随机值创建一个新的 Ball()
实例,然后将其 push()
到我们的 balls 数组的末尾,但前提是数组中的小球数量少于 25 个。因此,当数组中有 25 个小球时,将不再添加小球。你可以尝试改变 balls.length < 25
中的数字,以获取更多或更少的小球。根据你的计算机/浏览器的处理能力,指定几千个小球可能会使动画变慢很多!
接下来,将以下内容添加到代码底部:
function loop() {
ctx.fillStyle = "rgb(0 0 0 / 25%)";
ctx.fillRect(0, 0, width, height);
for (const ball of balls) {
ball.draw();
ball.update();
}
requestAnimationFrame(loop);
}
所有动画程序通常都包含一个动画循环,该循环用于更新程序中的信息,然后在动画的每一帧上渲染结果视图;这是大多数游戏和其他此类程序的基础。我们的 loop()
函数执行以下操作:
- 将画布填充颜色设置为半透明黑色,然后使用
fillRect()
在画布的整个宽度和高度上绘制一个相同颜色的矩形(四个参数提供起始坐标以及绘制矩形的宽度和高度)。这用于在绘制下一帧之前覆盖上一帧的绘制。如果你不这样做,你只会看到长长的蛇在画布上蠕动而不是小球移动!填充颜色设置为半透明的rgb(0 0 0 / 25%)
,以允许前几帧轻微透出,从而在小球移动时产生小球后面的小轨迹。如果你将 0.25 更改为 1,你将完全看不到它们。尝试改变这个数字以查看其效果。 - 遍历
balls
数组中的所有小球,并运行每个小球的draw()
和update()
函数,在屏幕上绘制每个小球,然后及时更新位置和速度,为下一帧做准备。 - 使用
requestAnimationFrame()
方法再次运行该函数——当此方法重复运行并传入相同的函数名时,它会每秒运行该函数设定的次数,以创建平滑的动画。这通常是递归完成的——这意味着函数每次运行时都会调用自身,因此它会一遍又一遍地运行。
最后,将以下行添加到代码底部——我们需要调用一次函数以启动动画。
loop();
基本内容就是这些了——尝试保存并刷新以测试你的弹跳球!
添加碰撞检测
现在为了增加一些乐趣,让我们在程序中添加一些碰撞检测,这样我们的小球就能知道何时撞到另一个小球。
首先,将以下方法定义添加到你的 Ball
类中。
class Ball {
// …
collisionDetect() {
for (const ball of balls) {
if (this !== ball) {
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();
}
}
}
}
}
这个方法有点复杂,所以如果现在你还不完全理解它的工作原理,也不必担心。下面是解释:
- 对于每个球,我们需要检查所有其他球,看它是否与当前球发生碰撞。为此,我们启动另一个
for...of
循环,遍历balls[]
数组中的所有球。 - 在 for 循环内部,我们立即使用一个
if
语句来检查当前遍历的球是否与我们正在检查的球是同一个。我们不想检查一个球是否与自己发生碰撞!为此,我们检查当前球(即正在调用其collisionDetect
方法的球)是否与循环球(即collisionDetect
方法中 for 循环的当前迭代所引用的球)相同。然后我们使用!
来否定检查,这样if
语句中的代码只有在它们不相同的情况下才会运行。 - 然后,我们使用一种常见的算法来检测两个圆的碰撞。我们基本上是在检查这两个圆的任何区域是否重叠。这在2D 碰撞检测中有进一步的解释。
- 如果检测到碰撞,则执行内部
if
语句中的代码。在这种情况下,我们只将两个圆的color
属性设置为新的随机颜色。我们本可以做更复杂的事情,比如让小球更真实地相互弹开,但这会复杂得多。对于此类物理模拟,开发人员倾向于使用游戏或物理库,如 PhysicsJS、matter.js、Phaser 等。
你还需要在动画的每一帧中调用此方法。更新你的 loop()
函数,在 ball.update()
之后调用 ball.collisionDetect()
。
function loop() {
ctx.fillStyle = "rgb(0 0 0 / 25%)";
ctx.fillRect(0, 0, width, height);
for (const ball of balls) {
ball.draw();
ball.update();
ball.collisionDetect();
}
requestAnimationFrame(loop);
}
再次保存并刷新演示,你会看到你的小球在碰撞时改变颜色!
总结
我们希望你通过使用本模块中的各种对象和面向对象技术,编写自己的真实世界随机弹跳球示例,玩得开心!这应该为你提供了使用对象的一些有益实践,以及良好的实际应用场景。
关于对象课程就到这里了——现在只剩下你在模块挑战中测试你的技能了。
另见
- Canvas 教程 — 初学者 2D Canvas 教程。
- requestAnimationFrame()
- 2D 碰撞检测
- 3D 碰撞检测
- 使用纯 JavaScript 构建 2D 打砖块游戏 — 一个很棒的初学者教程,展示如何构建 2D 游戏。
- 使用 Phaser 构建 2D 打砖块游戏 — 解释了使用 JavaScript 游戏库构建 2D 游戏的基础知识。