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;
}
}
注意:圆的 x 和 y 坐标指的是它们的左上角,因此我们需要加上半径来比较它们的中心。
分离轴定理
这是一种碰撞算法,可以检测任何两个凸多边形之间的碰撞。它比上述方法更复杂,但功能更强大。像这样的算法的复杂性意味着我们需要考虑性能优化,这将在下一节中介绍。
实现 SAT 超出了本页的范围,请参阅下面的推荐教程。
碰撞性能
虽然其中一些碰撞检测算法的计算足够简单,但将每个实体与其他所有实体进行测试可能会浪费 CPU 周期。通常,游戏会将碰撞分为两个阶段:粗略阶段和精确阶段。
粗略阶段
粗略阶段应该为您提供一个可能发生碰撞的实体列表。这可以通过空间数据结构来实现,该结构可以粗略了解实体的位置以及其周围存在的事物。空间数据结构的一些示例包括四叉树、R 树或空间哈希表。
精确阶段
当您有一小部分实体需要检查时,您将希望使用精确阶段算法(如上所列)来提供一个确切的答案,说明是否存在碰撞。