引言
AABB(Axis-Aligned Bounding Box,轴对齐包围盒)碰撞检测是2D游戏物理引擎的基石。从早期的《吃豆人》到现代的《王者荣耀》,几乎每一款游戏都离不开高效的碰撞检测系统。当场景中同时存在成百上千个动态对象时,暴力O(n²)的两两检测将迅速成为性能瓶颈。本文将用Java实现一套完整的AABB碰撞检测系统,核心讲解四叉树空间划分算法如何将碰撞检测复杂度从O(n²)降低到O(n log n)。
AABB基础概念
轴对齐包围盒定义
AABB是与坐标轴平行的矩形,由左下角坐标 (minX, minY) 和右上角坐标 (maxX, maxY) 唯一确定:
public class AABB {
public float minX, minY, maxX, maxY;
public int id; // 对象唯一标识
public AABB(int id, float minX, float minY, float maxX, float maxY) {
this.id = id;
this.minX = minX;
this.minY = minY;
this.maxX = maxX;
this.maxY = maxY;
}
/**
* 判断两个AABB是否相交
* 分离轴定理的简化版:若两矩形在X轴或Y轴上投影不重叠,则不相交
*/
public boolean intersects(AABB other) {
return this.minX <= other.maxX && this.maxX >= other.minX &&
this.minY <= other.maxY && this.maxY >= other.minY;
}
/**
* 计算AABB的面积
*/
public float area() {
return (maxX - minX) * (maxY - minY);
}
/**
* 判断点是否在AABB内
*/
public boolean contains(float x, float y) {
return x >= minX && x <= maxX && y >= minY && y <= maxY;
}
@Override
public String toString() {
return String.format("AABB[%d](%.1f,%.1f,%.1f,%.1f)", id, minX, minY, maxX, maxY);
}
}
暴力检测与性能瓶颈
import java.util.*;
public class BruteForceCollision {
/**
* 暴力两两检测所有对象对
* 时间复杂度: O(n²)
*/
static List<String> bruteForceDetect(List<AABB> objects) {
List<String> collisions = new ArrayList<>();
int n = objects.size();
for (int i = 0; i < n; i++) {
for (int j = i + 1; j < n; j++) {
if (objects.get(i).intersects(objects.get(j))) {
collisions.add(objects.get(i).id + "-" + objects.get(j).id);
}
}
}
return collisions;
}
}
当 n = 1000 时,需要进行约50万次相交测试;当 n = 10000 时,测试次数飙升至约5000万次。这在实时游戏中是不可接受的。
四叉树空间划分加速
四叉树核心思想
四叉树(Quadtree)递归地将2D空间划分为四个象限。每个节点最多包含 MAX_OBJECTS 个对象;当对象数量超过阈值时,节点分裂为四个子节点:
public class QuadTree {
private static final int MAX_OBJECTS = 4; // 节点最大对象数
private static final int MAX_LEVELS = 5; // 最大递归深度
private int level;
private AABB bounds; // 当前节点覆盖的矩形区域
private List<AABB> objects; // 落入该区域的对象
private QuadTree[] nodes; // 四个子节点:0=NW, 1=NE, 2=SW, 3=SE
public QuadTree(int level, AABB bounds) {
this.level = level;
this.bounds = bounds;
this.objects = new ArrayList<>();
this.nodes = new QuadTree[4];
}
/**
* 将当前节点分裂为4个子节点
*/
private void split() {
float subWidth = (bounds.maxX - bounds.minX) / 2;
float subHeight = (bounds.maxY - bounds.minY) / 2;
float x = bounds.minX;
float y = bounds.minY;
// 右上 (NE)
nodes[0] = new QuadTree(level + 1, new AABB(-1, x + subWidth, y, x + 2*subWidth, y + subHeight));
// 左上 (NW)
nodes[1] = new QuadTree(level + 1, new AABB(-1, x, y, x + subWidth, y + subHeight));
// 右下 (SE)
nodes[2] = new QuadTree(level + 1, new AABB(-1, x + subWidth, y + subHeight, x + 2*subWidth, y + 2*subHeight));
// 左下 (SW)
nodes[3] = new QuadTree(level + 1, new AABB(-1, x, y + subHeight, x + subWidth, y + 2*subHeight));
}
/**
* 判断对象完全落入哪个象限
* 返回值: 0=NE, 1=NW, 2=SE, 3=SW, -1=跨越多个象限
*/
private int getIndex(AABB obj) {
float midX = (bounds.minX + bounds.maxX) / 2;
float midY = (bounds.minY + bounds.maxY) / 2;
boolean top = obj.minY >= bounds.minY && obj.maxY <= midY;
boolean bottom = obj.minY >= midY && obj.maxY <= bounds.maxY;
boolean left = obj.minX >= bounds.minX && obj.maxX <= midX;
boolean right = obj.minX >= midX && obj.maxX <= bounds.maxX;
if (top) {
if (right) return 0; // NE
if (left) return 1; // NW
} else if (bottom) {
if (right) return 2; // SE
if (left) return 3; // SW
}
return -1; // 跨越多个象限,留在当前节点
}
}
插入与检索
/**
* 将对象插入四叉树
*/
public void insert(AABB obj) {
// 若有子节点,尝试放入子节点
if (nodes[0] != null) {
int index = getIndex(obj);
if (index != -1) {
nodes[index].insert(obj);
return;
}
}
objects.add(obj);
// 超过容量且未达最大深度,分裂节点
if (objects.size() > MAX_OBJECTS && level < MAX_LEVELS) {
if (nodes[0] == null) split();
// 将当前对象重新分配到子节点
Iterator<AABB> it = objects.iterator();
while (it.hasNext()) {
AABB o = it.next();
int index = getIndex(o);
if (index != -1) {
nodes[index].insert(o);
it.remove();
}
}
}
}
/**
* 检索可能与给定对象碰撞的所有对象
*/
public List<AABB> retrieve(List<AABB> result, AABB obj) {
int index = getIndex(obj);
if (index != -1 && nodes[0] != null) {
nodes[index].retrieve(result, obj);
}
result.addAll(objects);
return result;
}
/**
* 清空四叉树(每帧重建)
*/
public void clear() {
objects.clear();
for (int i = 0; i < 4; i++) {
if (nodes[i] != null) {
nodes[i].clear();
nodes[i] = null;
}
}
}
基于四叉树的碰撞检测系统
public class QuadTreeCollisionSystem {
private QuadTree tree;
private Set<String> collisionPairs;
public QuadTreeCollisionSystem(AABB worldBounds) {
this.tree = new QuadTree(0, worldBounds);
this.collisionPairs = new HashSet<>();
}
/**
* 每帧执行碰撞检测
*/
public Set<String> detectCollisions(List<AABB> objects) {
collisionPairs.clear();
tree.clear();
// 构建四叉树
for (AABB obj : objects) {
tree.insert(obj);
}
// 对每个对象,检索候选碰撞对象并精确检测
for (AABB obj : objects) {
List<AABB> candidates = new ArrayList<>();
tree.retrieve(candidates, obj);
for (AABB other : candidates) {
if (obj.id != other.id && obj.intersects(other)) {
// 使用有序对去重
String pair = obj.id < other.id ?
obj.id + "-" + other.id : other.id + "-" + obj.id;
collisionPairs.add(pair);
}
}
}
return collisionPairs;
}
}
性能对比测试
public static void main(String[] args) {
Random rand = new Random(42);
int[] sizes = {100, 500, 1000, 5000};
for (int n : sizes) {
List<AABB> objects = new ArrayList<>();
for (int i = 0; i < n; i++) {
float x = rand.nextFloat() * 800;
float y = rand.nextFloat() * 600;
float w = 10 + rand.nextFloat() * 40;
float h = 10 + rand.nextFloat() * 40;
objects.add(new AABB(i, x, y, x + w, y + h));
}
// 暴力检测
long start = System.nanoTime();
List<String> brute = BruteForceCollision.bruteForceDetect(objects);
long bruteTime = System.nanoTime() - start;
// 四叉树检测
AABB world = new AABB(-1, 0, 0, 800, 600);
QuadTreeCollisionSystem qt = new QuadTreeCollisionSystem(world);
start = System.nanoTime();
Set<String> quad = qt.detectCollisions(objects);
long quadTime = System.nanoTime() - start;
System.out.printf("n=%d: 暴力=%d对 (%.2fms), 四叉树=%d对 (%.2fms), 加速比=%.1fx%n",
n, brute.size(), bruteTime / 1e6, quad.size(), quadTime / 1e6,
(double)bruteTime / quadTime);
}
}
Sweep and Prune优化(一维投影)
对于物体分布密集的场景,Sweep and Prune(SAP)是另一种高效方法:
public class SweepAndPrune {
/**
* SAP算法:按X轴投影排序,仅检查投影重叠的相邻对象
*/
static List<String> sapDetect(List<AABB> objects) {
// 构建端点列表(每个对象产生minX和maxX两个端点)
List<Endpoint> endpoints = new ArrayList<>();
for (AABB obj : objects) {
endpoints.add(new Endpoint(obj.minX, obj, true)); // 起始端点
endpoints.add(new Endpoint(obj.maxX, obj, false)); // 终止端点
}
// 按X坐标排序
endpoints.sort(Comparator.comparingDouble(e -> e.value));
List<String> collisions = new ArrayList<>();
Set<AABB> active = new HashSet<>(); // 当前活跃的区间
for (Endpoint ep : endpoints) {
if (ep.isStart) {
// 新对象开始:与所有活跃对象进行Y轴相交测试
for (AABB other : active) {
if (ep.obj.intersects(other)) {
String pair = ep.obj.id < other.id ?
ep.obj.id + "-" + other.id : other.id + "-" + ep.obj.id;
collisions.add(pair);
}
}
active.add(ep.obj);
} else {
active.remove(ep.obj);
}
}
return collisions;
}
static class Endpoint {
float value;
AABB obj;
boolean isStart;
Endpoint(float v, AABB o, boolean s) { value=v; obj=o; isStart=s; }
}
}
复杂度分析
| 方法 | 最优时间 | 平均时间 | 最坏时间 | 适用场景 |
|---|---|---|---|---|
| 暴力检测 | O(n²) | O(n²) | O(n²) | n < 100 |
| 四叉树 | O(n) | O(n log n) | O(n²) | 空间分布均匀 |
| SAP | O(n) | O(n + k) | O(n²) | 物体密集重叠 |
其中 k 为实际碰撞对数。
小结
本文实现了完整的2D AABB碰撞检测系统:
– AABB相交测试的分离轴定理简化实现
– 四叉树空间划分的插入、检索与分裂机制
– 基于四叉树的批量碰撞检测流程
– Sweep and Prune作为补充优化方案
四叉树的核心价值在于:通过空间局部性原理,将全局O(n²)问题转化为局部的O(1)或O(log n)问题。这种”分而治之”的思想,正是算法设计中最具普适性的策略之一。