每日算法 — 使用java实现打砖块:AABB碰撞检测与弹球反射物理

一、游戏介绍与算法目标

打砖块(Breakout)是1976年诞生的经典街机游戏,玩家控制底部挡板反弹弹球,击碎屏幕上方的砖块。本文的目标是构建一个带物理引擎的打砖块游戏核心,重点讲解以下算法:

  • AABB(轴对齐包围盒)碰撞检测:判断弹球与砖块、挡板是否相交
  • 反射向量计算:弹球碰撞后的速度方向更新
  • 边界约束处理:弹球在画面四周的反弹逻辑

二、核心算法原理

2.1 AABB碰撞检测

AABB(Axis-Aligned Bounding Box)是一种与坐标轴对齐的矩形包围盒。由于打砖块中的砖块和挡板都是水平或垂直放置的矩形,AABB是最自然的碰撞检测方案。

两个AABB相交的充要条件是在X轴和Y轴上的投影区间均重叠:

相交条件:
  box1.minX < box2.maxX && box1.maxX > box2.minX
  && box1.minY < box2.maxY && box1.maxY > box2.minY

对于圆形弹球与矩形砖块的碰撞,采用”最近点法”:计算矩形上距离圆心最近的点,若该点与圆心距离小于半径,则发生碰撞。

2.2 反射向量计算

弹球碰撞后的速度更新遵循反射定律。设碰撞面的法向量为 n,入射速度为 v,则反射速度 v’ 为:

v' = v - 2 * (v · n) * n

其中 v · n 表示向量点积。对于水平碰撞面(如砖块顶部/底部),法向量为 (0, ±1),仅反转Y分量;对于垂直碰撞面,法向量为 (±1, 0),仅反转X分量。

三、完整Java实现

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

/**
 * 打砖块游戏核心引擎
 * 包含AABB碰撞检测、反射物理与游戏状态管理
 */
public class BreakoutEngine {

    // 游戏画布尺寸
    public static final int WIDTH = 800;
    public static final int HEIGHT = 600;

    // 弹球对象
    public static class Ball {
        double x, y;      // 圆心坐标
        double vx, vy;    // 速度向量(像素/帧)
        double radius;    // 半径

        public Ball(double x, double y, double vx, double vy, double radius) {
            this.x = x; this.y = y;
            this.vx = vx; this.vy = vy;
            this.radius = radius;
        }

        /** 更新位置 */
        public void update() {
            x += vx;
            y += vy;
        }

        /** 获取AABB包围盒(用于快速排除) */
        public AABB getBounds() {
            return new AABB(x - radius, y - radius, x + radius, y + radius);
        }
    }

    // 矩形对象(砖块或挡板)
    public static class Rectangle {
        double x, y;      // 左上角坐标
        double width, height;
        boolean active;   // 是否还存在

        public Rectangle(double x, double y, double width, double height) {
            this.x = x; this.y = y;
            this.width = width; this.height = height;
            this.active = true;
        }

        /** 获取AABB包围盒 */
        public AABB getBounds() {
            return new AABB(x, y, x + width, y + height);
        }
    }

    // 轴对齐包围盒
    public static class AABB {
        double minX, minY, maxX, maxY;

        public AABB(double minX, double minY, double maxX, double maxY) {
            this.minX = minX; this.minY = minY;
            this.maxX = maxX; this.maxY = maxY;
        }

        /** AABB相交测试 */
        public boolean intersects(AABB other) {
            return this.minX < other.maxX && this.maxX > other.minX
                && this.minY < other.maxY && this.maxY > other.minY;
        }
    }

    // 碰撞信息
    public static class CollisionInfo {
        boolean hit;       // 是否碰撞
        double normalX;    // 碰撞面法向量X
        double normalY;    // 碰撞面法向量Y

        public CollisionInfo(boolean hit, double normalX, double normalY) {
            this.hit = hit;
            this.normalX = normalX;
            this.normalY = normalY;
        }
    }

    private Ball ball;
    private Rectangle paddle;               // 挡板
    private List<Rectangle> bricks;         // 砖块列表
    private int score;
    private int lives;

