【30分鐘學完】canvas動畫|遊戲基礎(4):邊界與碰撞

前言

本系列前幾篇中常出現物體跑到畫布外的狀況,本篇就是爲了解決這個問題。
閱讀本篇前請先打好前面的基礎。
本人能力有限,歡迎牛人共同討論,批評指正。javascript

越界檢測

假定物體是個圓形,如圖其圓心座標便是物體的x軸和y軸座標。
越界是常見的場景,通常會有兩種場景的越界:一是整個物體移出區域,二是物體接觸到區域邊界。咱們以畫布邊界爲例進行討論,示例中矩形邊界便是:html

let top = 0;
let bottom = canvas.height;
let left = 0;
let right = canvas.width;

邊界

整個物體移出區域

要整個物體離開範圍纔算越界,則可得越界條件以下,如下任何一項爲true便可斷定越界。java

// 右側越界
object.x - object.width/2 > right
// 左側越界
object.x + object.width/2 < left
// 上部越界
object.y + object.height/2 < top
// 下部越界
object.y - object.height/2 > bottom

物體接觸到區域邊界

物體接觸到區域邊界就算越界,則可得越界條件以下,如下任何一項爲true便可斷定越界。git

// 右側越界
object.x + object.width/2 > right
// 左側越界
object.x - object.width/2 < left
// 上部越界
object.y - object.height/2 < top
// 下部越界
object.y + object.height/2 > bottom

越界了該怎麼辦

搞明白越界條件後,接下來討論越界以後的處理辦法,通常是一下四種。github

將物體移除

這是最簡單的處理辦法,屬於整個物體移出區域纔算越界的狀況。
下面的例子會先批量建立ball,保存在balls數組裏,每次動畫循環都會遍歷這個數組,依次輸入draw()函數,改變ball的位置並檢測是否越界。下面只列出draw()函數的代碼。
完整示例:清除越界圓canvas

function draw(ball, pos) {
  // 依據球的速度改變球的位置
  ball.x += ball.vx;
  ball.y += ball.vy;
  // 檢查是否越界
  if (ball.x - ball.radius > canvas.width || ball.x + ball.radius < 0 || ball.y - ball.radius > canvas.height || ball.y + ball.radius < 0) {
    // 在數組中清除越界的球
    balls.splice(pos, 1);
    // 打印提示
    if (balls.length > 0) {
      log.value += `Removed ${ball.id}\n`;
      log.scrollTop = log.scrollHeight;
    } else {
      log.value += 'All gone!\n';
    }
  }
  // 畫球
  ball.draw(context);
}

將其物體置回邊界內

屬於整個物體移出區域纔算越界的狀況。
下面的例子也是把建立的ball保存在balls數組裏,但ball的初始位置都是畫布中間的下部,若是檢測到有ball越界,則會重置ball的位置。下面只列出draw()函數的代碼。
完整示例:彩色噴泉數組

function draw(ball) {
  // 依據球的速度改變球的位置,這裏包含了僞重力
  ball.vy += gravity;
  ball.x += ball.vx;
  ball.y += ball.vy;
  // 檢測是否越界
  if (ball.x - ball.radius > canvas.width || ball.x + ball.radius < 0 || ball.y - ball.radius > canvas.height || ball.y + ball.radius < 0) {
    // 重置ball的位置
    ball.x = canvas.width / 2;
    ball.y = canvas.height;
    // 重置ball的速度
    ball.vx = Math.random() * 6 - 3;
    ball.vy = Math.random() * -10 - 10;
    // 打印提示
    log.value = `Reset ${ball.id}\n`;
  }
  // 畫球
  ball.draw(context);
}

屏幕環繞

屬於整個物體移出區域纔算越界的狀況。
屏幕環繞就是讓同一個物體出如今邊界內的另外一個位置,若是一個物體從屏幕左側移出,它就會在屏幕右側再次出現,反之亦然,上下也是同理。
這裏比前面的要稍微複雜的判斷物體躍的是那邊的界,僞代碼以下:dom

if(object.x - object.width/2 > right){
    object.x = left - object.widht/2;
}else if(object.x + object.width/2 < left){
    object.x = right + object.width/2;
}
if(object.y - object.height/2 > bottom){
    object.y = top - object.height/2;
}else if(object.y + object.height/2 < top){
    obejct.y = bottom + object.height/2;
}

反彈(粗略版)

這是較複雜的一種狀況,屬於物體接觸到區域邊界就算越界的狀況。基本思路:函數

  1. 檢查物體是否越過任意邊界;
  2. 若是發生越界, 當即將物體置回邊界;
  3. 反轉物體的速度向量的方向。

