每日算法 — 使用java实现AABB碰撞检测:轴对齐包围盒与空间划分优化

引言

碰撞检测是游戏物理引擎、机器人路径规划、计算机图形学的核心问题。轴对齐包围盒(AABB, Axis-Aligned Bounding Box)是最基础且高效的碰撞检测方法之一,因其结构简单、计算 O(1) 而被广泛应用于商业游戏引擎中。

本文将用 Java 从零实现 AABB 碰撞检测系统,从基础相交测试、扫描剪裁、四叉树空间划分到完整游戏场景模拟,呈现这一基础算法的工程实践。

AABB 基础

定义

AABB 是一个与坐标轴平行的矩形(或长方体)包围盒。在 2D 中表示为:

class AABB {
    float minX, minY;  // 左下角
    float maxX, maxY;  // 右上角
}

相交判定

两个 AABB 相交当且仅当它们在所有轴上都有重叠:

AABB A 与 AABB B 相交 ⟺
  A.minX ≤ B.maxX 且 A.maxX ≥ B.minX 且
  A.minY ≤ B.maxY 且 A.maxY ≥ B.minY
package com.algorithm.aabb;

import java.util.Objects;

/**
 * 2D 轴对齐包围盒
 * 不可变结构,便于缓存与共享
 */
public final class AABB {
    public final float minX, minY;
    public final float maxX, maxY;

    public AABB(float minX, float minY, float maxX, float maxY) {
        // 保证 minX ≤ maxX 等
        if (minX > maxX || minY > maxY) {
            throw new IllegalArgumentException("Invalid AABB bounds");
        }
        this.minX = minX;
        this.minY = minY;
        this.maxX = maxX;
        this.maxY = maxY;
    }

    public static AABB fromCenter(float cx, float cy, float halfWidth, float halfHeight) {
        return new AABB(cx - halfWidth, cy - halfHeight, cx + halfWidth, cy + halfHeight);
    }

    public float width() { return maxX - minX; }
    public float height() { return maxY - minY; }
    public float area() { return width() * height(); }

    /**
     * 判断两个 AABB 是否相交(包含边界)
     * 复杂度:O(1)
     */
    public boolean intersects(AABB other) {
        return minX <= other.maxX && maxX >= other.minX &&
               minY <= other.maxY && maxY >= other.minY;
    }

    /**
     * 判断 AABB 是否包含点 (x, y)
     */
    public boolean contains(float x, float y) {
        return x >= minX && x <= maxX && y >= minY && y <= maxY;
    }

    /**
     * 计算两个 AABB 的相交区域
     * @return 相交 AABB,不相交返回 null
     */
    public AABB intersection(AABB other) {
        float ixMin = Math.max(minX, other.minX);
        float iyMin = Math.max(minY, other.minY);
        float ixMax = Math.min(maxX, other.maxX);
        float iyMax = Math.min(maxY, other.maxY);
        if (ixMin > ixMax || iyMin > iyMax) return null;
        return new AABB(ixMin, iyMin, ixMax, iyMax);
    }

    /**
     * 平移 AABB,返回新对象
     */
    public AABB translate(float dx, float dy) {
        return new AABB(minX + dx, minY + dy, maxX + dx, maxY + dy);
    }

    @Override
    public boolean equals(Object o) {
        if (this == o) return true;
        if (!(o instanceof AABB)) return false;
        AABB other = (AABB) o;
        return Float.compare(minX, other.minX) == 0 &&
               Float.compare(minY, other.minY) == 0 &&
               Float.compare(maxX, other.maxX) == 0 &&
               Float.compare(maxY, other.maxY) == 0;
    }

    @Override
    public int hashCode() {
        return Objects.hash(minX, minY, maxX, maxY);
    }
}

运动 AABB 与扫掠测试

对于高速运动的物体,需要做扫掠测试(Swept AABB)以避免”穿透”现象。

package com.algorithm.aabb;

/**
 * 运动 AABB:用于移动物体的连续碰撞检测
 * 通过在每个轴上求解进入/离开时间,避免穿透
 */
public class MovingAABB {
    public final AABB box;       // 当前位置
    public final float dx, dy;   // 位移向量
    public final float invDx, invDy;  // 位移倒数(用于快速求交)

    public MovingAABB(AABB box, float dx, float dy) {
        this.box = box;
        this.dx = dx;
        this.dy = dy;
        // 避免除零
        this.invDx = (dx == 0) ? Float.POSITIVE_INFINITY : 1.0f / dx;
        this.invDy = (dy == 0) ? Float.POSITIVE_INFINITY : 1.0f / dy;
    }

