弹球游戏是街机时代的经典作品,玩家操控挡板反弹小球击碎砖块。其核心算法在于精确的碰撞检测与物理反弹计算。本文用Java实现一个带物理引擎的弹球游戏,重点讲解二维向量运算、边界反射定律以及球体与矩形碰撞检测算法,包含可直接运行的完整代码与复杂度分析。
一、问题分析与物理模型
弹球游戏的物理模型包含三个核心要素:小球运动、边界反射和碰撞检测。小球在二维平面上运动,速度用向量 (vx, vy) 表示。当小球碰到边界或挡板/砖块时,需要根据入射角计算反射方向。碰撞检测则需要判断圆形与矩形的相交关系。
反射定律的核心是:入射角等于反射角。在向量表示中,若碰撞面的法向量为 n,入射速度为 v,则反射速度 v' 为:
v' = v - 2 * (v · n) * n
其中 · 表示点积运算。当碰撞面为水平面时,法向量为 (0, 1) 或 (0, -1),反射仅改变 vy 符号;垂直面则仅改变 vx 符号。
二、二维向量工具类
首先实现一个通用的二维向量类,封装向量运算:
/**
* 二维向量类,封装向量运算
*/
public class Vector2D {
public double x;
public double y;
public Vector2D(double x, double y) {
this.x = x;
this.y = y;
}
/** 向量加法 */
public Vector2D add(Vector2D other) {
return new Vector2D(this.x + other.x, this.y + other.y);
}
/** 向量减法 */
public Vector2D subtract(Vector2D other) {
return new Vector2D(this.x - other.x, this.y - other.y);
}
/** 数乘 */
public Vector2D scale(double scalar) {
return new Vector2D(this.x * scalar, this.y * scalar);
}
/** 点积 */
public double dot(Vector2D other) {
return this.x * other.x + this.y * other.y;
}
/** 向量长度 */
public double length() {
return Math.sqrt(x * x + y * y);
}
/** 单位向量 */
public Vector2D normalize() {
double len = length();
if (len < 1e-9) return new Vector2D(0, 0);
return new Vector2D(x / len, y / len);
}
/** 沿法向量反射 */
public Vector2D reflect(Vector2D normal) {
Vector2D n = normal.normalize();
double dot = this.dot(n);
return this.subtract(n.scale(2 * dot));
}
@Override
public String toString() {
return String.format("(%.2f, %.2f)", x, y);
}
}
三、小球与碰撞体定义
/**
* 小球实体,包含位置、速度、半径
*/
public class Ball {
public Vector2D position; // 球心位置
public Vector2D velocity; // 速度向量
public double radius; // 半径
public double speed; // 基础速度大小
public Ball(double x, double y, double radius, double speed) {
this.position = new Vector2D(x, y);
this.radius = radius;
this.speed = speed;
// 初始速度向右上45度
this.velocity = new Vector2D(speed / Math.sqrt(2), -speed / Math.sqrt(2));
}
/** 更新位置 */
public void update(double dt) {
position = position.add(velocity.scale(dt));
}
}
/**
* 矩形碰撞体,用于挡板和砖块
*/
public class Rectangle {
public double x, y, width, height;
public Rectangle(double x, double y, double width, double height) {
this.x = x;
this.y = y;
this.width = width;
this.height = height;
}
/** 获取矩形中心点 */
public Vector2D center() {
return new Vector2D(x + width / 2, y + height / 2);
}
/** 判断点是否在矩形内 */
public boolean contains(Vector2D p) {
return p.x >= x && p.x <= x + width && p.y >= y && p.y <= y + height;
}
}
四、碰撞检测算法
4.1 圆与矩形碰撞检测
圆与矩形的碰撞检测是弹球游戏的核心。标准算法是:找到矩形上距离圆心最近的点,若该点到圆心的距离小于半径,则发生碰撞。
public class CollisionDetector {
/**
* 计算圆与矩形的碰撞信息
* @return 碰撞信息,若无碰撞返回null
*/
public static CollisionInfo circleRectangle(Ball ball, Rectangle rect) {
// 找到矩形边界上距离圆心最近的点( Clamp投影法)
double closestX = clamp(ball.position.x, rect.x, rect.x + rect.width);
double closestY = clamp(ball.position.y, rect.y, rect.y + rect.height);
Vector2D closestPoint = new Vector2D(closestX, closestY);
Vector2D diff = ball.position.subtract(closestPoint);
double distSq = diff.x * diff.x + diff.y * diff.y;
// 圆心在矩形内部(特殊处理:找到最近边)
if (rect.contains(ball.position)) {
// 计算到四条边的距离
double distLeft = ball.position.x - rect.x;
double distRight = rect.x + rect.width - ball.position.x;
double distTop = ball.position.y - rect.y;
double distBottom = rect.y + rect.height - ball.position.y;
double minDist = Math.min(Math.min(distLeft, distRight),
Math.min(distTop, distBottom));
Vector2D normal;
if (minDist == distLeft) normal = new Vector2D(-1, 0);
else if (minDist == distRight) normal = new Vector2D(1, 0);
else if (minDist == distTop) normal = new Vector2D(0, -1);
else normal = new Vector2D(0, 1);
return new CollisionInfo(true, normal, ball.radius + minDist);
}
// 无碰撞
if (distSq > ball.radius * ball.radius) {
return null;
}
// 计算碰撞法向量
double dist = Math.sqrt(distSq);
Vector2D normal;
if (dist < 1e-9) {
// 圆心恰好投影到角点,法向量指向圆心到矩形中心
normal = ball.position.subtract(rect.center()).normalize();
} else {
normal = diff.scale(-1.0 / dist).normalize();
}
double penetration = ball.radius - dist;
return new CollisionInfo(true, normal, penetration);
}
private static double clamp(double value, double min, double max) {
return Math.max(min, Math.min(max, value));
}
}
/** 碰撞信息记录 */
public class CollisionInfo {
public boolean collided; // 是否碰撞
public Vector2D normal; // 碰撞法向量(指向球体外)
public double penetration; // 穿透深度
public CollisionInfo(boolean collided, Vector2D normal, double penetration) {
this.collided = collided;
this.normal = normal;
this.penetration = penetration;
}
}
4.2 边界碰撞处理
/**
* 处理小球与游戏边界的碰撞
* @param ball 小球
* @param gameWidth 游戏区域宽度
* @param gameHeight 游戏区域高度
* @return 若底部边界被穿出(球掉落),返回true
*/
public static boolean handleBoundaryCollision(Ball ball, double gameWidth, double gameHeight) {
boolean lost = false;
// 左边界
if (ball.position.x - ball.radius < 0) {
ball.position.x = ball.radius;
ball.velocity.x = Math.abs(ball.velocity.x);
}
// 右边界
if (ball.position.x + ball.radius > gameWidth) {
ball.position.x = gameWidth - ball.radius;
ball.velocity.x = -Math.abs(ball.velocity.x);
}
// 上边界
if (ball.position.y - ball.radius < 0) {
ball.position.y = ball.radius;
ball.velocity.y = Math.abs(ball.velocity.y);
}
// 下边界(球掉落判定)
if (ball.position.y - ball.radius > gameHeight) {
lost = true;
}
return lost;
}
五、反射物理与挡板交互
当小球碰撞挡板时,除了基本的反射,还可以根据碰撞位置改变反射角度,增加游戏操控性:击中挡板中心时垂直反弹,击中边缘时大角度反弹。
/**
* 处理小球与挡板的碰撞,支持根据碰撞位置调整反射角度
*/
public static void handlePaddleCollision(Ball ball, Rectangle paddle) {
CollisionInfo info = CollisionDetector.circleRectangle(ball, paddle);
if (info == null || !info.collided) return;
// 防止球陷入挡板:沿法向量推出
ball.position = ball.position.add(info.normal.scale(info.penetration));
// 计算碰撞点在挡板上的相对位置 [-1, 1]
double relativeIntersectX = (ball.position.x - paddle.center().x) / (paddle.width / 2);
relativeIntersectX = Math.max(-0.99, Math.min(0.99, relativeIntersectX));
// 根据碰撞位置调整反射角度,最大75度
double bounceAngle = relativeIntersectX * (Math.PI / 2 * 0.83);
double speed = ball.velocity.length();
ball.velocity = new Vector2D(
speed * Math.sin(bounceAngle),
-speed * Math.cos(bounceAngle)
);
}
六、完整游戏引擎
import java.util.ArrayList;
import java.util.List;
/**
* 弹球游戏核心引擎
*/
public class PinballEngine {
private static final double GAME_WIDTH = 800;
private static final double GAME_HEIGHT = 600;
private static final double BALL_RADIUS = 8;
private static final double PADDLE_WIDTH = 120;
private static final double PADDLE_HEIGHT = 16;
private static final double BALL_SPEED = 400;
private static final double DT = 1.0 / 60.0; // 60FPS
private Ball ball;
private Rectangle paddle;
private List<Rectangle> bricks;
private int score;
private boolean gameOver;
public PinballEngine() {
reset();
}
public void reset() {
ball = new Ball(GAME_WIDTH / 2, GAME_HEIGHT / 2, BALL_RADIUS, BALL_SPEED);
paddle = new Rectangle(
(GAME_WIDTH - PADDLE_WIDTH) / 2,
GAME_HEIGHT - 50,
PADDLE_WIDTH, PADDLE_HEIGHT
);
bricks = new ArrayList<>();
initBricks();
score = 0;
gameOver = false;
}
/** 初始化砖块阵列 */
private void initBricks() {
int rows = 5;
int cols = 10;
double brickWidth = (GAME_WIDTH - 40) / cols;
double brickHeight = 25;
for (int r = 0; r < rows; r++) {
for (int c = 0; c < cols; c++) {
bricks.add(new Rectangle(
20 + c * brickWidth,
40 + r * brickHeight,
brickWidth - 4,
brickHeight - 4
));
}
}
}
/** 主更新循环,每帧调用一次 */
public void update() {
if (gameOver) return;
ball.update(DT);
// 边界碰撞
if (CollisionUtil.handleBoundaryCollision(ball, GAME_WIDTH, GAME_HEIGHT)) {
gameOver = true;
return;
}
// 挡板碰撞
CollisionUtil.handlePaddleCollision(ball, paddle);
// 砖块碰撞检测与消除
for (int i = bricks.size() - 1; i >= 0; i--) {
Rectangle brick = bricks.get(i);
CollisionInfo info = CollisionDetector.circleRectangle(ball, brick);
if (info != null && info.collided) {
// 反射速度
ball.velocity = ball.velocity.reflect(info.normal);
// 推出穿透部分
ball.position = ball.position.add(info.normal.scale(info.penetration));
bricks.remove(i);
score += 10;
break; // 每帧最多消除一个砖块
}
}
}
/** 移动挡板 */
public void movePaddle(double mouseX) {
paddle.x = mouseX - paddle.width / 2;
paddle.x = Math.max(0, Math.min(GAME_WIDTH - paddle.width, paddle.x));
}
public boolean isGameOver() { return gameOver; }
public int getScore() { return score; }
public int getRemainingBricks() { return bricks.size(); }
public Ball getBall() { return ball; }
public Rectangle getPaddle() { return paddle; }
public List<Rectangle> getBricks() { return bricks; }
public double getWidth() { return GAME_WIDTH; }
public double getHeight() { return GAME_HEIGHT; }
}
七、复杂度分析
| 操作 | 时间复杂度 | 空间复杂度 | 说明 |
|---|---|---|---|
| 向量运算 | O(1) | O(1) | 基础数学运算 |
| 圆-矩形碰撞检测 | O(1) | O(1) | Clamp投影法,常数时间 |
| 砖块碰撞检测 | O(n) | O(1) | n为砖块数量,每帧线性扫描 |
| 反射计算 | O(1) | O(1) | 向量点积与数乘 |
优化建议:当砖块数量极大时,可使用 空间哈希 或 四叉树 将碰撞检测优化至平均 O(log n) 或 O(1)。
八、总结
本文完整实现了弹球游戏的物理引擎核心算法,重点包括:
- 向量运算体系:用
Vector2D类封装二维向量的加减、点积、数乘、归一化与反射运算 - 圆-矩形碰撞检测:利用 Clamp 投影法找到最近点,判断相交并计算法向量与穿透深度
- 反射定律实现:通过
v' = v - 2(v·n)n公式精确计算反射速度 - 挡板交互增强:根据碰撞相对位置调整反射角度,提升操控感
这些技术不仅适用于弹球游戏,也可推广到任意需要物理碰撞的2D游戏开发中。