物件構建練習

在之前的文章中,我們回顧了所有 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 程式碼與我們的完成版本進行比較(也可以檢視線上執行版本)。

總結

我們希望你在使用本模組中各種物件和麵向物件的技術編寫自己的真實隨機彈跳球示例時玩得開心!這應該讓你在使用物件方面獲得了一些有用的練習,並提供了良好的現實世界上下文。

關於物件的文章到此結束——現在剩下的就是你在物件評估中測試你的技能了。

另請參閱