引言
弹珠台(Pinball)是街机厅中经典的物理游戏,玩家通过控制挡板让弹珠在倾斜的桌面上弹射,击中各种目标得分。弹珠台的魅力在于其复杂的物理交互:弹珠与边界的碰撞、与挡板的反弹、与缓冲器的弹性碰撞等。本文将用Java实现一个简化版弹珠台模拟器,重点讲解圆形碰撞检测与反射向量计算两大核心算法。
核心算法原理
1. 圆形与线段碰撞检测
弹珠可视为一个圆,桌面边界由若干线段构成。判断圆与线段是否碰撞,需要计算圆心到线段的最近距离:
- 设线段端点为
A(x1,y1)和B(x2,y2),圆心为C(cx,cy),半径为r - 计算向量
AB = B - A,AC = C - A - 将
AC投影到AB上,得到参数t = (AC·AB) / (AB·AB) - 若
t < 0,最近点为A;若t > 1,最近点为B;否则最近点为A + t·AB - 若圆心到最近点的距离 ≤
r,则发生碰撞
2. 反射向量计算
碰撞后弹珠速度需要沿碰撞法线方向反射。设入射速度为 v,碰撞法线为单位向量 n:
- 将速度分解为法向分量
v_n = (v·n) * n和切向分量v_t = v - v_n - 反射后速度
v' = v_t - v_n,即法向分量反向,切向分量不变 - 考虑能量损失,乘以恢复系数
e(0 < e ≤ 1):v' = v_t - e * v_n
Java完整实现
import java.util.ArrayList;
import java.util.List;
/**
* 简化版弹珠台物理模拟器
* 核心算法:圆形碰撞检测 + 反射向量计算
*/
public class PinballSimulator {
// 物理参数
private static final double GRAVITY = 0.15; // 重力加速度(向下)
private static final double FRICTION = 0.998; // 摩擦力衰减系数
private static final double RESTITUTION = 0.85; // 碰撞恢复系数(弹性)
private static final double BALL_RADIUS = 6.0; // 弹珠半径
// 弹珠状态
static class Ball {
double x, y; // 位置
double vx, vy; // 速度
int score; // 得分
Ball(double x, double y) {
this.x = x;
this.y = y;
this.vx = 3.0;
this.vy = -2.0;
this.score = 0;
}
}
// 线段障碍物(桌面边界或挡板)
static class LineSegment {
double x1, y1, x2, y2; // 端点坐标
boolean isBumper; // 是否为缓冲器(弹性更高)
int pointValue; // 击中得分
LineSegment(double x1, double y1, double x2, double y2,
boolean isBumper, int pointValue) {
this.x1 = x1; this.y1 = y1;
this.x2 = x2; this.y2 = y2;
this.isBumper = isBumper;
this.pointValue = pointValue;
}
}
// 圆形障碍物(缓冲柱)
static class CircleBumper {
double cx, cy, radius;
int pointValue;
CircleBumper(double cx, double cy, double radius, int pointValue) {
this.cx = cx; this.cy = cy;
this.radius = radius;
this.pointValue = pointValue;
}
}
private final Ball ball;
private final List<LineSegment> walls;
private final List<CircleBumper> bumpers;
public PinballSimulator() {
this.ball = new Ball(50, 100);
this.walls = new ArrayList<>();
this.bumpers = new ArrayList<>();
initTable();
}
/**
* 初始化弹珠台布局
*/
private void initTable() {
// 桌面外边界(梯形桌面)
walls.add(new LineSegment(10, 10, 90, 10, false, 0)); // 顶边
walls.add(new LineSegment(90, 10, 95, 200, false, 0)); // 右边
walls.add(new LineSegment(95, 200, 5, 200, false, 0)); // 底边
walls.add(new LineSegment(5, 200, 10, 10, false, 0)); // 左边
// 左挡板(可活动)
walls.add(new LineSegment(20, 180, 45, 170, true, 0));
// 右挡板(可活动)
walls.add(new LineSegment(55, 170, 80, 180, true, 0));
// 中间障碍物
walls.add(new LineSegment(40, 80, 60, 80, false, 50)); // 中间横杆
walls.add(new LineSegment(30, 50, 30, 70, false, 30)); // 左立柱
walls.add(new LineSegment(70, 50, 70, 70, false, 30)); // 右立柱
// 圆形缓冲柱
bumpers.add(new CircleBumper(50, 40, 8, 100));
bumpers.add(new CircleBumper(35, 120, 6, 50));
bumpers.add(new CircleBumper(65, 120, 6, 50));
}
/**
* 核心算法1:计算点到线段的最近点
* @param px 点x坐标
* @param py 点y坐标
* @param seg 线段
* @return 最近点坐标 [x, y]
*/
public double[] closestPointOnSegment(double px, double py, LineSegment seg) {
double abx = seg.x2 - seg.x1;
double aby = seg.y2 - seg.y1;
double acx = px - seg.x1;
double acy = py - seg.y1;
// 计算投影参数 t = (AC·AB) / |AB|^2
double abLenSq = abx * abx + aby * aby;
if (abLenSq < 1e-10) {
return new double[]{seg.x1, seg.y1}; // 退化为点
}
double t = (acx * abx + acy * aby) / abLenSq;
// 限制 t 在 [0, 1] 范围内
t = Math.max(0.0, Math.min(1.0, t));
double closestX = seg.x1 + t * abx;
double closestY = seg.y1 + t * aby;
return new double[]{closestX, closestY};
}
/**
* 核心算法2:反射向量计算
* @param vx 入射速度x分量
* @param vy 入射速度y分量
* @param nx 法线x分量(单位向量)
* @param ny 法线y分量(单位向量)
* @param restitution 恢复系数
* @return 反射后的速度 [vx', vy']
*/
public double[] reflect(double vx, double vy,
double nx, double ny, double restitution) {
// 计算速度在法线方向的投影(点积)
double dot = vx * nx + vy * ny;
// 切向分量 = 原速度 - 法向分量
double tx = vx - dot * nx;
double ty = vy - dot * ny;
// 法向分量反向并乘以恢复系数
double rx = tx - restitution * dot * nx;
double ry = ty - restitution * dot * ny;
return new double[]{rx, ry};
}
/**
* 检测并处理弹珠与所有线段的碰撞
*/
public void handleLineCollisions() {
for (LineSegment wall : walls) {
double[] closest = closestPointOnSegment(ball.x, ball.y, wall);
double dx = ball.x - closest[0];
double dy = ball.y - closest[1];
double distSq = dx * dx + dy * dy;
if (distSq <= BALL_RADIUS * BALL_RADIUS) {
// 计算碰撞法线(从最近点指向圆心)
double dist = Math.sqrt(distSq);
if (dist < 1e-10) continue; // 避免除零
double nx = dx / dist;
double ny = dy / dist;
// 判断弹珠是否正在朝向线段运动(防止粘连)
double approach = ball.vx * nx + ball.vy * ny;
if (approach > 0) continue;
// 计算反射速度
double rest = wall.isBumper ? 0.95 : RESTITUTION;
double[] reflected = reflect(ball.vx, ball.vy, nx, ny, rest);
ball.vx = reflected[0];
ball.vy = reflected[1];
// 将弹珠推出碰撞区域,防止粘连
double overlap = BALL_RADIUS - dist + 0.5;
ball.x += nx * overlap;
ball.y += ny * overlap;
// 计分
ball.score += wall.pointValue;
}
}
}
/**
* 检测并处理弹珠与圆形缓冲柱的碰撞
*/
public void handleCircleCollisions() {
for (CircleBumper bumper : bumpers) {
double dx = ball.x - bumper.cx;
double dy = ball.y - bumper.cy;
double dist = Math.sqrt(dx * dx + dy * dy);
double minDist = BALL_RADIUS + bumper.radius;
if (dist <= minDist) {
// 碰撞法线
double nx = dx / dist;
double ny = dy / dist;
double approach = ball.vx * nx + ball.vy * ny;
if (approach > 0) continue;
double[] reflected = reflect(ball.vx, ball.vy, nx, ny, 1.0);
// 缓冲柱给予额外速度奖励
ball.vx = reflected[0] * 1.05;
ball.vy = reflected[1] * 1.05;
double overlap = minDist - dist + 0.5;
ball.x += nx * overlap;
ball.y += ny * overlap;
ball.score += bumper.pointValue;
}
}
}
/**
* 模拟一帧物理更新
*/
public void update() {
// 应用重力(桌面倾斜,重力有x和y分量)
ball.vy += GRAVITY * 0.3; // 沿桌面下滑分量
ball.vx += GRAVITY * 0.8; // 向底部滚动的分量
// 应用摩擦力
ball.vx *= FRICTION;
ball.vy *= FRICTION;
// 更新位置
ball.x += ball.vx;
ball.y += ball.vy;
// 碰撞检测与响应
handleLineCollisions();
handleCircleCollisions();
}
/**
* 控制台可视化输出
*/
public void render() {
int width = 100, height = 40;
char[][] canvas = new char[height][width];
for (int i = 0; i < height; i++) {
for (int j = 0; j < width; j++) {
canvas[i][j] = ' ';
}
}
// 绘制边界
for (LineSegment wall : walls) {
drawLine(canvas, wall.x1, wall.y1, wall.x2, wall.y2, wall.isBumper ? '=' : '#');
}
// 绘制缓冲柱
for (CircleBumper b : bumpers) {
int cx = (int) b.cx;
int cy = (int) b.cy;
int r = (int) b.radius;
for (int dy = -r; dy <= r; dy++) {
for (int dx = -r; dx <= r; dx++) {
if (dx * dx + dy * dy <= r * r) {
int px = cx + dx, py = cy + dy;
if (px >= 0 && px < width && py >= 0 && py < height) {
canvas[py][px] = 'O';
}
}
}
}
}
// 绘制弹珠
int bx = (int) ball.x;
int by = (int) ball.y;
if (bx >= 0 && bx < width && by >= 0 && by < height) {
canvas[by][bx] = '*';
}
// 输出画面
StringBuilder sb = new StringBuilder();
sb.append("┌" + "─".repeat(width) + "┐\n");
for (int i = 0; i < height; i++) {
sb.append("│");
for (int j = 0; j < width; j++) {
sb.append(canvas[i][j]);
}
sb.append("│\n");
}
sb.append("└" + "─".repeat(width) + "┘\n");
sb.append(String.format("得分: %d | 速度: (%.2f, %.2f) | 位置: (%.1f, %.1f)\n",
ball.score, ball.vx, ball.vy, ball.x, ball.y));
System.out.print("\033[H\033[2J"); // 清屏
System.out.println(sb);
}
private void drawLine(char[][] canvas, double x1, double y1, double x2, double y2, char ch) {
int steps = (int) (Math.max(Math.abs(x2 - x1), Math.abs(y2 - y1)) * 2);
for (int i = 0; i <= steps; i++) {
double t = (double) i / steps;
int x = (int) (x1 + t * (x2 - x1));
int y = (int) (y1 + t * (y2 - y1));
if (x >= 0 && x < canvas[0].length && y >= 0 && y < canvas.length) {
canvas[y][x] = ch;
}
}
}
public static void main(String[] args) throws InterruptedException {
PinballSimulator sim = new PinballSimulator();
System.out.println("=== 弹珠台物理模拟器 ===");
System.out.println("* = 弹珠, O = 缓冲柱, # = 边界, = = 挡板");
Thread.sleep(1500);
for (int frame = 0; frame < 500; frame++) {
sim.update();
sim.render();
Thread.sleep(50);
// 弹珠掉出底部则重置
if (sim.ball.y > 210) {
sim.ball.x = 50;
sim.ball.y = 100;
sim.ball.vx = 2 + Math.random() * 3;
sim.ball.vy = -1 - Math.random() * 2;
}
}
}
}
算法复杂度分析
| 操作 | 时间复杂度 | 空间复杂度 | 说明 |
|---|---|---|---|
| 点到线段最近点计算 | O(1) | O(1) | 向量投影运算 |
| 反射向量计算 | O(1) | O(1) | 简单的向量分解与合成 |
| 单帧碰撞检测 | O(N+M) | O(1) | N为线段数,M为圆形缓冲柱数 |
| 整体模拟 | O(T×(N+M)) | O(1) | T为模拟帧数 |
总结
本文通过Java实现了一个简化版弹珠台物理模拟器,重点实现了两个核心算法:
- 圆形与线段碰撞检测:利用向量投影计算圆心到线段的最近距离,判断碰撞并计算碰撞法线。
- 反射向量计算:将速度分解为法向和切向分量,法向分量反向实现反弹,并引入恢复系数模拟能量损失。
这两个算法不仅是弹珠台的基础,也广泛应用于游戏物理引擎、光线追踪和刚体碰撞模拟等领域。理解反射向量的数学本质,对掌握计算机图形学和物理模拟至关重要。