引言
《打砖块》(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) | 向量点乘与减法 |
扩展方向
- 空间分割优化:使用四叉树或均匀网格管理砖块,将碰撞检测从O(n)降至O(log n)或O(1)
- 多球模式:支持同时存在多个小球,增加连击得分机制
- 道具系统:击碎特定砖块掉落道具(加长挡板、分裂球、激光等)
- 关卡编辑器:基于配置文件动态生成砖块阵列布局
总结
本文完整实现了打砖块游戏的核心算法体系:
– AABB包围盒实现高效碰撞检测
– 最近点法解决圆-矩形精确碰撞问题
– 向量反射公式模拟真实物理反弹
– 挡板角度控制增加游戏策略深度
– 爆炸连锁实现级联消除效果
读者可在此基础上接入JavaFX或Swing渲染层,打造一个可运行的完整游戏。