物件構建實踐

在之前的文章中,我們學習了所有重要的 JavaScript 物件理論和語法細節,為你打下了堅實的基礎。在本文中,我們將深入到一個實踐練習,透過一個有趣且多彩的結果,為你提供更多構建自定義 JavaScript 物件的實踐。

預備知識 熟悉 JavaScript 基礎(尤其是物件基礎)和本模組先前課程中涵蓋的面向物件 JavaScript 概念。
學習成果 在實際情境中練習使用物件和麵向物件技術。

讓我們來彈跳一些小球

在本文中,我們將編寫一個經典的“彈跳小球”演示,向你展示物件在 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) 是直接表示畫布繪圖區域的物件,允許我們在其上繪製 2D 圖形。

接下來,我們設定了名為 widthheight 的常量,並將 canvas 元素的寬度和高度(由 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
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() 方法在紙上描繪一個弧形。它的引數是:

    • 圓弧中心的 xy 位置 — 我們指定了小球的 xy 屬性。
    • 圓弧的半徑 — 在這種情況下,是球的 size 屬性。
    • 最後兩個引數指定了圓弧繪製的起始和結束度數。這裡我們指定 0 度和 2 * PI,這相當於 360 度(以弧度表示,令人惱火的是你必須以弧度指定)。這給我們一個完整的圓。如果你只指定了 1 * PI,你將得到一個半圓(180 度)。
  • 最後,我們使用 fill() 方法,它基本上表示“完成我們用 beginPath() 開始繪製的路徑,並用我們之前在 fillStyle 中指定的顏色填充它所佔據的區域。”

你現在就可以開始測試你的物件了。

  1. 儲存目前的 L 程式碼,並在瀏覽器中載入 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
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 座標——每次呼叫此方法時,球實際上都會移動。

暫時就這些了;讓我們繼續做動畫吧!

給小球新增動畫

現在讓我們來點樂趣。我們現在要開始將小球新增到畫布上,並對其進行動畫處理。

首先,我們需要建立一個地方來儲存我們所有的小球,然後填充它。以下程式碼將完成這項工作——現在將其新增到程式碼底部:

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
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 屬性設定為新的隨機顏色。我們本可以做更復雜的事情,比如讓小球更真實地相互彈開,但這會複雜得多。對於此類物理模擬,開發人員傾向於使用遊戲或物理庫,如 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 程式碼與我們的最終版本進行比較(也可檢視其即時執行)。

總結

我們希望你透過使用本模組中的各種物件和麵向物件技術,編寫自己的真實世界隨機彈跳球示例,玩得開心!這應該為你提供了使用物件的一些有益實踐,以及良好的實際應用場景。

關於物件課程就到這裡了——現在只剩下你在模組挑戰中測試你的技能了。

另見