    public BreakoutEngine() {
        this.ball = new Ball(WIDTH / 2.0, HEIGHT / 2.0, 4.0, -4.0, 8.0);
        this.paddle = new Rectangle(WIDTH / 2.0 - 60, HEIGHT - 30, 120, 15);
        this.bricks = new ArrayList<>();
        this.score = 0;
        this.lives = 3;
        initBricks();
    }

    /** 初始化砖块阵列 */
    private void initBricks() {
        int rows = 5;
        int cols = 10;
        double brickW = 70;
        double brickH = 25;
        double gap = 5;
        double startX = (WIDTH - cols * (brickW + gap) + gap) / 2.0;
        double startY = 50;

        for (int r = 0; r < rows; r++) {
            for (int c = 0; c < cols; c++) {
                bricks.add(new Rectangle(
                    startX + c * (brickW + gap),
                    startY + r * (brickH + gap),
                    brickW, brickH
                ));
            }
        }
    }

    /**
     * 圆形与矩形的精确碰撞检测(最近点法)
     * 返回碰撞面法向量,用于反射计算
     */
    public CollisionInfo circleRectCollision(Ball ball, Rectangle rect) {
        // 步骤1:AABB快速排除
        if (!ball.getBounds().intersects(rect.getBounds())) {
            return new CollisionInfo(false, 0, 0);
        }

        // 步骤2:计算矩形上距离圆心最近的点
        double closestX = clamp(ball.x, rect.x, rect.x + rect.width);
        double closestY = clamp(ball.y, rect.y, rect.y + rect.height);

        // 步骤3:计算圆心到最近点的距离
        double dx = ball.x - closestX;
        double dy = ball.y - closestY;
        double distSq = dx * dx + dy * dy;

        if (distSq > ball.radius * ball.radius) {
            return new CollisionInfo(false, 0, 0);
        }

        // 步骤4:确定碰撞面法向量
        // 如果最近点在矩形内部(圆心在矩形内),需要特殊处理
        double normalX, normalY;
        if (distSq < 1e-9) {
            // 圆心在矩形内部,根据相对位置判断
            double centerX = rect.x + rect.width / 2.0;
            double centerY = rect.y + rect.height / 2.0;
            double relX = (ball.x - centerX) / (rect.width / 2.0);
            double relY = (ball.y - centerY) / (rect.height / 2.0);
            if (Math.abs(relX) > Math.abs(relY)) {
                normalX = Math.signum(relX);
                normalY = 0;
            } else {
                normalX = 0;
                normalY = Math.signum(relY);
            }
        } else {
            // 正常碰撞:法向量为圆心指向最近点的方向
            double dist = Math.sqrt(distSq);
            normalX = dx / dist;
            normalY = dy / dist;
        }

        return new CollisionInfo(true, normalX, normalY);
    }

    /** 数值限制在[min, max]区间 */
    private double clamp(double value, double min, double max) {
        return Math.max(min, Math.min(max, value));
    }

    /**
     * 反射速度计算
     * v' = v - 2 * (v·n) * n
     */
    public void reflect(Ball ball, double normalX, double normalY) {
        double dot = ball.vx * normalX + ball.vy * normalY;
        ball.vx -= 2 * dot * normalX;
        ball.vy -= 2 * dot * normalY;
    }

