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

引言

《打砖块》(Breakout/Arkanoid)是一款经典的街机游戏,玩家控制挡板反弹小球,击碎屏幕上方的砖块阵列。游戏的核心算法挑战在于实时碰撞检测与物理反射模拟。本文将用Java实现一个完整的打砖块游戏框架,重点讲解AABB(轴对齐包围盒)碰撞检测、弹性碰撞反射向量计算,以及砖块消除的级联判定逻辑。

核心数据结构:游戏实体表示

首先定义游戏中各类实体的基础数据结构,采用面向对象设计便于扩展。

/**
 * 二维向量类,用于表示位置、速度、方向
 */
public class Vec2 {
    public double x, y;

    public Vec2(double x, double y) {
        this.x = x;
        this.y = y;
    }

    public Vec2 add(Vec2 other) {
        return new Vec2(this.x + other.x, this.y + other.y);
    }

    public Vec2 subtract(Vec2 other) {
        return new Vec2(this.x - other.x, this.y - other.y);
    }

    public Vec2 multiply(double scalar) {
        return new Vec2(this.x * scalar, this.y * scalar);
    }

    public double dot(Vec2 other) {
        return this.x * other.x + this.y * other.y;
    }

    public double length() {
        return Math.sqrt(x * x + y * y);
    }

    public Vec2 normalize() {
        double len = length();
        return len > 0 ? new Vec2(x / len, y / len) : new Vec2(0, 0);
    }

    @Override
    public String toString() {
        return String.format("(%.2f, %.2f)", x, y);
    }
}

/**
 * AABB轴对齐包围盒
 */
public class AABB {
    public Vec2 min; // 左上角
    public Vec2 max; // 右下角

    public AABB(Vec2 min, Vec2 max) {
        this.min = min;
        this.max = max;
    }

    public double width() { return max.x - min.x; }
    public double height() { return max.y - min.y; }
    public Vec2 center() {
        return new Vec2((min.x + max.x) / 2, (min.y + max.y) / 2);
    }

    /**
     * AABB与AABB碰撞检测
     */
    public boolean intersects(AABB other) {
        return this.min.x < other.max.x && this.max.x > other.min.x
            && this.min.y < other.max.y && this.max.y > other.min.y;
    }

    /**
     * 计算AABB上距离给定点最近的点(用于球-矩形碰撞)
     */
    public Vec2 closestPoint(Vec2 point) {
        double x = Math.max(min.x, Math.min(point.x, max.x));
        double y = Math.max(min.y, Math.min(point.y, max.y));
        return new Vec2(x, y);
    }
}

核心算法一:球与矩形的精确碰撞检测

小球与挡板、砖块的碰撞不能简单使用AABB相交判断,因为球是圆形。我们使用最近点法计算圆心到矩形的最短距离,若距离小于半径则发生碰撞。

/**
 * 碰撞信息记录类
 */
public class CollisionInfo {
    public boolean collided;    // 是否发生碰撞
    public Vec2 normal;         // 碰撞法线方向
    public double penetration;  // 穿透深度
    public Vec2 contactPoint;   // 碰撞接触点

    public CollisionInfo() {
        this.collided = false;
    }
}

/**
 * 圆形碰撞体
 */
public class Circle {
    public Vec2 center;
    public double radius;

    public Circle(Vec2 center, double radius) {
        this.center = center;
        this.radius = radius;
    }

    /**
     * 圆与AABB的碰撞检测(最近点法)
     * 算法步骤:
     * 1. 在AABB上找到距离圆心最近的点
     * 2. 计算该点与圆心的距离
     * 3. 若距离 < 半径,则发生碰撞
     * 4. 碰撞法线 = (圆心 - 最近点).normalize()
     */
    public CollisionInfo intersectAABB(AABB box) {
        CollisionInfo info = new CollisionInfo();
        Vec2 closest = box.closestPoint(center);
        Vec2 diff = center.subtract(closest);
        double distSq = diff.x * diff.x + diff.y * diff.y;

        if (distSq < radius * radius) {
            info.collided = true;
            double dist = Math.sqrt(distSq);
            if (dist > 1e-6) {
                info.normal = diff.normalize();
                info.penetration = radius - dist;
            } else {
                // 圆心在矩形内部,取中心到矩形中心的方向
                info.normal = center.subtract(box.center()).normalize();
                info.penetration = radius;
            }
            info.contactPoint = closest;
        }
        return info;
    }
}

