canvas核心技術-如何實現碰撞檢測

這篇是學習和回顧canvas系列筆記的第六篇,完整筆記詳見:canvas核心技術javascript

在上一篇canvas核心技術-如何實現複雜的動畫筆記中,咱們詳細討論了在製做複雜動畫時,須要考慮時間因素,物理因素等,同時還回顧瞭如何使用緩動函數來扭曲時間軸實現非線性運動,好比常見的緩入,緩出,緩入緩出等。在遊戲或者動畫中,運動的物體在變化的過程當中,它們是有可能碰撞在一塊兒的,那麼這一篇咱們就來詳細學習下如何進行碰撞檢測。html

邊界值檢測

最簡單的檢測手段就是邊界值檢測了,就是對一個運動的物體的某些屬性進行條件判斷,若是達到了這個條件,則說明發生了碰撞。例如在上一篇中的示例,小球自由下落,當在檢測小球是否與地面發生碰撞時,咱們是檢測小球下落的高度fh是否達到了小球自己距離地面的高度dh,若是fh>dh,則說明小球與地面發生了碰撞。java

let distance = ball.currentSpeed * t; 
    if (ball.offset + distance > ball.verticalHeight) {
      //落到地面了,發生了碰撞
      // ...
    } else {
      // 尚未落到地面,沒有發生碰撞
      ball.offset += distance;
    }
複製代碼

這裏是個人小球自由落體完整在線示例git

這種檢測方式很是的簡單且準確,在針對相似業務開發時,咱們能夠簡化成邊界值檢測。可是當咱們開發較爲複雜遊戲時,邊界值檢測一般不能很好的實現,爲了更加真實,它一般與其餘檢測方法一塊兒使用。github

外接圖形檢測

在canvas遊戲中,對於不規則的物體,好比運動的小人等,咱們能夠經過抽象成一個矩形,使得這個矩形剛好能夠包裹這個物體,在進行碰撞檢測時,就可使用這個矩形來代替實際的物體。這種方法,實際上就是經過抽象,將複雜簡單化,對於精確度不是那麼高的動畫或者遊戲,咱們直接使用這種外接圖形來檢測就能夠了。在抽象圖形的時候,咱們要根據具體的物體,好比小人能夠抽象成矩形,太陽就要抽象成圓了,把具體的物體抽象的跟它類似的形狀,這樣在檢測時就會更加準確。typescript

進行了圖形抽象以後,咱們在檢測就只需對圖形進行檢測了。對於兩個圖形是否發生碰撞,咱們只須要判斷它們是否存在相交的部分,若是存在相交的部分,那麼則能夠認爲是發生了碰撞,不然就沒有。下面,咱們分別來學習矩形和矩形的碰撞檢測,圓和圓的碰撞檢測,矩形和圓的碰撞檢測。canvas

矩形與矩形碰撞狀況,ide

這裏列舉兩個矩形發生碰撞的全部狀況,在canvas中具體代碼實現以下,函數

/* 判斷是否兩個矩形發生碰撞 */
  private didRectCollide(sprite: RectSprite, otherSprite: RectSprite) {
    let horizontal = sprite.left + sprite.width > otherSprite.left && sprite.left < otherSprite.left + otherSprite.width;
    let vertical = sprite.top < otherSprite.top + otherSprite.height && sprite.top + sprite.height > otherSprite.top;
    return horizontal && vertical;
  }
複製代碼

其實就是分別在水平方向和垂直方向判斷這兩個矩形是否發生重疊。post

圓和圓碰撞狀況,

判斷兩個圓是否發生碰撞,就是判斷兩個圓的圓心之間的距離是否小於它們的半徑之和,若是小於半徑之和,則發生碰撞,不然就沒有發生碰撞。主要就是計算兩個圓心之間的距離,能夠根據座標系中兩點之間距離公式獲得,

|AB| = \sqrt{(x_1-x_2)^2 + (y_1-y2)^2}

在canvas中具體代碼實現以下,

/* 判斷是否兩個圓發生碰撞 */
  private didCircleCollide(sprite: CircleSprite, otherSprite: CircleSprite) {
    return distance(sprite.x, sprite.y, otherSprite.x, otherSprite.y) < sprite.radius + otherSprite.radius;
  }
複製代碼

矩形和圓碰撞狀況,

這種狀況,就是判斷圓形到矩形上最近的一點的距離是否小於圓的半徑,若是小於圓的半徑,則發生碰撞,不然就沒有發生碰撞。咱們首先要找到圓距離矩形上最近的點的座標,這種就要考慮圓心在矩形左側,圓心在矩形上面,圓心在矩形右側,圓心在矩形下面,圓心在矩形裏面這五種狀況。若是圓心在矩形裏面,那麼必定是碰撞的。其餘四種狀況根據每一種狀況來計算獲得矩形上離圓心最近的一點,下面舉例其中一種狀況,其餘狀況原理相似,好比圓心在矩形左側,

