对象构建实践

在之前的文章中,我们研究了所有基本的 JavaScript 对象理论和语法细节,为你提供了坚实的基础。在本文中,我们将深入进行实践练习,为你提供构建自定义 JavaScript 对象的更多练习,并带来有趣且丰富多彩的结果。

¥In previous articles we looked at all the essential JavaScript object theory and syntax details, giving you a solid base to start from. In this article we dive into a practical exercise, giving you some more practice in building custom JavaScript objects, with a fun and colorful result.

先决条件: 对 HTML 和 CSS 有基本了解,熟悉 JavaScript 基础知识(请参阅 第一步架构模块)和 OOJS 基础知识(请参阅 对象介绍)。
目标: 获得在现实环境中使用对象和面向对象技术的一些练习。

让我们弹一些球

¥Let's bounce some balls

在本文中,我们将编写一个经典的 "弹跳球" 演示,向你展示对象在 JavaScript 中的用途。我们的小球会在屏幕上弹跳,当它们相互接触时会改变颜色。完成的示例看起来有点像这样:

¥In this article we will write a classic "bouncing balls" demo, to show you how useful objects can be in JavaScript. Our little balls will bounce around on the screen, and change color when they touch each other. The finished example will look a little something like this:

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.

此示例将使用 画布 API 将球绘制到屏幕上,并使用 requestAnimationFrame API 为整个显示设置动画 - 你不需要具备这些 API 的任何先前知识,我们希望当你了解这些 API 时,你已经了解了 读完本文后,你将会有兴趣进一步探索它们。在此过程中,我们将使用一些漂亮的对象,并向你展示一些不错的技术,例如将球从墙上弹起,并检查它们是否互相撞击(也称为碰撞检测)。

¥This example will make use of the Canvas API for drawing the balls to the screen, and the requestAnimationFrame API for animating the whole display — you don't need to have any previous knowledge of these APIs, and we hope that by the time you've finished this article you'll be interested in exploring them more. Along the way, we'll make use of some nifty objects, and show you a couple of nice techniques like bouncing balls off walls, and checking whether they have hit each other (otherwise known as collision detection).

入门

¥Getting started

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

¥To begin with, make local copies of our index.html, style.css, and main.js files. These contain the following, respectively:

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

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

¥The first part of the script looks like so:

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

¥This script gets a reference to the <canvas> element, then calls the getContext() method on it to give us a context on which we can start to draw. The resulting constant (ctx) is the object that directly represents the drawing area of the canvas and allows us to draw 2D shapes on it.

接下来,我们设置名为 widthheight 的常量,以及画布元素的宽度和高度(由 canvas.widthcanvas.height 属性表示)等于浏览器视口(网页出现的区域 - 这可以是 从 Window.innerWidthWindow.innerHeight 属性获得)。

¥Next, we set constants called width and height, and the width and height of the canvas element (represented by the canvas.width and canvas.height properties) to equal the width and height of the browser viewport (the area which the webpage appears on — this can be gotten from the Window.innerWidth and Window.innerHeight properties).

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

¥Note that we are chaining multiple assignments together, to get the variables all set quicker — this is perfectly OK.

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

¥Then we have two helper functions:

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

¥The random() function takes two numbers as arguments, and returns a random number in the range between the two. The randomRGB() function generates a random color represented as an rgb() string.

在我们的程序中建模球

¥Modeling a ball in our program

我们的程序将包含大量在屏幕上弹跳的球。由于这些球的行为方式相同,因此用一个对象来表示它们是有意义的。让我们首先将以下类定义添加到代码底部。

¥Our program will feature lots of balls bouncing around the screen. Since these balls will all behave in the same way, it makes sense to represent them with an object. Let's start by adding the following class definition to the bottom of our code.

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

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

¥So far this class only contains a constructor, in which we can initialize the properties each ball needs in order to function in our program:

  • xy 坐标 — 球在屏幕上开始的水平和垂直坐标。该范围可以在 0(左上角)到浏览器视口(右下角)的宽度和高度之间。
  • 水平和垂直速度(velXvelY) - 每个球都有一个水平和垂直速度;实际上,当我们为球设置动画时,这些值会定期添加到 x/y 坐标值中,以便在每一帧上将它们移动这么多。
  • color — 每个球都有一种颜色。
  • size - 每个球都有一个大小 - 这是它的半径,以像素为单位。