核心算法二:弹性反射与速度更新

碰撞发生后,需要根据碰撞法线计算反射方向。对于理想弹性碰撞,入射向量关于法线对称反射。

/**
 * 物理引擎:处理碰撞响应
 */
public class PhysicsEngine {

    /**
     * 计算反射速度向量
     * 公式: v' = v - 2 * (v·n) * n
     * 其中 v 是入射速度,n 是单位法线向量
     */
    public static Vec2 reflect(Vec2 velocity, Vec2 normal) {
        // v·n 是速度在法线方向的投影分量
        double dot = velocity.dot(normal);
        // 若物体正在远离碰撞面,不做处理
        if (dot >= 0) return velocity;

        // 反射公式
        Vec2 reflection = normal.multiply(2 * dot);
        return velocity.subtract(reflection);
    }

    /**
     * 应用碰撞响应,更新球的位置和速度
     * @param ballCenter 球的当前位置(会被修改)
     * @param velocity   球的速度(会被修改)
     * @param info       碰撞信息
     * @param restitution 弹性系数(1.0为完全弹性,0.0为完全非弹性)
     */
    public static void resolveCollision(Vec2 ballCenter, Vec2 velocity,
                                         CollisionInfo info, double restitution) {
        if (!info.collided) return;

        // 1. 位置修正:将球推出穿透深度,防止陷入
        Vec2 correction = info.normal.multiply(info.penetration + 0.01);
        ballCenter.x += correction.x;
        ballCenter.y += correction.y;

        // 2. 速度反射
        Vec2 reflected = reflect(velocity, info.normal);
        velocity.x = reflected.x * restitution;
        velocity.y = reflected.y * restitution;
    }

    /**
     * 挡板碰撞特殊处理:根据击中位置改变反射角度
     * 击中挡板中心 → 垂直向上反射
     * 击中挡板边缘 → 大角度斜向反射
     * 这增加了游戏的可控性和策略性
     */
    public static Vec2 paddleReflect(Vec2 velocity, AABB paddle, Vec2 ballCenter,
                                      double maxBounceAngle) {
        double paddleCenterX = paddle.center().x;
        double hitOffset = (ballCenter.x - paddleCenterX) / (paddle.width() / 2);
        // 限制偏移范围在 [-1, 1]
        hitOffset = Math.max(-1.0, Math.min(1.0, hitOffset));

        // 根据偏移计算反射角度
        double bounceAngle = hitOffset * maxBounceAngle;
        double speed = velocity.length();

        return new Vec2(
            speed * Math.sin(bounceAngle),
            -speed * Math.cos(bounceAngle)  // 负y表示向上
        );
    }
}

核心算法三:砖块管理与级联消除

砖块阵列采用二维数组管理,支持不同强度砖块和特殊效果。当球击中砖块时,触发消除判定,并检查是否满足级联消除条件(如特殊道具触发相邻消除)。

import java.util.*;

/**
 * 砖块类型
 */
enum BrickType {
    NORMAL(1, 10),    // 普通砖块,1点血量,10分
    STRONG(2, 20),    // 强化砖块,2点血量
    EXPLOSIVE(1, 30), // 爆炸砖块,击碎时消除周围3x3
    UNBREAKABLE(-1, 0); // 不可破坏

    final int hp;
    final int score;

    BrickType(int hp, int score) {
        this.hp = hp;
        this.score = score;
    }
}

/**
 * 单个砖块实例
 */
class Brick {
    BrickType type;
    int currentHp;
    AABB bounds;
    boolean destroyed = false;

    Brick(BrickType type, AABB bounds) {
        this.type = type;
        this.currentHp = type.hp;
        this.bounds = bounds;
    }

    /**
     * 受到伤害,返回是否被击碎
     */
    public boolean takeDamage(int damage) {
        if (type == BrickType.UNBREAKABLE) return false;
        currentHp -= damage;
        if (currentHp <= 0) {
            destroyed = true;
            return true;
        }
        return false;
    }
}

/**
 * 砖块管理器:负责砖块阵列的生成、碰撞检测和消除
 */
public class BrickManager {
    private final Brick[][] bricks;
    private final int rows;
    private final int cols;
    private final double brickWidth;
    private final double brickHeight;
    private final double padding;

