[JS 小東西] 利用 JavaScript 在網頁實作彈跳球

這篇是我搬運自己以前在 github page 上 2020年 7 月的文章,最近要開始慢慢搬家、把那個網站改成實驗其他東西的地方,過程中還要修飾當初的語句也是略累。

為了複習關於物件移動的概念,我用網路上所能找到的最簡單的例子去理解。後來決定實作看看彈力球。


所要思考的元素

彈力球顧名思義是會彈來彈去的球,在一個有限的空間裡面遇到牆壁會反彈,我們需要思考的東西很簡單:

  1. 畫布的背景
  2. 球:速度、外觀、位置、運動方式、和牆壁的碰撞反應
  3. 如果要有不同的球,則需要考慮不同大小、速度等等,需要設定一個隨機取值的東西。

前端部分

在前端顯示畫布,先寫好簡單的設定(標題、畫布、引入待會寫的 js 檔)。

噢我發現無法直接在網站上寫這東西,好像會引發不可預期的錯誤,待我研究一下,先用圖片頂著:

背後的 javascript

接下來就是新增一個上面要引入的 main.js 檔案。

設定隨機取值的函式

上面的需求有提到,我需要能夠回傳在某個範圍內的隨機值。功能是輸入最小值和最大值後,可以幫我任意挑出這兩個數之間的某個數字。

function random(min, max) {
  var num = Math.floor(Math.random() * (max - min)) + min;
  return num;
}

方法說明:
Math.random() : 回傳隨機浮點數 \(x\),其中 \(0 \leq x < 1\)。
Math.floor(x):回傳小於等於 \(x\) 的最大整數。

抓取畫布大小

要先設定讓球彈跳的範圍(畫布大小):

const canvas = document.querySelector('canvas');
const ctx = canvas.getContext('2d');

const width = canvas.width = window.innerWidth;
const height = canvas.height = window.innerHeight;

製造一顆球

要開始實作我們的主角:球。一顆新的球產生,要有位置、大小(半徑)、速度、顏色。其中位置就可以使用上方我們剛寫完的 random() 函式產生隨機位置,分成 x 座標和 y 座標。

velXvelY 分別是球的 x 方向和 y 方向的速度,color 以及 size 是顏色和大小,也是一樣在某個範圍內隨機產生。這樣子每次新的球的生成都會不太一樣。

function Ball() {
  this.x = random(0, width);
  this.y = random(0, height);
  this.velX = random(-7, 7);
  this.velY = random(-7, 7);
  this.color = 'rgb(' + random(0, 255) + ',' + random(0, 255) + ',' + random(0, 255) + ')';
  this.size = random(10, 20);
}

畫出新生成的球

要利用 canvas 的 arc() 方法畫出這個球,並且填色。
arc(x, y, radius, startAngle, endAngle, anticlockwise)

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

球的碰到牆壁的反應

接下來就是重頭戲了。像是動畫影格的概念,想讓東西動起來,實際上背後就是拆解成讓它的位置隨著時間的增加而變動。接著考慮例外狀況:碰到牆了怎麼辦?要怎麼判斷有沒有碰到牆?

前面設定了球的速度和位置這時候就派上用場了。

以 x 方向為例子:要判斷有沒有碰左邊的牆,利用球的半徑、目前位置,去跟畫布的寬度比較,如下圖所示:

如果判斷是碰到牆壁了,那麼 x 方向的速度就必須設定為目前速度的反方向(回彈)。

/*碰到左邊的牆*/
if ((this.x - this.size) <= 0) {
  this.velX = -(this.velX);
}

/*碰到右邊的牆*/
if ((this.size + this.x) >= width) {
  this.velX = -(this.velX);
}

至於y方向的大同小異。

讓球移動(變更位置)

搞定好速度的變化後,要怎麼讓球應用在畫布上,順利移動呢?先前提到,動畫簡單講就是讓東西的位置隨著時間而變動。所以希望每次呼叫這個函數時,能夠依修正的速度變更位置。

簡單來說就是以前國高中很常在算的速度變化XD
this.x += this.velX;
this.y += this.velY;

如果寫得完整一點、考慮各個方向的話,如下方:

Ball.prototype.update = function () {
  if ((this.size + this.x) >= width) {
    this.velX = -(this.velX);
  }
  if ((this.x - this.size) <= 0) {
    this.velX = -(this.velX);
  }
  if ((this.size + this.y) >= height) {
    this.velY = -(this.velY);
  }
  if ((this.y - this.size) <= 0) {
    this.velY = -(this.velY);
  }
  this.x += this.velX;
  this.y += this.velY;
}

讓動作重複執行

基本上一顆球的設定都寫好了,接下來是設定畫布狀態(幾顆球、要不要重複、下一個影格產生後畫布怎麼處理……)以及通知瀏覽器做事。

  1. 首先製造個空間放置這些球
  2. 初始化小球後,放進儲存空間,並限制小球顆數(我設置25顆)
  3. 讓球開始動作,每顆球畫出來、更新。

最後會用到一個方法:requestAnimationFrame()。這個方法是通知瀏覽器我們想要產生動畫,並且要求瀏覽器在下次重繪畫面前呼叫特定函數更新動畫。

var balls = [];
function loop() {
  while (balls.length < 25) {
    var ball = new Ball();
    balls.push(ball);
  }
  for (i = 0; i < balls.length; i++) {
    balls[i].draw();
    balls[i].update();
  }
  requestAnimationFrame(loop);
}

前面說過,動畫是位置隨著時間演進。但如果沒有消除前一個瞬間的東西,下個瞬間又畫上去,那就只是一般單純畫畫。所以我們必須在繪製下一個影格之前,覆蓋前一個影格。

利用 fillRect(x, y, width, height) 方法後,發現單純彈跳有些單調,想加入小尾巴的效果,實際上就是疊加透明層上去,隨著時間增加,疊加的層數越多,先前產生的小球會漸漸消失,看起來就像是彗星的尾巴一樣:ctx.fillStyle = 'rgba(0, 0, 0, 0.25)';

因此 loop() 這個函式所要做的完整事情像這樣:

var balls = []; 
function loop() {
  ctx.fillStyle = 'rgba(0, 0, 0, 0.25)';
  ctx.fillRect(0, 0, width, height);

  while (balls.length < 25) {
    var ball = new Ball();
    balls.push(ball);
  }
  for (i = 0; i < balls.length; i++) {
    balls[i].draw();
    balls[i].update();
  }
  requestAnimationFrame(loop);
}
loop(); //記得執行

一般來說用網頁多少都會寫個 css,不過這裡大部分都用 canvas 畫完了,css 沒多少,就不拉出來講述了。

實作後就像這樣,因為這有壓縮到畫質,實際上漂亮很多。

參考資料


後記

這個彈跳球的範例還能延伸到乒乓球,未來會再寫寫看。另外應該也有不少人看到這個例子想到能不能連球體互相碰撞也反彈?這就牽涉到更複雜的設定了(也是本例中直接忽略的部分)。另外有些函式庫值得參考:PhysicsJSmatter.jsPhaser,這種物件建構很常用在遊戲製作上,對遊戲有興趣的可以多翻以上幾個網頁。

另外我沒想到搬運過來文章後還有新的後記好寫耶,竟然出現解析錯誤!好像是因為內嵌的 html 標籤範例讓網頁更新出錯?又挖了一個坑給我自己跳了……

讓我知道你在想什麼!

Picture of ALEX

ALEX

藍白拖愛好者,一事無成話偏多

Recent Posts

C++

NLP