物件構建實踐
在之前的文章中,我們學習了所有重要的 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 遊戲的基礎知識。