    /**
     * 静态 AABB 的扩展 AABB(包含扫掠路径)
     */
    public AABB sweptBoundingBox() {
        return new AABB(
            dx > 0 ? box.minX : box.minX + dx,
            dy > 0 ? box.minY : box.minY + dy,
            dx > 0 ? box.maxX + dx : box.maxX,
            dy > 0 ? box.maxY + dy : box.maxY
        );
    }
}

SAP 扫描剪裁算法

当物体在某一轴上排序后,可以利用扫描剪裁(SAP, Sweep and Prune)大幅减少碰撞检测的次数。

package com.algorithm.aabb;

import java.util.*;

/**
 * SAP(Sweep and Prune)扫描剪裁算法
 * 适用于大量物体在某一方向相对静止或缓慢移动的场景
 * 时间复杂度:O(n log n + k),其中 k 为实际碰撞对数
 */
public class SweepAndPrune {
    private final List<AABB> objects;
    private float[] endPoints;  // 端点数组:偶数索引为 min,奇数为 max
    private int[] indices;      // 端点对应的物体索引
    private boolean[] active;   // 物体是否激活
    private int size;

    public SweepAndPrune(int capacity) {
        this.objects = new ArrayList<>(capacity);
        this.endPoints = new float[capacity * 2];
        this.indices = new int[capacity * 2];
        this.active = new boolean[capacity];
        this.size = 0;
    }

    public int addObject(AABB box) {
        int id = size++;
        objects.add(box);
        active[id] = true;
        endPoints[id * 2] = box.minX;
        endPoints[id * 2 + 1] = box.maxX;
        indices[id * 2] = id;
        indices[id * 2 + 1] = id;
        return id;
    }

    /**
     * 对端点排序(按 X 坐标)
     * 使用插入排序:相邻帧的端点变化小
     */
    public void sortEndpoints() {
        // 简化实现:使用 ArrayList 排序
        List<float[]> list = new ArrayList<>();
        for (int i = 0; i < size; i++) {
            list.add(new float[]{endPoints[i * 2], indices[i * 2]});
            list.add(new float[]{endPoints[i * 2 + 1], indices[i * 2 + 1]});
        }
        list.sort(Comparator.comparingDouble(a -> a[0]));
        for (int i = 0; i < list.size(); i++) {
            if (i % 2 == 0) {
                endPoints[i] = list.get(i)[0];
                indices[i] = (int) list.get(i)[1];
            } else {
                endPoints[i] = list.get(i)[0];
                indices[i] = (int) list.get(i)[1];
            }
        }
    }

    /**
     * 检测所有碰撞对
     */
    public List<int[]> findCollisions() {
        sortEndpoints();
        Set<Long> pairs = new HashSet<>();
        List<int[]> collisions = new ArrayList<>();
        Set<Integer> open = new HashSet<>();

        for (int i = 0; i < size * 2; i++) {
            int objId = indices[i];
            if (i % 2 == 0) {
                // 物体开始:检查所有已开启的物体
                for (int other : open) {
                    if (objId == other) continue;
                    AABB a = objects.get(objId);
                    AABB b = objects.get(other);
                    if (a.intersects(b)) {
                        long key = ((long) Math.min(objId, other) << 32) | 
                                   Math.max(objId, other);
                        if (pairs.add(key)) {
                            collisions.add(new int[]{Math.min(objId, other), 
                                                     Math.max(objId, other)});
                        }
                    }
                }
                open.add(objId);
            } else {
                // 物体结束:从开启集合中移除
                open.remove(objId);
            }
        }
        return collisions;
    }
}

四叉树空间划分

四叉树(Quadtree)通过递归地将二维空间划分为四个象限,将碰撞检测从 O(n²) 降到 O(n log n)。

package com.algorithm.aabb;

import java.util.ArrayList;
import java.util.List;

/**
 * 四叉树:2D 空间划分数据结构
 * 适用于稀疏分布的对象集合
 * 节点容量满时分裂为 4 个子节点
 */
public class Quadtree {
    private static final int MAX_OBJECTS = 4;  // 节点最大容量
    private static final int MAX_LEVELS = 5;   // 最大深度

    private final int level;
    private final AABB bounds;
    private final List<AABB> objects;  // 当前节点的对象
    private final List<Quadtree> children;  // 四个子节点

