一、游戏介绍与算法目标
打砖块(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)。
五、关键设计要点
- 碰撞法向量方向:始终从矩形指向圆心,确保反射后弹球远离碰撞体,避免”粘连”现象。
- 挡板X速度微调:击中挡板边缘时增加水平速度分量,让游戏更具策略性。
- 单帧单碰撞:每帧只处理第一个碰撞对象,防止弹球在密集砖块间反复反弹导致速度异常。
- 数值稳定性:碰撞后若弹球仍与砖块相交,可在反射后额外沿法向量偏移1像素,消除浮点误差。
六、扩展方向
- 多球模式:管理多个弹球对象,碰撞检测升级为批量查询
- 道具系统:添加分裂球、激光、挡板加长等道具,引入状态机管理
- 关卡编辑器:基于JSON的砖块布局配置,支持不同砖块生命值
- 粒子特效:砖块击碎时生成粒子,使用对象池优化内存分配