    /** 游戏主循环更新 */
    public void update() {
        // 更新弹球位置
        ball.update();

        // 边界碰撞检测与反弹
        if (ball.x - ball.radius < 0) {
            ball.x = ball.radius;
            ball.vx = Math.abs(ball.vx);
        } else if (ball.x + ball.radius > WIDTH) {
            ball.x = WIDTH - ball.radius;
            ball.vx = -Math.abs(ball.vx);
        }

        if (ball.y - ball.radius < 0) {
            ball.y = ball.radius;
            ball.vy = Math.abs(ball.vy);
        } else if (ball.y - ball.radius > HEIGHT) {
            // 弹球掉落底部,生命减少并重置
            lives--;
            resetBall();
            return;
        }

        // 挡板碰撞检测
        CollisionInfo paddleHit = circleRectCollision(ball, paddle);
        if (paddleHit.hit) {
            reflect(ball, paddleHit.normalX, paddleHit.normalY);
            // 根据击中挡板位置微调X速度,增加游戏性
            double hitPos = (ball.x - (paddle.x + paddle.width / 2.0)) / (paddle.width / 2.0);
            ball.vx += hitPos * 2.0;
            // 确保弹球向上运动
            if (ball.vy > 0) ball.vy = -Math.abs(ball.vy);
        }

        // 砖块碰撞检测
        for (Rectangle brick : bricks) {
            if (!brick.active) continue;
            CollisionInfo hit = circleRectCollision(ball, brick);
            if (hit.hit) {
                reflect(ball, hit.normalX, hit.normalY);
                brick.active = false;
                score += 10;
                break; // 每帧只处理一次碰撞
            }
        }
    }

    /** 重置弹球 */
    private void resetBall() {
        ball.x = WIDTH / 2.0;
        ball.y = HEIGHT / 2.0;
        ball.vx = 4.0 * (Math.random() > 0.5 ? 1 : -1);
        ball.vy = -4.0;
    }

    /** 移动挡板 */
    public void movePaddle(double newX) {
        paddle.x = clamp(newX - paddle.width / 2.0, 0, WIDTH - paddle.width);
    }

    /** 检查是否获胜 */
    public boolean isWin() {
        return bricks.stream().noneMatch(b -> b.active);
    }

    /** 检查是否失败 */
    public boolean isGameOver() {
        return lives <= 0;
    }

    public int getScore() { return score; }
    public int getLives() { return lives; }
    public Ball getBall() { return ball; }
    public Rectangle getPaddle() { return paddle; }
    public List<Rectangle> getBricks() { return bricks; }

    /** 测试主程序 */
    public static void main(String[] args) {
        BreakoutEngine engine = new BreakoutEngine();

        // 模拟1000帧游戏过程
        for (int frame = 0; frame < 1000; frame++) {
            // AI挡板:跟随弹球X坐标
            engine.movePaddle(engine.getBall().x);
            engine.update();

            if (engine.isWin()) {
                System.out.println("恭喜!所有砖块已被击碎!");
                System.out.println("最终得分: " + engine.getScore());
                System.out.println("剩余生命: " + engine.getLives());
                break;
            }
            if (engine.isGameOver()) {
                System.out.println("游戏结束!");
                System.out.println("最终得分: " + engine.getScore());
                break;
            }
        }

        System.out.println("模拟结束 - 得分: " + engine.getScore()
            + ", 剩余砖块: " + engine.getBricks().stream().filter(b -> b.active).count()
            + ", 剩余生命: " + engine.getLives());
    }
}

四、算法复杂度分析

操作 时间复杂度 空间复杂度 说明
AABB快速排除 O(1) O(1) 4次比较运算
最近点精确检测 O(1) O(1) 固定计算步骤
单帧碰撞检测 O(n) O(1) n为砖块数量
反射向量计算 O(1) O(1) 向量点积与减法

优化建议:当砖块数量较大时(如超过1000个),可采用空间哈希或四叉树将碰撞检测优化至 O(log n) 或均摊 O(1)。

五、关键设计要点

  1. 碰撞法向量方向:始终从矩形指向圆心,确保反射后弹球远离碰撞体,避免”粘连”现象。
  2. 挡板X速度微调:击中挡板边缘时增加水平速度分量,让游戏更具策略性。
  3. 单帧单碰撞:每帧只处理第一个碰撞对象,防止弹球在密集砖块间反复反弹导致速度异常。
  4. 数值稳定性:碰撞后若弹球仍与砖块相交,可在反射后额外沿法向量偏移1像素,消除浮点误差。

六、扩展方向

  • 多球模式:管理多个弹球对象,碰撞检测升级为批量查询
  • 道具系统:添加分裂球、激光、挡板加长等道具,引入状态机管理
  • 关卡编辑器:基于JSON的砖块布局配置,支持不同砖块生命值
  • 粒子特效:砖块击碎时生成粒子,使用对象池优化内存分配