    public Quadtree(int level, AABB bounds) {
        this.level = level;
        this.bounds = bounds;
        this.objects = new ArrayList<>();
        this.children = new ArrayList<>();
    }

    /**
     * 清除四叉树
     */
    public void clear() {
        objects.clear();
        for (Quadtree child : children) {
            child.clear();
        }
        children.clear();
    }

    /**
     * 插入对象
     */
    public void insert(int objectId, AABB box) {
        // 如果有子节点,递归插入
        if (!children.isEmpty()) {
            int index = getIndex(box);
            if (index != -1) {
                children.get(index).insert(objectId, box);
                return;
            }
        }

        // 否则加入当前节点
        objects.add(box);

        // 容量超限且未达最大深度:分裂
        if (objects.size() > MAX_OBJECTS && level < MAX_LEVELS) {
            if (children.isEmpty()) split();

            // 重新分配已有对象
            List<AABB> remaining = new ArrayList<>();
            for (AABB obj : objects) {
                int index = getIndex(obj);
                if (index != -1) {
                    children.get(index).insert(objectId, obj);
                } else {
                    remaining.add(obj);
                }
            }
            objects.clear();
            objects.addAll(remaining);
        }
    }

    /**
     * 分裂为四象限
     */
    private void split() {
        float subWidth = bounds.width() / 2;
        float subHeight = bounds.height() / 2;
        float x = bounds.minX;
        float y = bounds.minY;

        children.add(new Quadtree(level + 1, 
            new AABB(x + subWidth, y, x + 2 * subWidth, y + subHeight)));          // 右上
        children.add(new Quadtree(level + 1,
            new AABB(x, y, x + subWidth, y + subHeight)));                          // 左上
        children.add(new Quadtree(level + 1,
            new AABB(x, y + subHeight, x + subWidth, y + 2 * subHeight)));         // 左下
        children.add(new Quadtree(level + 1,
            new AABB(x + subWidth, y + subHeight, x + 2 * subWidth, y + 2 * subHeight))); // 右下
    }

    /**
     * 确定 AABB 属于哪个象限
     * @return 0-3 象限索引,完全跨界返回 -1
     */
    private int getIndex(AABB box) {
        int index = -1;
        double verticalMidpoint = bounds.minX + bounds.width() / 2;
        double horizontalMidpoint = bounds.minY + bounds.height() / 2;

        // 上半部分(y < horizontalMidpoint)
        boolean topQuadrant = box.maxY < horizontalMidpoint;
        // 下半部分
        boolean bottomQuadrant = box.minY > horizontalMidpoint;
        // 左半部分
        boolean leftQuadrant = box.maxX < verticalMidpoint;
        // 右半部分
        boolean rightQuadrant = box.minX > verticalMidpoint;

        if (leftQuadrant) {
            if (topQuadrant) index = 1;
            else if (bottomQuadrant) index = 2;
        } else if (rightQuadrant) {
            if (topQuadrant) index = 0;
            else if (bottomQuadrant) index = 3;
        }
        return index;
    }

    /**
     * 检索与目标 AABB 可能有碰撞的所有对象
     */
    public List<AABB> retrieve(AABB box) {
        List<AABB> found = new ArrayList<>();
        int index = getIndex(box);
        if (index != -1 && !children.isEmpty()) {
            found.addAll(children.get(index).retrieve(box));
        } else {
            for (Quadtree child : children) {
                found.addAll(child.retrieve(box));
            }
        }
        found.addAll(objects);
        return found;
    }
}

动态 AABB 演示

package com.algorithm.aabb;

import java.util.ArrayList;
import java.util.List;
import java.util.Random;

/**
 * 动态 AABB 演示:模拟大量移动物体的碰撞检测
 * 对比朴素 O(n²) 与四叉树/SAP 的性能差异
 */
public class CollisionDemo {

    static class MovingObject {
        AABB box;
        float vx, vy;
        int id;

        MovingObject(int id, AABB box, float vx, float vy) {
            this.id = id;
            this.box = box;
            this.vx = vx;
            this.vy = vy;
        }

        void update() {
            // 简化:随机移动并反弹
            float nx = box.minX + vx;
            if (nx < 0 || nx + box.width() > 1000) vx = -vx;
            box = box.translate(vx, vy);
            float ny = box.minY + vy;
            if (ny < 0 || ny + box.height() > 1000) vy = -vy;
        }
    }

    /**
     * 朴素 O(n²) 碰撞检测
     */
    public static int bruteForceCollision(List<MovingObject> objects) {
        int count = 0;
        for (int i = 0; i < objects.size(); i++) {
            for (int j = i + 1; j < objects.size(); j++) {
                if (objects.get(i).box.intersects(objects.get(j).box)) {
                    count++;
                }
            }
        }
        return count;
    }