这处理了属性,但是方法呢?我们想让我们的球在我们的计划中真正做一些事情。

¥This handles the properties, but what about the methods? We want to get our balls to actually do something in our program.

画球

¥Drawing the ball

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

¥First add the following draw() method to the Ball class:

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

使用此函数,我们可以通过调用我们之前定义的 2D 画布上下文 (ctx) 的一系列成员,告诉球将自己绘制到屏幕上。上下文就像纸,现在我们想要命令我们的笔在上面画一些东西:

¥Using this function, we can tell the ball to draw itself onto the screen, by calling a series of members of the 2D canvas context we defined earlier (ctx). The context is like the paper, and now we want to command our pen to draw something on it:

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

你已经可以开始测试你的对象了。

¥You can start testing your object out already.

  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. 当你输入最后一行时,你应该看到球在画布上的某处绘制。

更新球的数据

¥Updating the ball's data

我们可以将球绘制到位,但要实际移动球,我们需要某种更新函数。在 Ball 的类定义中添加以下代码:

¥We can draw the ball in position, but to actually move the ball, we need an update function of some kind. Add the following code inside the class definition for 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)。

¥The first four parts of the function check whether the ball has reached the edge of the canvas. If it has, we reverse the polarity of the relevant velocity to make the ball travel in the opposite direction. So for example, if the ball was traveling upwards (negative velY), then the vertical velocity is changed so that it starts to travel downwards instead (positive velY).

在这四种情况下,我们正在检查:

¥In the four cases, we are checking to see:

  • 如果 x 坐标大于画布的宽度(球离开右边缘)。
  • 如果 x 坐标小于 0(球离开左边缘)。
  • 如果 y 坐标大于画布的高度(球离开底部边缘)。
  • 如果 y 坐标小于 0(球离开顶部边缘)。

在每种情况下,我们都将球的 size 纳入计算中,因为 x/y 坐标位于球的中心,但我们希望球的边缘从周边反弹 - 我们不希望球 在屏幕开始反弹之前离开屏幕一半。

¥In each case, we include the size of the ball in the calculation because the x/y coordinates are in the center of the ball, but we want the edge of the ball to bounce off the perimeter — we don't want the ball to go halfway off the screen before it starts to bounce back.

最后两行将 velX 值添加到 x 坐标,将 velY 值添加到 y 坐标 - 每次调用此方法时,球实际上都会移动。

¥The last two lines add the velX value to the x coordinate, and the velY value to the y coordinate — the ball is in effect moved each time this method is called.

现在就这样了;让我们继续看一些动画吧!

¥This will do for now; let's get on with some animation!

为球设置动画

¥Animating the ball

现在让我们让这件事变得有趣。我们现在要开始将球添加到画布上,并为它们设置动画。

¥Now let's make this fun. We are now going to start adding balls to the canvas, and animating them.

首先,我们需要创建一个地方来存储所有的球,然后填充它。以下代码将完成这项工作 - 现在将其添加到代码的底部:

¥First, we need to create somewhere to store all our balls and then populate it. The following will do this job — add it to the bottom of your code now:

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()es 到 balls 数组的末尾,但前提是数组中的球数小于 25。因此,当数组中有 25 个球时,就不会再有球被推入。你可以尝试改变 balls.length < 25 中的数量,以在数组中获得更多或更少的球。根据你的计算机/浏览器的处理能力,指定数千个球可能会大大减慢动画速度!

¥The while loop creates a new instance of our Ball() using random values generated with our random() and randomRGB() functions, then push()es it onto the end of our balls array, but only while the number of balls in the array is less than 25. So when we have 25 balls in the array, no more balls will be pushed. You can try varying the number in balls.length < 25 to get more or fewer balls in the array. Depending on how much processing power your computer/browser has, specifying several thousand balls might slow down the animation rather a lot!

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

¥Next, add the following to the bottom of your code:

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() 函数执行以下操作:

