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 树或空间哈希表。

精确阶段

当您有一小部分实体需要检查时,您将希望使用精确阶段算法(如上所列)来提供一个确切的答案,说明是否存在碰撞。