    /**
     * 四叉树碰撞检测
     */
    public static int quadtreeCollision(List<MovingObject> objects) {
        Quadtree tree = new Quadtree(0, new AABB(0, 0, 1000, 1000));
        for (MovingObject obj : objects) {
            tree.insert(obj.id, obj.box);
        }

        int count = 0;
        for (MovingObject obj : objects) {
            List<AABB> candidates = tree.retrieve(obj.box);
            for (AABB other : candidates) {
                if (other != obj.box && obj.box.intersects(other)) {
                    count++;
                }
            }
        }
        return count / 2;  // 每对被检测两次
    }

    public static void main(String[] args) {
        int n = 1000;
        Random rand = new Random(42);
        List<MovingObject> objects = new ArrayList<>();

        for (int i = 0; i < n; i++) {
            float x = rand.nextFloat() * 900;
            float y = rand.nextFloat() * 900;
            float size = 20 + rand.nextFloat() * 30;
            AABB box = new AABB(x, y, x + size, y + size);
            float vx = (rand.nextFloat() - 0.5f) * 4;
            float vy = (rand.nextFloat() - 0.5f) * 4;
            objects.add(new MovingObject(i, box, vx, vy));
        }

        // 性能对比
        long start = System.nanoTime();
        int c1 = bruteForceCollision(objects);
        long t1 = System.nanoTime() - start;

        start = System.nanoTime();
        int c2 = quadtreeCollision(objects);
        long t2 = System.nanoTime() - start;

        System.out.printf("物体数: %d%n", n);
        System.out.printf("朴素 O(n²): %d 碰撞对, 耗时 %d μs%n", c1, t1 / 1000);
        System.out.printf("四叉树:    %d 碰撞对, 耗时 %d μs%n", c2, t2 / 1000);
        System.out.printf("加速比: %.1fx%n", (double) t1 / t2);
    }
}

复杂度分析

算法 时间复杂度 空间复杂度 适用场景
朴素两两检测 O(n²) O(1) 物体数 < 50
空间哈希 O(n) 平均 O(n) 物体均匀分布
SAP 扫描剪裁 O(n log n + k) O(n) 沿轴相对静止
四叉树 O(n log n) O(n) 稀疏不均匀分布
八叉树 (3D) O(n log n) O(n) 3D 空间
BVH O(n log n) 构建 O(n) 静态场景

应用场景

  • 2D 游戏碰撞检测:Mario、Platformer 游戏中玩家与地砖
  • 弹道预测:子弹与目标扫掠测试
  • 物理引擎:Box2D 内部大量使用 AABB 粗筛
  • GUI 框架:鼠标点击命中测试
  • CAD 软件:对象选取
  • 机器人路径规划:障碍物检测
  • VR/AR:虚拟对象与现实场景交互

进阶话题

旋转 AABB:OBB

AABB 仅适合轴对齐的物体。旋转物体的更紧致包围盒是 OBB(Oriented Bounding Box),使用 SAT(Separating Axis Theorem)检测,复杂度 O(轴数 × 面数)。

连续碰撞检测

对于高速物体(子弹、赛博物理),离散 AABB 检测会产生穿透现象。需要 CCD(Continuous Collision Detection),常见算法包括:

  • 扫掠测试:求解进入/离开时间
  • 二次积分:保留历史位置做时间切片
  • GJK + EPA:凸体之间的精确距离计算

SIMD 加速

现代 CPU 的 SIMD 指令(SSE/AVX)可同时检测 4-8 对 AABB 碰撞,性能提升显著。

总结

AABB 碰撞检测是游戏与图形学的”基础设施”。其优势在于实现简单、计算高效、内存紧凑;不足是不适合旋转对象。工程实践中,AABB 常作为粗筛阶段,再与 OBB、GJK 等精细检测组合使用。掌握 AABB 与空间划分(SAP、四叉树)能让我们设计出支持大量物体的物理引擎。

参考资料

  1. Ericson, C. (2004). Real-Time Collision Detection. Morgan Kaufmann.
  2. Akenine-Möller, T., Haines, E., & Hoffman, N. (2018). Real-Time Rendering 4th Edition. CRC Press.
  3. Box2D Physics Engine – https://box2d.org/
  4. Christer Ericson’s GameDev Articles: https://realtimecollisiondetection.net/