    public BrickManager(int rows, int cols, double brickWidth,
                        double brickHeight, double padding) {
        this.rows = rows;
        this.cols = cols;
        this.brickWidth = brickWidth;
        this.brickHeight = brickHeight;
        this.padding = padding;
        this.bricks = new Brick[rows][cols];
        generateBricks();
    }

    /**
     * 生成砖块阵列,随机分配类型
     */
    private void generateBricks() {
        Random rand = new Random();
        BrickType[] types = BrickType.values();
        for (int r = 0; r < rows; r++) {
            for (int c = 0; c < cols; c++) {
                double x = c * (brickWidth + padding);
                double y = r * (brickHeight + padding) + 50; // 顶部留白
                AABB bounds = new AABB(
                    new Vec2(x, y),
                    new Vec2(x + brickWidth, y + brickHeight)
                );
                // 越往上排,强化砖块比例越高
                BrickType type = rand.nextDouble() < 0.1 + r * 0.05
                    ? BrickType.STRONG : BrickType.NORMAL;
                if (rand.nextDouble() < 0.05) type = BrickType.EXPLOSIVE;
                bricks[r][c] = new Brick(type, bounds);
            }
        }
    }

    /**
     * 检测球与所有砖块的碰撞,返回得分和需要消除的砖块列表
     */
    public CollisionResult checkBallCollisions(Circle ball, Vec2 velocity) {
        CollisionResult result = new CollisionResult();
        List<Brick> toExplode = new ArrayList<>();

        for (int r = 0; r < rows; r++) {
            for (int c = 0; c < cols; c++) {
                Brick brick = bricks[r][c];
                if (brick == null || brick.destroyed) continue;

                CollisionInfo info = ball.intersectAABB(brick.bounds);
                if (info.collided) {
                    result.hasCollision = true;
                    result.collisionNormal = info.normal;

                    if (brick.takeDamage(1)) {
                        // 砖块被击碎
                        result.score += brick.type.score;
                        result.destroyedCount++;

                        if (brick.type == BrickType.EXPLOSIVE) {
                            toExplode.add(brick);
                        }
                    }
                    break; // 每帧只处理一次碰撞,防止抖动
                }
            }
        }

        // 处理爆炸连锁
        for (Brick explosive : toExplode) {
            result.score += triggerExplosion(explosive);
        }

        return result;
    }

    /**
     * 触发爆炸效果,消除3x3范围内的砖块
     */
    private int triggerExplosion(Brick centerBrick) {
        int extraScore = 0;
        Vec2 center = centerBrick.bounds.center();
        double rangeX = brickWidth * 2.5;
        double rangeY = brickHeight * 2.5;

        for (int r = 0; r < rows; r++) {
            for (int c = 0; c < cols; c++) {
                Brick b = bricks[r][c];
                if (b == null || b.destroyed) continue;
                Vec2 bCenter = b.bounds.center();
                if (Math.abs(bCenter.x - center.x) <= rangeX &&
                    Math.abs(bCenter.y - center.y) <= rangeY) {
                    if (!b.destroyed) {
                        b.destroyed = true;
                        extraScore += b.type.score;
                    }
                }
            }
        }
        return extraScore;
    }

    public int getRemainingBricks() {
        int count = 0;
        for (Brick[] row : bricks) {
            for (Brick b : row) {
                if (b != null && !b.destroyed) count++;
            }
        }
        return count;
    }

    static class CollisionResult {
        boolean hasCollision = false;
        Vec2 collisionNormal = new Vec2(0, 0);
        int score = 0;
        int destroyedCount = 0;
    }
}

完整游戏循环

将上述模块整合为完整的游戏主循环。

/**
 * 打砖块游戏主引擎
 */
public class BreakoutGame {
    // 游戏配置参数
    static final double SCREEN_W = 800;
    static final double SCREEN_H = 600;
    static final double PADDLE_W = 120;
    static final double PADDLE_H = 15;
    static final double BALL_RADIUS = 8;
    static final double BALL_SPEED = 6.0;
    static final double MAX_BOUNCE_ANGLE = Math.toRadians(60);

    Vec2 ballPos;
    Vec2 ballVel;
    AABB paddle;
    BrickManager brickManager;
    int score = 0;
    int lives = 3;

    public BreakoutGame() {
        resetBall();
        paddle = new AABB(
            new Vec2(SCREEN_W / 2 - PADDLE_W / 2, SCREEN_H - 40),
            new Vec2(SCREEN_W / 2 + PADDLE_W / 2, SCREEN_H - 40 + PADDLE_H)
        );
        brickManager = new BrickManager(6, 10, 70, 25, 5);
    }

