对象构建练习

在之前的文章中,我们回顾了所有 JavaScript 对象的基本理论和语法细节,为您打下了坚实的基础。在本文中,我们将深入探讨一个实践练习,让您在构建自定义 JavaScript 对象方面获得更多练习,并获得有趣且多彩的结果。

先决条件 对 HTML 和 CSS 的基本理解,熟悉 JavaScript 基础知识(请参阅第一步构建模块)和 OOJS 基础知识(请参阅对象简介)。
目标 在真实的场景中练习使用对象和面向对象的技术。

让我们弹跳一些球

在本文中,我们将编写一个经典的“弹球”演示,向您展示对象在 JavaScript 中的用途。我们的球将在屏幕上弹跳,并在彼此接触时改变颜色。完成后的示例看起来像这样

Screenshot of a webpage titled "Bouncing balls". 23 balls of various pastel colors and sizes are visible across a black screen with long trails behind them indicating motion.

此示例将使用Canvas API将球绘制到屏幕上,并使用requestAnimationFrame API 来动画化整个显示——您不需要事先了解这些 API,我们希望在您完成本文后,您会对进一步探索它们感兴趣。在此过程中,我们将使用一些巧妙的对象,并向您展示一些不错的技巧,例如让球弹离墙壁,以及检查它们是否相互碰撞(也称为碰撞检测)。

入门

首先,制作我们index.htmlstyle.cssmain.js文件的本地副本。这些文件分别包含以下内容:

  1. 一个非常简单的 HTML 文档,其中包含一个h1元素、一个<canvas>元素(用于在上面绘制球)以及用于将 CSS 和 JavaScript 应用于 HTML 的元素。
  2. 一些非常简单的样式,主要用于设置 <h1> 的样式和位置,并消除页面边缘周围的任何滚动条或边距(使其看起来美观整洁)。
  3. 一些 JavaScript 代码,用于设置 <canvas> 元素并提供我们将要使用的通用函数。

脚本的第一部分如下所示:

js
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)是直接表示画布绘图区域的对象,并允许我们在其上绘制二维形状。

接下来,我们设置名为 widthheight 的常量,并将画布元素的宽度和高度(由 canvas.widthcanvas.height 属性表示)设置为浏览器视口(网页显示的区域——可以从Window.innerWidthWindow.innerHeight属性中获取)的宽度和高度。

请注意,我们将多个赋值链接在一起,以便更快地设置所有变量——这是完全可以的。

然后我们有两个辅助函数:

js
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()字符串表示的随机颜色。

在我们的程序中建模球体

我们的程序将包含许多在屏幕上弹跳的球。由于这些球的行为都相同,因此用对象表示它们是有意义的。让我们从在代码底部添加以下类定义开始。

js
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;
  }
}

到目前为止,此类仅包含一个构造函数,我们可以在其中初始化每个球体在程序中运行所需属性。

  • xy 坐标——球体在屏幕上开始时的水平和垂直坐标。这可以介于 0(左上角)到浏览器视口的宽度和高度(右下角)之间。
  • 水平和垂直速度(velXvelY)——每个球都赋予一个水平和垂直速度;实际上,当我们对球体进行动画处理时,这些值会定期添加到 x/y 坐标值中,以便在每一帧上移动这么多。
  • color——每个球都具有颜色。
  • size——每个球都有一个尺寸——这是它的半径,以像素为单位。

这处理了属性,但是方法呢?我们希望让球体在程序中真正地做一些事情。

绘制球体

首先将以下 draw() 方法添加到 Ball 类中:

js
draw() {
  ctx.beginPath();
  ctx.fillStyle = this.color;
  ctx.arc(this.x, this.y, this.size, 0, 2 * Math.PI);
  ctx.fill();
}

