打砖块(Breakout)是街机黄金时代的经典之作,玩家操控底部挡板反弹小球击碎上方砖块。这款看似简单的游戏,其核心魅力在于物理碰撞的真实感——球与砖块、挡板、边界的每一次碰撞都需要精确的数学计算。本文将用Java完整实现带物理引擎的打砖块游戏,重点讲解AABB轴对齐边界框碰撞检测与反射向量计算两大核心算法。
一、核心算法原理
1.1 AABB碰撞检测
AABB(Axis-Aligned Bounding Box,轴对齐边界框)是游戏开发中最常用的碰撞检测技术。由于砖块、挡板和小球的运动区域均与坐标轴对齐,AABB检测可以简化为区间重叠判断:
对于两个矩形 R1(x1, y1, w1, h1) 和 R2(x2, y2, w2, h2),碰撞发生的充要条件是:
- X轴区间重叠:
x1 < x2 + w2且x1 + w1 > x2 - Y轴区间重叠:
y1 < y2 + h2且y1 + h1 > y2
该算法时间复杂度为 O(1),仅需4次比较运算,效率极高。
1.2 反射向量计算
小球碰撞后的反弹方向由入射向量和碰撞面法向量共同决定。设小球入射速度向量为 v,碰撞面法向量为 n(单位向量),则反射向量 r 的计算公式为:
r = v - 2 * (v · n) * n
其中 v · n 表示向量点积。对于水平碰撞面(如砖块上下边界),法向量为 (0, 1) 或 (0, -1),只需反转Y分量;对于垂直碰撞面,只需反转X分量。
1.3 挡板碰撞的角度控制
为增加游戏性,挡板碰撞采用相对位置映射法:根据小球击中挡板的水平位置,动态调整反射角度。击中挡板中心时垂直反弹,击中边缘时以较大角度斜向反弹。
二、完整Java实现
以下是可直接运行的完整项目结构,使用Swing进行渲染:
import javax.swing.*;
import java.awt.*;
import java.awt.event.*;
import java.util.ArrayList;
import java.util.List;
/**
* 打砖块游戏主类
* 核心算法:AABB碰撞检测 + 反射向量计算
*/
public class BreakoutGame extends JPanel implements ActionListener, KeyListener {
// 游戏常量
private static final int WIDTH = 800;
private static final int HEIGHT = 600;
private static final int BALL_RADIUS = 8;
private static final int PADDLE_WIDTH = 100;
private static final int PADDLE_HEIGHT = 12;
private static final int BRICK_ROWS = 5;
private static final int BRICK_COLS = 10;
private static final int BRICK_WIDTH = 70;
private static final int BRICK_HEIGHT = 25;
private static final int BRICK_GAP = 5;
private static final int TIMER_DELAY = 16; // ~60 FPS
// 游戏状态
private Ball ball;
private Paddle paddle;
private List<Brick> bricks;
private Timer timer;
private boolean gameRunning = true;
private int score = 0;
public BreakoutGame() {
setPreferredSize(new Dimension(WIDTH, HEIGHT));
setBackground(Color.BLACK);
setFocusable(true);
addKeyListener(this);
initGame();
}
/** 初始化游戏对象 */
private void initGame() {
// 小球初始位置在挡板上方
ball = new Ball(WIDTH / 2, HEIGHT - 60, 4, -4);
paddle = new Paddle(WIDTH / 2 - PADDLE_WIDTH / 2, HEIGHT - 40);
bricks = new ArrayList<>();
// 生成砖块网格
int startX = (WIDTH - (BRICK_COLS * (BRICK_WIDTH + BRICK_GAP))) / 2;
int startY = 50;
Color[] colors = {Color.RED, Color.ORANGE, Color.YELLOW, Color.GREEN, Color.CYAN};
for (int row = 0; row < BRICK_ROWS; row++) {
for (int col = 0; col < BRICK_COLS; col++) {
int x = startX + col * (BRICK_WIDTH + BRICK_GAP);
int y = startY + row * (BRICK_HEIGHT + BRICK_GAP);
bricks.add(new Brick(x, y, colors[row]));
}
}
timer = new Timer(TIMER_DELAY, this);
timer.start();
}
/** 每帧更新游戏逻辑 */
@Override
public void actionPerformed(ActionEvent e) {
if (!gameRunning) return;
ball.move();
checkWallCollision();
checkPaddleCollision();
checkBrickCollision();
checkGameOver();
repaint();
}
/** 检测小球与墙壁的碰撞 */
private void checkWallCollision() {
// 左右边界:反转X速度
if (ball.x - BALL_RADIUS <= 0 || ball.x + BALL_RADIUS >= WIDTH) {
ball.vx = -ball.vx;
ball.x = Math.max(BALL_RADIUS, Math.min(ball.x, WIDTH - BALL_RADIUS));
}
// 上边界:反转Y速度
if (ball.y - BALL_RADIUS <= 0) {
ball.vy = -ball.vy;
ball.y = BALL_RADIUS;
}
// 下边界:游戏结束
if (ball.y + BALL_RADIUS >= HEIGHT) {
gameRunning = false;
}
}
/**
* 检测小球与挡板的碰撞
* 使用AABB检测 + 相对位置映射调整反射角度
*/
private void checkPaddleCollision() {
if (ball.y + BALL_RADIUS >= paddle.y &&
ball.y - BALL_RADIUS <= paddle.y + PADDLE_HEIGHT &&
ball.x >= paddle.x &&
ball.x <= paddle.x + PADDLE_WIDTH) {
// 计算小球击中挡板的相对位置 [-1.0, 1.0]
double relativeIntersectX = (ball.x - (paddle.x + PADDLE_WIDTH / 2.0)) / (PADDLE_WIDTH / 2.0);
// 映射为反弹角度(最大60度)
double bounceAngle = relativeIntersectX * (Math.PI / 3);
double speed = Math.sqrt(ball.vx * ball.vx + ball.vy * ball.vy);
ball.vx = speed * Math.sin(bounceAngle);
ball.vy = -Math.abs(speed * Math.cos(bounceAngle));
ball.y = paddle.y - BALL_RADIUS - 1; // 防止粘连
}
}
/**
* 检测小球与砖块的碰撞(AABB)
* 对每个砖块执行O(1)的区间重叠检测
*/
private void checkBrickCollision() {
for (int i = bricks.size() - 1; i >= 0; i--) {
Brick brick = bricks.get(i);
if (!brick.alive) continue;
// AABB碰撞检测:4次比较
boolean collideX = ball.x + BALL_RADIUS > brick.x &&
ball.x - BALL_RADIUS < brick.x + BRICK_WIDTH;
boolean collideY = ball.y + BALL_RADIUS > brick.y &&
ball.y - BALL_RADIUS < brick.y + BRICK_HEIGHT;
if (collideX && collideY) {
brick.alive = false;
score += 10;
// 判断碰撞面方向,计算反射
double overlapLeft = (ball.x + BALL_RADIUS) - brick.x;
double overlapRight = (brick.x + BRICK_WIDTH) - (ball.x - BALL_RADIUS);
double overlapTop = (ball.y + BALL_RADIUS) - brick.y;
double overlapBottom = (brick.y + BRICK_HEIGHT) - (ball.y - BALL_RADIUS);
// 找到最小重叠方向,确定碰撞面
double minOverlap = Math.min(Math.min(overlapLeft, overlapRight),
Math.min(overlapTop, overlapBottom));
if (minOverlap == overlapLeft || minOverlap == overlapRight) {
ball.vx = -ball.vx; // 垂直面碰撞,反转X
} else {
ball.vy = -ball.vy; // 水平面碰撞,反转Y
}
bricks.remove(i);
break; // 每帧最多处理一次砖块碰撞
}
}
}
private void checkGameOver() {
if (bricks.isEmpty()) {
gameRunning = false; // 胜利
}
}
@Override
protected void paintComponent(Graphics g) {
super.paintComponent(g);
Graphics2D g2d = (Graphics2D) g;
g2d.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON);
// 绘制砖块
for (Brick brick : bricks) {
g2d.setColor(brick.color);
g2d.fillRect(brick.x, brick.y, BRICK_WIDTH, BRICK_HEIGHT);
g2d.setColor(Color.WHITE);
g2d.drawRect(brick.x, brick.y, BRICK_WIDTH, BRICK_HEIGHT);
}
// 绘制挡板
g2d.setColor(Color.WHITE);
g2d.fillRoundRect(paddle.x, paddle.y, PADDLE_WIDTH, PADDLE_HEIGHT, 8, 8);
// 绘制小球
g2d.setColor(Color.YELLOW);
g2d.fillOval((int)(ball.x - BALL_RADIUS), (int)(ball.y - BALL_RADIUS),
BALL_RADIUS * 2, BALL_RADIUS * 2);
// 绘制UI
g2d.setColor(Color.WHITE);
g2d.setFont(new Font("Arial", Font.BOLD, 18));
g2d.drawString("Score: " + score, 10, 25);
if (!gameRunning) {
g2d.setFont(new Font("Arial", Font.BOLD, 48));
String msg = bricks.isEmpty() ? "YOU WIN!" : "GAME OVER";
int msgWidth = g2d.getFontMetrics().stringWidth(msg);
g2d.drawString(msg, (WIDTH - msgWidth) / 2, HEIGHT / 2);
}
}
@Override
public void keyPressed(KeyEvent e) {
int speed = 20;
if (e.getKeyCode() == KeyEvent.VK_LEFT) {
paddle.x = Math.max(0, paddle.x - speed);
} else if (e.getKeyCode() == KeyEvent.VK_RIGHT) {
paddle.x = Math.min(WIDTH - PADDLE_WIDTH, paddle.x + speed);
} else if (e.getKeyCode() == KeyEvent.VK_R) {
initGame(); // 按R重新开始
}
}
@Override public void keyReleased(KeyEvent e) {}
@Override public void keyTyped(KeyEvent e) {}
public static void main(String[] args) {
SwingUtilities.invokeLater(() -> {
JFrame frame = new JFrame("Breakout - AABB碰撞检测与反射向量");
frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
frame.add(new BreakoutGame());
frame.pack();
frame.setLocationRelativeTo(null);
frame.setVisible(true);
});
}
}
/** 小球类:位置 + 速度向量 */
class Ball {
double x, y; // 位置
double vx, vy; // 速度向量
Ball(double x, double y, double vx, double vy) {
this.x = x;
this.y = y;
this.vx = vx;
this.vy = vy;
}
void move() {
x += vx;
y += vy;
}
}
/** 挡板类 */
class Paddle {
int x, y;
Paddle(int x, int y) {
this.x = x;
this.y = y;
}
}
/** 砖块类 */
class Brick {
int x, y;
Color color;
boolean alive = true;
Brick(int x, int y, Color color) {
this.x = x;
this.y = y;
this.color = color;
}
}
三、关键算法详解
3.1 碰撞面方向判定
砖块碰撞的核心难点在于判断小球从哪个方向击中砖块。本文采用最小重叠法:分别计算小球与砖块四个边的重叠深度,取最小值对应的边作为碰撞面。这种方法比简单的中心点判断更加精确,可以避免小球穿过砖角的错误反弹。
overlapLeft = ball右边缘 - brick左边缘
overlapRight = brick右边缘 - ball左边缘
overlapTop = ball下边缘 - brick上边缘
overlapBottom = brick下边缘 - ball上边缘
3.2 挡板角度映射
挡板碰撞不采用简单的垂直反弹,而是根据击中位置映射反弹角度:
relativeIntersectX = (ball.x - paddle中心) / (paddle半宽)
bounceAngle = relativeIntersectX * 60°
这使得玩家可以通过控制挡板击球位置来引导小球方向,增加了策略深度。
四、算法复杂度分析
| 模块 | 时间复杂度 | 空间复杂度 | 说明 |
|---|---|---|---|
| AABB碰撞检测(单对) | O(1) | O(1) | 4次浮点比较 |
| 砖块碰撞遍历 | O(n) | O(1) | n为存活砖块数 |
| 每帧总更新 | O(n) | O(n) | 砖块列表存储 |
| 反射向量计算 | O(1) | O(1) | 基本向量运算 |
在50块砖的典型场景下,每帧仅需约200次基本运算,在现代CPU上耗时不足1微秒,完全满足60 FPS的实时性要求。
五、扩展方向
- 多级砖块生命值:为砖块增加耐久度,使用不同颜色表示剩余生命,碰撞后更新而非直接销毁
- 道具系统:击碎特定砖块掉落道具(挡板加长、球分裂、激光射击),引入状态机管理道具效果
- 球分裂算法:实现多球同时运动,对每个球独立执行碰撞检测
- 关卡编辑器:基于二维数组描述砖块布局,支持从文件加载关卡配置
- 粒子特效:砖块击碎时生成粒子系统,使用简单的欧拉积分模拟粒子运动
六、总结
打砖块是理解物理碰撞检测的绝佳入门项目。本文实现的AABB碰撞检测与反射向量计算是2D游戏开发的基石算法,广泛应用于平台跳跃、射击、弹球等各类游戏。掌握这些基础后,你可以进一步学习OBB(定向边界框)、分离轴定理(SAT)等更高级的碰撞检测技术,向更复杂的物理引擎迈进。