彈跳

這是 Gamedev Canvas 教程第 3 步,共 10 步。您可以在 Gamedev-Canvas-workshop/lesson3.html 找到完成本課後程式碼應有的樣子。

看到我們的球在移動很不錯,但它很快就從螢幕上消失了,限制了我們的樂趣!為了克服這一點,我們將實現一些碰撞檢測(稍後將在 更詳細地解釋),讓球從 Canvas 的四條邊反彈。

簡單的碰撞檢測

要檢測碰撞,我們將檢查球是否碰到(碰撞到)牆壁,如果碰到,我們將相應地改變其移動方向。

為了簡化計算,讓我們定義一個名為 ballRadius 的變數,它將儲存繪製圓的半徑並用於計算。將此新增到您的程式碼中,位於現有變數宣告的某個位置下方。

js
const ballRadius = 10;

現在,將 drawBall() 函式中繪製球的程式碼更新為如下所示:

js
ctx.arc(x, y, ballRadius, 0, Math.PI * 2);

彈跳頂部和底部

球有四面牆可以反彈 — 讓我們先關注頂部。我們需要在每一幀都檢查球是否碰到 Canvas 的頂部邊緣 — 如果是,我們將反轉球的移動方向,使其開始向相反方向移動並保持在可見邊界內。記住座標系從左上角開始,我們可以得出這樣的結論:

js
if (y + dy < 0) {
  dy = -dy;
}

如果球位置的 y 值小於零,則透過將其設定為自身的反值來改變 y 軸上的移動方向。如果球以每幀 2 畫素的速度向上移動,現在它將以 -2 畫素的速度“向上”移動,這實際上等於以每幀 2 畫素的速度向下移動。

上面的程式碼處理了球從頂部邊緣反彈的情況,現在讓我們考慮底部邊緣:

js
if (y + dy > canvas.height) {
  dy = -dy;
}

如果球的 y 位置大於 Canvas 的高度(記住我們從左上角開始計算 y 值,所以頂部邊緣從 0 開始,底部邊緣是 320 畫素,即 Canvas 的高度),然後透過像以前一樣反轉 y 軸移動來使其從底部邊緣反彈。

我們可以將這兩個語句合併為一個,以節省程式碼冗餘。

js
if (y + dy > canvas.height || y + dy < 0) {
  dy = -dy;
}

如果兩個語句中的任何一個為 true,則反轉球的移動。

彈跳左側和右側

我們已經處理了頂部和底部邊緣,所以讓我們考慮左側和右側。實際上非常相似,您所要做的就是為 x 重複 y 的語句。

js
if (x + dx > canvas.width || x + dx < 0) {
  dx = -dx;
}

if (y + dy > canvas.height || y + dy < 0) {
  dy = -dy;
}

此時,您應該將上面的程式碼塊插入到 draw() 函式中,就在閉合花括號之前。

球仍然會消失到牆裡!

此時測試您的程式碼,您會感到驚訝 — 現在我們有了一個能從畫布四邊反彈的球!然而,我們遇到了另一個問題 — 當球碰到每面牆時,它會在改變方向之前稍微沉入牆裡。

skyblue ball disappearing into the top of the white wall.

這是因為我們正在計算牆壁和球心之間的碰撞點,而我們應該為球的周長進行計算。球應該在碰到牆壁後立即反彈,而不是在已經半個球體都在牆裡之後,所以讓我們稍微調整一下我們的語句以包含這一點。將您最後新增的程式碼更新為如下所示:

js
if (x + dx > canvas.width - ballRadius || x + dx < ballRadius) {
  dx = -dx;
}
if (y + dy > canvas.height - ballRadius || y + dy < ballRadius) {
  dy = -dy;
}

當球心與牆壁邊緣之間的距離等於球的半徑時,它將改變移動方向。從一個邊緣的寬度中減去半徑,並在另一個邊緣加上半徑,讓我們對正確的碰撞檢測有印象 — 球應該像預期的那樣從牆壁上反彈。

Compare your code

讓我們再次檢查此部分的程式碼是否與您的程式碼相符,並進行一些嘗試。

js
const canvas = document.getElementById("myCanvas");
const ctx = canvas.getContext("2d");
const ballRadius = 10;
let x = canvas.width / 2;
let y = canvas.height - 30;
let dx = 2;
let dy = -2;

function drawBall() {
  ctx.beginPath();
  ctx.arc(x, y, ballRadius, 0, Math.PI * 2);
  ctx.fillStyle = "#0095DD";
  ctx.fill();
  ctx.closePath();
}

function draw() {
  ctx.clearRect(0, 0, canvas.width, canvas.height);
  drawBall();

  if (x + dx > canvas.width - ballRadius || x + dx < ballRadius) {
    dx = -dx;
  }
  if (y + dy > canvas.height - ballRadius || y + dy < ballRadius) {
    dy = -dy;
  }

  x += dx;
  y += dy;
}

function startGame() {
  setInterval(draw, 10);
}

const runButton = document.getElementById("runButton");
runButton.addEventListener("click", () => {
  startGame();
  runButton.disabled = true;
});

注意: 嘗試在球碰到牆壁時將其顏色更改為隨機顏色。

後續步驟

現在我們已經到了球既能移動又能停留在遊戲板上的階段。在第四章中,我們將實現一個可控制的擋板 — 請參閱 擋板和鍵盤控制