使用此函数,我们可以告诉球体将自身绘制到屏幕上,方法是调用我们之前定义的二维画布上下文(ctx)的一系列成员。上下文就像纸张,现在我们希望命令我们的笔在上面画一些东西。

  • 首先,我们使用beginPath()来声明我们想要在纸上绘制一个形状。
  • 接下来,我们使用fillStyle来定义我们希望形状的颜色——我们将其设置为球体的 color 属性。
  • 接下来,我们使用arc()方法在纸上描绘一个弧形。它的参数是:
    • 弧形中心的 xy 位置——我们指定了球体的 xy 属性。
    • 弧形的半径——在本例中,是球体的 size 属性。
    • 最后两个参数指定弧形绘制的圆圈周围的起始和结束度数。在这里,我们指定 0 度和 2 * PI,这相当于弧度中的 360 度(令人讨厌的是,您必须以弧度指定)。这给了我们一个完整的圆圈。如果您只指定了 1 * PI,则会得到一个半圆(180 度)。
  • 最后,我们使用fill()方法,它基本上表示“完成我们用 beginPath() 开始的路径的绘制,并用我们之前在 fillStyle 中指定的颜色填充它占据的区域”。

您现在可以开始测试您的对象了。

  1. 保存到目前为止的代码,并在浏览器中加载 HTML 文件。
  2. 打开浏览器的 JavaScript 控制台,然后刷新页面,以便画布大小更改为控制台打开时剩余的较小的可见视口。
  3. 输入以下内容以创建一个新的球体实例:
    js
    const testBall = new Ball(50, 100, 4, 4, "blue", 10);
    
  4. 尝试调用其成员:
    js
    testBall.x;
    testBall.size;
    testBall.color;
    testBall.draw();
    
  5. 当您输入最后一行时,您应该会看到球体在画布上的某个位置绘制自身。

更新球体的数据

我们可以绘制处于位置的球体,但要真正移动球体,我们需要某种更新函数。将以下代码添加到 Ball 的类定义中:

js
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 坐标——实际上,每次调用此方法时,球体都会移动。

现在这样就足够了;让我们继续进行动画!

动画球体

现在让我们让它变得有趣。我们现在将开始向画布中添加球体并对其进行动画处理。

首先,我们需要创建一个存储所有球体的位置,然后填充它。以下代码将完成此工作——现在将其添加到代码的底部:

js
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 中的数字以在数组中获得更多或更少的球体。根据您的计算机/浏览器的处理能力,指定几千个球体可能会大大降低动画速度!

接下来,将以下内容添加到代码的底部:

js
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() 方法再次运行该函数——当此方法重复运行并传递相同的函数名时,它会每秒运行该函数一定次数以创建平滑的动画。这通常以递归方式完成——这意味着函数在每次运行时都会调用自身,因此它会一遍又一遍地运行。

最后,将以下行添加到代码的底部——我们需要调用该函数一次以启动动画。

js
loop();

这就是基础知识——尝试保存并刷新以测试您的弹球!

添加碰撞检测

现在让我们来点有趣的,让我们为程序添加一些碰撞检测,以便我们的球体知道何时撞到另一个球体。

首先,将以下方法定义添加到您的 Ball 类中。

js
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 语句内部的代码才会执行。
  • 然后,我们使用一种常见的算法来检查两个圆的碰撞。我们基本上是在检查两个圆的任何区域是否有重叠。这在二维碰撞检测中有更详细的解释。
  • 如果检测到碰撞,则运行内部 if 语句中的代码。在本例中,我们只将两个圆的 color 属性设置为新的随机颜色。我们本可以做一些更复杂的事情,例如让球以逼真的方式互相弹开,但这将更加复杂。对于此类物理模拟,开发人员倾向于使用游戏或物理库,例如PhysicsJSmatter.jsPhaser等。

你还需要在动画的每一帧中调用此方法。更新你的 loop() 函数,在 ball.update() 后调用 ball.collisionDetect()

js
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);
}

再次保存并刷新演示,你就会看到你的球在碰撞时改变颜色!

注意:如果你在运行此示例时遇到问题,请尝试将你的 JavaScript 代码与我们的完成版本进行比较(也可以查看在线运行版本)。

总结

我们希望你在使用本模块中各种对象和面向对象的技术编写自己的真实随机弹跳球示例时玩得开心!这应该让你在使用对象方面获得了一些有用的练习,并提供了良好的现实世界上下文。

关于对象的文章到此结束——现在剩下的就是你在对象评估中测试你的技能了。

另请参阅