這種狀況下,最近一點的X軸座標跟矩形左上角座標的X軸座標相等,跟圓心Y軸座標相等,這樣就能夠得出來了,(rect_x,circle_y)。在canvas中具體代碼實現以下,

/* 判斷是否矩形和圓形發生碰撞 */
  private didRectWidthCircleCollide(rectSprite: RectSprite, circleSprite: CircleSprite) {
    let closePoint = { x: undefined, y: undefined };
    if (circleSprite.x < rectSprite.left) {
      closePoint.x = rectSprite.left;
    } else if (circleSprite.x < rectSprite.left + rectSprite.width) {
      closePoint.x = circleSprite.x;
    } else {
      closePoint.x = rectSprite.left + rectSprite.width;
    }
    if (circleSprite.y < rectSprite.top) {
      closePoint.y = rectSprite.top;
    } else if (circleSprite.y < rectSprite.top + rectSprite.height) {
      closePoint.y = circleSprite.y;
    } else {
      closePoint.y = rectSprite.top + rectSprite.height;
    }
    return distance(circleSprite.x, circleSprite.y, closePoint.x, closePoint.y) < circleSprite.radius;
  }
複製代碼

這裏是個人外接圖形碰撞檢測在線示例

光線投射檢測

光線投射法:畫一條與物體的速度向量相重合的線,而後再從另一個待檢測物體出發,繪製第二條線,根據兩條線的交點位置來斷定是否發生碰撞。

光線投射法通常還會結合邊界值檢測來進行嚴格準確的判斷,這種方法要求咱們在動畫更新中,不斷計算出兩個速度向量的交點座標,根據交點座標判斷是否知足碰撞條件,交點知足了條件,咱們還要運用邊界值檢測方法來檢測運動物體是否知足邊界值條件,只有同時知足才判斷爲發生碰撞。這種檢測,準確度通常比較高,特別是適用於運動速度快的物體。以小球投桶示例,檢測代碼以下,

/* 是否發生碰撞 */
  public didCollide(ball: CircleSprite, bucket: ImageSprite) {
    let k1 = ball.verticalVelocity / ball.horizontalVelocity;
    let b1 = ball.y - k1 * ball.x;
    let inertSectionY = bucket.mockTop; //計算交點Y座標
    let insertSectionX = (inertSectionY - b1) / k1; //計算交點X座標
    return (
      insertSectionX > bucket.mockLeft &&
      insertSectionX < bucket.mockLeft + bucket.mockWidth &&
      ball.x > bucket.mockLeft &&
      ball.x < bucket.mockLeft + bucket.mockWidth &&
      ball.y > bucket.mockTop &&
      ball.y < bucket.mockTop + bucket.mockHeight
    );
  }
}
複製代碼

這裏是個人光線投射檢測在線示例

分離軸檢測

在判斷凸多邊形的碰撞檢測時,咱們可使用分離軸方法。在學習分離軸檢測以前,咱們須要先熟悉向量的一些基礎知識。

向量基礎知識:

  • 在平面二維座標系中,咱們可使用向量來表示某個點的位置。向量表示法就是從座標原點(0,0)指向目標點(x,y) 。
  • 兩個向量相減,結果是另一條新的向量。
  • 兩個向量作點積,能夠獲得投影的值。
  • 單位向量,就是長度爲1的向量,其實際做用是表示方向。
  • 一個向量垂直於另一個向量,咱們叫作法向量。

圖中能夠看到,\overrightarrow{oa} -\overrightarrow{ob} = \overrightarrow{ba}\overrightarrow{oa} * \overrightarrow{ob} = |od|。多餘凸多邊形的每一個頂點,咱們能夠用向量來表示。

分離軸檢測思路,

  1. 先獲取被檢測多邊形的全部的投影軸,通常只須要計算出多邊形對應邊的投影軸便可
  2. 計算出被檢測多邊形在每一條投影軸上的投影
  3. 判斷它們的投影是否重疊,若是存在在任意一條投影軸的投影不重疊,則說明它們沒有發生碰撞,不然就發生了碰撞
