2D 碰撞檢測

2D 遊戲中碰撞檢測的演算法取決於可能發生碰撞的形狀型別(例如,矩形與矩形、矩形與圓形、圓形與圓形)。通常,你會有一個簡單的通用形狀來覆蓋實體,稱為“碰撞盒”,因此即使碰撞可能不是畫素級的精確,它看起來也足夠好,並且在多個實體上效能良好。本文回顧了在 2D 遊戲中提供碰撞檢測的最常用技術。

引擎程式碼

此頁面上的演示不依賴任何外部庫,因此我們自己實現了所有編排,包括渲染、處理使用者輸入以及呼叫每個實體的行為。程式碼如下所示(每個示例都不會重複)。

html
<div id="container"></div>
css
.entity {
  display: inline-block;
  position: absolute;
  height: 20px;
  width: 20px;
  background-color: blue;
}

.movable {
  left: 50px;
  top: 50px;
  background-color: red;
}

.collision-state {
  background-color: green !important;
}
js
const collider = {
  moveableEntity: null,
  staticEntities: [],
  checkCollision() {
    // Important: the isCollidingWith method is what we are implementing
    const isColliding = this.staticEntities.some((staticEntity) =>
      this.moveableEntity.isCollidingWith(staticEntity),
    );
    this.moveableEntity.setCollisionState(isColliding);
  },
};

const container = document.getElementById("container");

class BaseEntity {
  ref;
  position;
  constructor(position) {
    this.position = position;
    this.ref = document.createElement("div");
    this.ref.classList.add("entity");
    this.ref.style.left = `${this.position.x}px`;
    this.ref.style.top = `${this.position.y}px`;
    container.appendChild(this.ref);
  }
  shiftPosition(dx, dy) {
    this.position.x += dx;
    this.position.y += dy;
    this.redraw();
  }
  redraw() {
    this.ref.style.left = `${this.position.x}px`;
    this.ref.style.top = `${this.position.y}px`;
  }
  setCollisionState(isColliding) {
    if (isColliding && !this.ref.classList.contains("collision-state")) {
      this.ref.classList.add("collision-state");
    } else if (!isColliding) {
      this.ref.classList.remove("collision-state");
    }
  }
  isCollidingWith(other) {
    throw new Error("isCollidingWith must be implemented in subclasses");
  }
}

document.addEventListener("keydown", (e) => {
  e.preventDefault();
  switch (e.key) {
    case "ArrowLeft":
      collider.moveableEntity.shiftPosition(-5, 0);
      break;
    case "ArrowUp":
      collider.moveableEntity.shiftPosition(0, -5);
      break;
    case "ArrowRight":
      collider.moveableEntity.shiftPosition(5, 0);
      break;
    case "ArrowDown":
      collider.moveableEntity.shiftPosition(0, 5);
      break;
  }
  collider.checkCollision();
});

軸對齊邊界框

碰撞檢測最簡單的形式之一是兩個軸對齊的矩形之間的碰撞。軸對齊意味著沒有旋轉。該演算法透過確保矩形的 4 個邊之間沒有間隙來工作。任何間隙都意味著不存在碰撞。

js
class BoxEntity extends BaseEntity {
  width = 20;
  height = 20;

  isCollidingWith(other) {
    return (
      this.position.x < other.position.x + other.width &&
      this.position.x + this.width > other.position.x &&
      this.position.y < other.position.y + other.height &&
      this.position.y + this.height > other.position.y
    );
  }
}

圓形碰撞

碰撞檢測的另一個簡單形狀是兩個圓形之間的碰撞。該演算法透過獲取兩個圓的中心點,並確保中心點之間的距離小於兩個半徑之和來工作。

css
.entity {
  border-radius: 50%;
}
js
class CircleEntity extends BaseEntity {
  radius = 10;

  isCollidingWith(other) {
    const dx =
      this.position.x + this.radius - (other.position.x + other.radius);
    const dy =
      this.position.y + this.radius - (other.position.y + other.radius);
    const distance = Math.sqrt(dx * dx + dy * dy);
    return distance < this.radius + other.radius;
  }
}

注意:圓的 xy 座標指的是它們的左上角,因此我們需要加上半徑來比較它們的中心。

分離軸定理

這是一種碰撞演算法,可以檢測任何兩個多邊形之間的碰撞。它比上述方法更復雜,但功能更強大。像這樣的演算法的複雜性意味著我們需要考慮效能最佳化,這將在下一節中介紹。

實現 SAT 超出了本頁的範圍,請參閱下面的推薦教程。

  1. 分離軸定理 (SAT) 解釋
  2. 碰撞檢測和響應
  3. 使用分離軸定理進行碰撞檢測
  4. SAT (分離軸定理)
  5. 分離軸定理

碰撞效能

雖然其中一些碰撞檢測演算法的計算足夠簡單,但將每個實體與其他所有實體進行測試可能會浪費 CPU 週期。通常,遊戲會將碰撞分為兩個階段:粗略階段和精確階段。

粗略階段

粗略階段應該為您提供一個可能發生碰撞的實體列表。這可以透過空間資料結構來實現,該結構可以粗略瞭解實體的位置以及其周圍存在的事物。空間資料結構的一些示例包括四叉樹、R 樹或空間雜湊表。

精確階段

當您有一小部分實體需要檢查時,您將希望使用精確階段演算法(如上所列)來提供一個確切的答案,說明是否存在碰撞。