    private void resetBall() {
        ballPos = new Vec2(SCREEN_W / 2, SCREEN_H / 2);
        double angle = Math.toRadians(-45 + Math.random() * 90);
        ballVel = new Vec2(
            BALL_SPEED * Math.sin(angle),
            -BALL_SPEED * Math.cos(angle)
        );
    }

    /**
     * 游戏主循环中的单帧更新
     */
    public void update(double paddleTargetX) {
        // 1. 更新挡板位置(跟随鼠标/触摸)
        double paddleX = Math.max(0, Math.min(SCREEN_W - PADDLE_W, paddleTargetX));
        paddle.min.x = paddleX;
        paddle.max.x = paddleX + PADDLE_W;

        // 2. 更新球位置
        ballPos = ballPos.add(ballVel);

        // 3. 创建球碰撞体
        Circle ball = new Circle(ballPos, BALL_RADIUS);

        // 4. 检测与墙壁的碰撞
        if (ballPos.x - BALL_RADIUS <= 0 || ballPos.x + BALL_RADIUS >= SCREEN_W) {
            ballVel.x = -ballVel.x;
            ballPos.x = Math.max(BALL_RADIUS, Math.min(SCREEN_W - BALL_RADIUS, ballPos.x));
        }
        if (ballPos.y - BALL_RADIUS <= 0) {
            ballVel.y = -ballVel.y;
            ballPos.y = BALL_RADIUS;
        }

        // 5. 检测与挡板的碰撞
        CollisionInfo paddleCollision = ball.intersectAABB(paddle);
        if (paddleCollision.collided && ballVel.y > 0) {
            ballVel = PhysicsEngine.paddleReflect(ballVel, paddle, ballPos, MAX_BOUNCE_ANGLE);
            // 位置修正
            ballPos.y = paddle.min.y - BALL_RADIUS - 0.1;
        }

        // 6. 检测与砖块的碰撞
        BrickManager.CollisionResult brickResult = brickManager.checkBallCollisions(ball, ballVel);
        if (brickResult.hasCollision) {
            Vec2 reflected = PhysicsEngine.reflect(ballVel, brickResult.collisionNormal);
            ballVel.x = reflected.x;
            ballVel.y = reflected.y;
            score += brickResult.score;
        }

        // 7. 球掉落检测
        if (ballPos.y > SCREEN_H) {
            lives--;
            if (lives > 0) {
                resetBall();
            }
        }

        // 8. 胜利判定
        if (brickManager.getRemainingBricks() == 0) {
            System.out.println("胜利!最终得分:" + score);
        }
    }

    public static void main(String[] args) {
        BreakoutGame game = new BreakoutGame();
        // 模拟运行100帧
        for (int frame = 0; frame < 100; frame++) {
            game.update(game.ballPos.x); // 挡板跟随球(简化AI)
            if (game.lives <= 0) {
                System.out.println("游戏结束!得分:" + game.score);
                break;
            }
        }
    }
}

复杂度分析

算法模块 时间复杂度 空间复杂度 关键优化点
AABB相交检测 O(1) O(1) 四次比较运算
圆-AABB碰撞 O(1) O(1) 最近点法,避免开方直到必要时
砖块碰撞遍历 O(R × C) O(R × C) R=行数,C=列数;可用空间哈希优化
爆炸连锁 O(R × C) O(R × C) 最坏情况遍历全部砖块
反射计算 O(1) O(1) 向量点乘与减法

扩展方向

  1. 空间分割优化:使用四叉树或均匀网格管理砖块,将碰撞检测从O(n)降至O(log n)或O(1)
  2. 多球模式:支持同时存在多个小球,增加连击得分机制
  3. 道具系统:击碎特定砖块掉落道具(加长挡板、分裂球、激光等)
  4. 关卡编辑器:基于配置文件动态生成砖块阵列布局

总结

本文完整实现了打砖块游戏的核心算法体系:
– AABB包围盒实现高效碰撞检测
– 最近点法解决圆-矩形精确碰撞问题
– 向量反射公式模拟真实物理反弹
– 挡板角度控制增加游戏策略深度
– 爆炸连锁实现级联消除效果

读者可在此基础上接入JavaFX或Swing渲染层,打造一个可运行的完整游戏。