/* 判斷是否發生碰撞 */
  public didCollide(sprite: Sprite, otherSprite: Sprite) {
    let axes1 = sprite.type === 'circle' ? (sprite as Circle).getAxes(otherSprite as Polygon) : (sprite as Polygon).getAxes();
    let axes2 = otherSprite.type === 'circle' ? (otherSprite as Circle).getAxes(sprite as Polygon) : (otherSprite as Polygon).getAxes();
    // 第一步:獲取全部的投影軸
    // 第二步:獲取多邊形在各個投影軸的投影
    // 第三步:判斷是否存在一條投影軸上,多邊形的投影不相交,若是存在不相交的投影則直接返回false,若是有所的投影軸上的投影都存在相交,則說明相碰了。
    let axes = [...axes1, ...axes2];
    for (let axis of axes) {
      let projections1 = sprite.getProjection(axis);
      let projections2 = otherSprite.getProjection(axis);
      if (!projections1.overlaps(projections2)) {
        return false;
      }
    }
    return true;
  }
}
複製代碼

下面咱們就按照這三個步驟來,一步一步實現分離軸檢測方法。

獲取投影軸

在多邊形中,咱們是以邊來創建邊向量的,邊向量的法向量,就是這條邊的投影軸了。對於投影軸,咱們只需它的方向,因此通常會把它格式化爲單位向量。

//獲取凸多邊形的投影軸 
public getAxes() {
    let points = this.points;
    let axes = [];
    for (let i = 0, j = points.length - 1; i < j; i++) {
        let v1 = new Vector(points[i].x, points[i].y);
        let v2 = new Vector(points[i + 1].x, points[i + 1].y);
        axes.push(
            v1
            .subtract(v2)
            .perpendicular()
            .normalize(),
        );
    }
    let firstPoint = points[0];
    let lastPoint = points[points.length - 1];
    let v1 = new Vector(lastPoint.x, lastPoint.y);
    let v2 = new Vector(firstPoint.x, firstPoint.y);
    axes.push(
        v1
        .subtract(v2)
        .perpendicular()
        .normalize(),
    );
    return axes;
 }
複製代碼

獲取了待檢測圖形的投影軸以後,咱們就須要計算圖形在每條投影軸上的投影

public getProjection(v: Vector) {
    let min = Number.MAX_SAFE_INTEGER;
    let max = Number.MIN_SAFE_INTEGER;
    for (let point of this.points) {
      let p = new Vector(point.x, point.y);
      let dotProduct = p.dotProduct(v);
      min = Math.min(min, dotProduct);
      max = Math.max(max, dotProduct);
    }
    return new Projection(min, max);
  }
複製代碼

最後判斷投影是否重疊

/* 投影是否重疊 */
  overlaps(p: Projection) {
    return this.max > p.min && p.max > this.min;
  }
複製代碼

其中,若是是一個圓形與一個凸多邊形的檢測時,在計算圓對應的投影軸時比較特殊,圓只有一條投影軸,就是圓心與它距離多邊形最近頂點的向量,

//獲取圓的投影軸
  public getAxes(polygon: Polygon) {
    // 對於圓來講,獲取其投影軸就是將圓心與他距離多邊形最近頂點的連線
    let { x, y } = this;
    let nearestPoint = null;
    let nearestDistance = Number.MAX_SAFE_INTEGER;
    for (let [index, point] of polygon.points.entries()) {
      let d = distance(x, y, point.x, point.y);
      if (d < nearestDistance) {
        nearestDistance = d;
        nearestPoint = point;
      }
    }
    let v1 = new Vector(x, y);
    let v2 = new Vector(nearestPoint.x, nearestPoint.y);
    return [v1.subtract(v2).normalize()];
  }
複製代碼

這裏是個人分離軸檢測在線示例

小結

這篇筆記詳細記錄了2d圖形中碰撞檢測的方法,比較簡單的方法是外接圖形法和邊界值檢測法,它們相對不是那麼精確,比較複雜和精確的方法有光線投射法和分離軸法。根據不一樣的場景和精確度要求,咱們選擇不一樣的方法。其餘,除了上面幾種,還有像素檢測等方法也能夠實現碰撞檢測,像素檢測是以像素爲單位來檢測,若是存在不透明的像素在同一個座標上重疊,則說明發生了碰撞,具體實現能夠查看Pixel accurate collision detection with Javascript and Canvas

對於這幾種檢測方法,強力建議熟悉掌握分離軸法,由於它使用的範圍最爲普遍,對於任意的凸多邊形,它均可以較精確的檢測出來。因爲分離軸檢測法計算量通常比較大,因此在檢測以前,咱們先過濾掉那些根本不可能發生碰撞的圖形,通常方法是空間分隔法,或者過濾可視區間不可見的圖形等,而後再對較小的一部分可能發生碰撞的圖形來進行計算檢測,這樣能夠提高檢測的速度。

參考

相關文章
相關標籤/搜索