¥All programs that animate things generally involve an animation loop, which serves to update the information in the program and then render the resulting view on each frame of the animation; this is the basis for most games and other such programs. Our loop() function does the following:

  • 将画布填充颜色设置为半透明黑色,然后使用 fillRect() 在画布的整个宽度和高度上绘制一个颜色矩形(四个参数提供起始坐标以及所绘制矩形的宽度和高度)。这用于在绘制下一帧之前掩盖前一帧的绘制。如果你不这样做,你只会看到长蛇在画布上蠕动,而不是球在移动!填充的颜色设置为半透明 rgb(0 0 0 / 25%),以允许前几帧稍微透过,在球移动时在球后面产生小轨迹。如果将 0.25 更改为 1,你将不再看到它们。尝试改变这个数字来看看它的效果。
  • 循环遍历 balls 数组中的所有球,并运行每个球的 draw()update() 函数以在屏幕上绘制每个球,然后及时对下一帧的位置和速度进行必要的更新。
  • 使用 requestAnimationFrame() 方法再次运行该函数 - 当重复运行该方法并传递相同的函数名称时,它每秒运行该函数一定次数以创建平滑的动画。这通常是递归完成的 - 这意味着函数每次运行时都会调用自身,因此它会一遍又一遍地运行。

最后,将以下行添加到代码底部 - 我们需要调用该函数一次才能开始动画。

¥Finally, add the following line to the bottom of your code — we need to call the function once to get the animation started.

js
loop();

这就是基础知识 - 尝试保存并刷新来测试你的弹跳球!

¥That's it for the basics — try saving and refreshing to test your bouncing balls out!

添加碰撞检测

¥Adding collision detection

现在为了好玩,让我们在程序中添加一些碰撞检测,这样我们的球就知道它们何时撞到了另一个球。

¥Now for a bit of fun, let's add some collision detection to our program, so our balls know when they have hit another ball.

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

¥First, add the following method definition to your Ball class.

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

这个方法有点复杂,所以如果你暂时不明白它是如何工作的,请不要担心。解释如下:

¥This method is a little complex, so don't worry if you don't understand exactly how it works for now. An explanation follows:

  • 对于每个球,我们需要检查其他所有球,看看它是否与当前球发生碰撞。为此,我们启动另一个 for...of 循环来循环遍历 balls[] 数组中的所有球。
  • 在 for 循环中,我们立即使用 if 语句来检查当前循环的球是否与我们当前正在检查的球是同一个球。我们不想检查球是否与自身发生碰撞!为此,我们检查当前的球(即,正在调用其碰撞检测方法的球)是否与循环球(即,碰撞检测中 for 循环的当前迭代所引用的球)相同。 方法)。然后我们使用 ! 来否定检查,这样 if 语句内的代码仅在它们不相同时才运行。
  • 然后我们使用通用算法来检查两个圆的碰撞。我们基本上是在检查两个圆的区域是否重叠。这在 2D 碰撞检测 中有进一步解释。
  • 如果检测到冲突,则运行内部 if 语句内的代码。在本例中,我们仅将两个圆的 color 属性设置为新的随机颜色。我们本来可以做一些更复杂的事情,比如让球真实地相互弹开,但实现起来会复杂得多。对于此类物理模拟,开发者倾向于使用游戏或物理库,例如 PhysicsJSmatter.js移相器 等。

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

¥You also need to call this method in each frame of the animation. Update your loop() function to call ball.collisionDetect() after ball.update():

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

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

¥Save and refresh the demo again, and you'll see your balls change color when they collide!

注意:如果你在运行此示例时遇到困难,请尝试将你的 JavaScript 代码与我们的 成品版 进行比较(另请参阅 实时运行)。

¥Note: If you have trouble getting this example to work, try comparing your JavaScript code against our finished version (also see it running live).

概括

¥Summary

我们希望你使用整个模块中的各种对象和面向对象技术来编写自己的真实世界随机弹跳球示例,并从中获得乐趣!这应该为你提供了一些使用对象的有用实践以及良好的现实环境。

¥We hope you had fun writing your own real-world random bouncing balls example, using various object and object-oriented techniques from throughout the module! This should have given you some useful practice in using objects, and good real-world context.

对象文章就这样了 - 现在剩下的就是测试你在对象评估方面的技能了。

¥That's it for object articles — all that remains now is for you to test your skills in the object assessment.

也可以看看

¥See also