下面的示例是一個ball在畫布內移動,撞到邊界就反彈,反彈核心代碼以下。
完整示例:反彈球(粗略版)工具

if (ball.x + ball.radius > right) {
  ball.x = right - ball.radius;
  vx *= -1;
} else if (ball.x - ball.radius < left) {
  ball.x = left + ball.radius;
  vx *= -1;
}
if (ball.y + ball.radius > bottom) {
  ball.y = bottom - ball.radius;
  vy *= -1;
} else if (ball.y - ball.radius < top) {
  ball.y = top + ball.radius;
  vy *= -1;
}

反彈(完美版)

咋看彷佛效果不錯,但仔細想一想,咱們這樣將物體置回邊界的作法是準確的嗎?
答案是否認的,理想反彈與實際反彈是不一樣的,以下圖:

理想反彈與實際反彈

從圖中咱們能夠清除的知道,ball其實是不太可能會在理想反彈點反彈的,由於若是速度過大,計算位置時ball已經越過「理想反彈點」到達「實際反彈點」,而咱們若是隻是將ball的x軸座標簡單粗暴移到邊界上,那仍是不多是「理想反彈點」,也就是說這種反彈方法不許確。
那麼,完美反彈的思路就明確了,咱們須要找到「理想反彈點」,並將ball置到該點。若是是左右邊越界,則算出"理想反彈點"與「實際反彈點」在y軸上的距離;若是是上下邊越界,則算出"理想反彈點"與「實際反彈點」在x軸上的距離。如圖,思路以左右邊越界爲例:

求理想反彈點

  1. 由速度可求得物體的方向弧度angle;
  2. 算出"實際反彈點"和「理想反彈點」在x軸上的距離;
  3. 依據正切求"實際反彈點"和「理想反彈點」在y軸上的距離;
  4. 「理想反彈點」的y軸座標便是"實際反彈點"加上這段距離。

改造後的核心代碼以下,至於有沒有必要多作這麼多運算,這就要權衡性能和精密性了。
完整示例:反彈球(完美版)

if (ball.x + ball.radius > right) {
  const dx = ball.x - (right - ball.radius);
  const dy = Math.tan(angle) * dx;
  ball.x = right - ball.radius;
  ball.y += dy;
  vx *= bounce;
} else if (ball.x - ball.radius < left) {
  const dx = ball.x - (left + ball.radius);
  const dy = Math.tan(angle) * dx;
  ball.x = left + ball.radius;
  ball.y += dy;
  vx *= bounce;
}
if (ball.y + ball.radius > bottom) {
  const dy = ball.y - (bottom - ball.radius);
  const dx = dy / Math.tan(angle);
  ball.y = bottom - ball.radius;
  ball.x += dx;
  vy *= bounce;
} else if (ball.y - ball.radius < top) {
  const dy = ball.y - (top + ball.radius);
  const dx = dy / Math.tan(angle);
  ball.y = top + ball.radius;
  ball.x += dx;
  vy *= bounce;
}

碰撞檢測

和越界檢查很像,咱們擴展到兩個物體間的碰撞檢測,通常經常使用的有以下兩種辦法。

基於幾何圖形的碰撞檢測

通常是用在檢測矩形的碰撞,原理就是判斷一個物體是否和另外一個物體有重疊。
下面直接給出兩個檢測的工具函數。完整示例:

// 兩個矩形碰撞檢測
function intersects(rectA, rectB) {
  return !(rectA.x + rectA.width < rectB.x ||
    rectB.x + rectB.width < rectA.x ||
    rectA.y + rectA.height < rectB.y ||
    rectB.y + rectB.height < rectA.y);
};
// 矩形與點碰撞檢測
function containsPoint(rect, x, y) {
  return !(x < rect.x || x > rect.x + rect.width || y < rect.y || y > rect.y + rect.height);
};

基於距離的碰撞檢測

通常是用在檢測圓形的碰撞,原理就是判斷兩個物體是否足夠近到發生碰撞。
對於圓來講,只要兩個圓心距離小於兩圓半徑之和,那咱們就可斷定爲碰撞。圓心距離可經過勾股定理求得。核心代碼以下:
完整示例:兩圓基於距離的碰撞演示

const dx = ballB.x - ballA.x;
const dy = ballB.y - ballA.y;
const dist = Math.sqrt(dx ** 2 + dy ** 2);

if (dist < ballA.radius + ballB.radius) {
  log.value = 'Hit!';
} else {
  log.value = '';
}
相關文章
相關標籤/搜索