引言
弹珠台(Pinball)是起源于18世纪法国的 classic 桌面游戏,玩家通过控制挡板弹射钢珠,使其在布满障碍、缓冲器和坡道的台面上穿梭,尽可能获得高分。弹珠台的核心魅力在于其物理引擎——精确的碰撞检测、真实的反弹轨迹和引人入胜的动力学效果。本文用Java实现一个完整的2D弹珠台物理引擎,核心讲解AABB碰撞检测、圆形与线段碰撞的向量反射计算,以及基于欧拉积分的运动学模拟。
物理建模基础
坐标系与基本物理量
弹珠台采用二维平面坐标系,Y轴向下为正(符合屏幕坐标习惯)。每个物理实体具有:
- 位置向量
p = (x, y) - 速度向量
v = (vx, vy) - 加速度向量
a = (ax, ay) - 质量
m(弹珠质量归一化为1.0) - 弹性系数
e(0 ≤ e ≤ 1,e=1为完全弹性碰撞)
运动学更新(欧拉积分)
每一帧(时间步长 dt)按以下顺序更新:
a = F_gravity / m = (0, g) // 重力加速度
v = v + a * dt // 更新速度
p = p + v * dt // 更新位置
其中 g 取值为 500.0(像素/秒²),dt 通常为 1/60 秒。
重力与摩擦力
/**
* 弹珠物理实体类
*/
public class Ball {
public double x, y; // 位置
public double vx, vy; // 速度
public double radius; // 半径
public double mass; // 质量
public double elasticity; // 弹性系数
public double friction; // 摩擦系数
public Ball(double x, double y, double radius) {
this.x = x;
this.y = y;
this.radius = radius;
this.mass = 1.0;
this.elasticity = 0.85; // 85%弹性保留
this.friction = 0.995; // 每帧速度衰减0.5%
}
/**
* 物理更新:重力、速度、位置、摩擦
* @param dt 时间步长(秒)
* @param gravity 重力加速度
*/
public void update(double dt, double gravity) {
// 重力加速度
double ax = 0;
double ay = gravity;
// 更新速度(欧拉积分)
vx += ax * dt;
vy += ay * dt;
// 更新位置
x += vx * dt;
y += vy * dt;
// 应用摩擦力
vx *= friction;
vy *= friction;
// 速度阈值:防止数值漂移
if (Math.abs(vx) < 0.01) vx = 0;
if (Math.abs(vy) < 0.01) vy = 0;
}
/**
* 获取速度大小
*/
public double getSpeed() {
return Math.sqrt(vx * vx + vy * vy);
}
/**
* 获取速度方向(弧度)
*/
public double getVelocityAngle() {
return Math.atan2(vy, vx);
}
}
碰撞检测系统
AABB包围盒粗检测
在进行精确碰撞计算前,先用AABB(轴对齐包围盒)快速排除不相交的物体:
/**
* AABB包围盒
*/
public class AABB {
public 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;
}
public boolean intersects(AABB other) {
return this.maxX >= other.minX && this.minX <= other.maxX
&& this.maxY >= other.minY && this.minY <= other.maxY;
}
/**
* 从球体构建AABB
*/
public static AABB fromBall(Ball ball) {
return new AABB(
ball.x - ball.radius, ball.y - ball.radius,
ball.x + ball.radius, ball.y + ball.radius
);
}
}
圆形与线段的精确碰撞检测
弹珠台中最常见的碰撞是弹珠(圆形)与挡板、墙壁(线段)的碰撞。检测算法如下:
/**
* 线段类,表示弹珠台中的墙壁或挡板
*/
public class LineSegment {
public double x1, y1, x2, y2; // 端点坐标
public double nx, ny; // 法线方向(单位向量)
public double elasticity; // 弹性系数
public LineSegment(double x1, double y1, double x2, double y2, double elasticity) {
this.x1 = x1; this.y1 = y1;
this.x2 = x2; this.y2 = y2;
this.elasticity = elasticity;
// 计算法线(垂直于线段方向,指向碰撞侧)
double dx = x2 - x1;
double dy = y2 - y1;
double len = Math.sqrt(dx * dx + dy * dy);
// 法线:(dy, -dx) 或 (-dy, dx),这里取 (dy/len, -dx/len)
this.nx = dy / len;
this.ny = -dx / len;
}
/**
* 计算点到线段的最近距离和投影点
* @return [距离, 最近点x, 最近点y, 是否在线段范围内]
*/
public double[] pointToSegmentDistance(double px, double py) {
double dx = x2 - x1;
double dy = y2 - y1;
double len2 = dx * dx + dy * dy;
double t = Math.max(0, Math.min(1, ((px - x1) * dx + (py - y1) * dy) / len2));
double closestX = x1 + t * dx;
double closestY = y1 + t * dy;
double dist = Math.sqrt((px - closestX) * (px - closestX) + (py - closestY) * (py - closestY));
return new double[]{dist, closestX, closestY, t};
}
}
/**
* 碰撞检测器
*/
public class CollisionDetector {
/**
* 检测球与线段的碰撞
* @return 碰撞信息,无碰撞返回null
*/
public static CollisionInfo checkBallLineCollision(Ball ball, LineSegment line) {
// AABB粗检测
AABB ballAABB = AABB.fromBall(ball);
AABB lineAABB = new AABB(
Math.min(line.x1, line.x2), Math.min(line.y1, line.y2),
Math.max(line.x1, line.x2), Math.max(line.y1, line.y2)
);
if (!ballAABB.intersects(lineAABB)) return null;
// 精确检测:球心到线段距离
double[] result = line.pointToSegmentDistance(ball.x, ball.y);
double dist = result[0];
double closestX = result[1];
double closestY = result[2];
if (dist > ball.radius) return null;
// 计算碰撞法线(从线段指向球心)
double nx = ball.x - closestX;
double ny = ball.y - closestY;
double nLen = Math.sqrt(nx * nx + ny * ny);
if (nLen > 0) {
nx /= nLen;
ny /= nLen;
} else {
// 球心恰好在线段上,使用线段预计算法线
nx = line.nx;
ny = line.ny;
}
// 计算穿透深度
double penetration = ball.radius - dist;
return new CollisionInfo(ball, line, nx, ny, penetration, closestX, closestY);
}
static class CollisionInfo {
Ball ball;
LineSegment line;
double nx, ny; // 碰撞法线
double penetration; // 穿透深度
double contactX, contactY; // 碰撞点
CollisionInfo(Ball ball, LineSegment line, double nx, double ny,
double penetration, double contactX, double contactY) {
this.ball = ball;
this.line = line;
this.nx = nx; this.ny = ny;
this.penetration = penetration;
this.contactX = contactX;
this.contactY = contactY;
}
}
}
碰撞响应:向量反射
反射公式推导
当球以速度 v 碰撞到法线为 n 的表面时,反射速度 v' 由以下公式计算:
v' = v - 2(v · n)n
其中 v · n 是速度向量与法线的点积。如果表面有弹性系数 e,则:
v' = v - (1 + e)(v · n)n
碰撞响应实现
/**
* 碰撞响应处理器
*/
public class CollisionResponse {
/**
* 处理球与线段的碰撞响应
* @param info 碰撞信息
* @param restitution 弹性系数(取球和线的较小值)
*/
public static void resolveBallLineCollision(CollisionDetector.CollisionInfo info, double restitution) {
Ball ball = info.ball;
double nx = info.nx;
double ny = info.ny;
// 1. 位置修正:将球沿法线方向推出穿透深度
// 加上小量epsilon防止数值误差导致下一帧再次碰撞
double epsilon = 0.001;
ball.x += nx * (info.penetration + epsilon);
ball.y += ny * (info.penetration + epsilon);
// 2. 速度反射
double dot = ball.vx * nx + ball.vy * ny;
// 只有当球朝向线段运动时(dot < 0)才需要响应
if (dot < 0) {
double e = Math.min(ball.elasticity, restitution);
// v' = v - (1+e) * (v·n) * n
double impulse = -(1 + e) * dot;
ball.vx += impulse * nx;
ball.vy += impulse * ny;
}
}
/**
* 处理弹珠与圆形缓冲器(bumper)的碰撞
* 缓冲器通常是静止的圆形障碍物,碰撞后弹珠获得额外加速
*/
public static void resolveBallBumperCollision(Ball ball, Bumper bumper) {
double dx = ball.x - bumper.x;
double dy = ball.y - bumper.y;
double dist = Math.sqrt(dx * dx + dy * dy);
if (dist > ball.radius + bumper.radius) return;
// 法线方向
double nx = dx / dist;
double ny = dy / dist;
// 位置修正
double overlap = ball.radius + bumper.radius - dist;
ball.x += nx * overlap;
ball.y += ny * overlap;
// 速度反射 + 缓冲器加速
double dot = ball.vx * nx + ball.vy * ny;
if (dot < 0) {
double e = Math.min(ball.elasticity, bumper.elasticity);
double impulse = -(1 + e) * dot;
ball.vx += impulse * nx;
ball.vy += impulse * ny;
// 缓冲器额外加速(弹珠台特色机制)
double boostSpeed = bumper.boostFactor * ball.getSpeed();
double currentSpeed = ball.getSpeed();
if (currentSpeed > 0) {
double scale = (currentSpeed + boostSpeed) / currentSpeed;
ball.vx *= scale;
ball.vy *= scale;
}
}
}
/**
* 圆形缓冲器类
*/
public static class Bumper {
public double x, y;
public double radius;
public double elasticity;
public double boostFactor; // 加速倍数
public Bumper(double x, double y, double radius, double boostFactor) {
this.x = x; this.y = y;
this.radius = radius;
this.elasticity = 1.0; // 缓冲器通常完全弹性
this.boostFactor = boostFactor;
}
}
}
挡板(Flipper)交互系统
挡板运动学
挡板是弹珠台中玩家可控的关键部件,分为左挡板和右挡板,通过按键触发快速上摆:
/**
* 挡板类,模拟真实的摆动物理
*/
public class Flipper {
public double pivotX, pivotY; // 旋转轴心
public double length; // 挡板长度
public double restAngle; // 静止角度(弧度)
public double activeAngle; // 触发角度(弧度)
public double currentAngle; // 当前角度
public double angularVelocity; // 角速度
public boolean isActive; // 是否被触发
// 物理参数
private static final double SPRING_STRENGTH = 20.0; // 弹簧恢复系数
private static final double DAMPING = 0.85; // 阻尼
private static final double ACTIVE_FORCE = 35.0; // 触发驱动力
public Flipper(double pivotX, double pivotY, double length,
double restAngle, double activeAngle) {
this.pivotX = pivotX;
this.pivotY = pivotY;
this.length = length;
this.restAngle = restAngle;
this.activeAngle = activeAngle;
this.currentAngle = restAngle;
this.angularVelocity = 0;
this.isActive = false;
}
/**
* 更新挡板角度(每帧调用)
*/
public void update(double dt) {
double targetAngle = isActive ? activeAngle : restAngle;
// 弹簧-阻尼系统
double displacement = currentAngle - targetAngle;
double acceleration = -SPRING_STRENGTH * displacement;
if (isActive) acceleration += ACTIVE_FORCE; // 触发时额外推力
angularVelocity += acceleration * dt;
angularVelocity *= DAMPING;
currentAngle += angularVelocity * dt;
// 角度限制
double minAngle = Math.min(restAngle, activeAngle);
double maxAngle = Math.max(restAngle, activeAngle);
currentAngle = Math.max(minAngle, Math.min(maxAngle, currentAngle));
}
/**
* 获取挡板末端坐标
*/
public double getTipX() {
return pivotX + length * Math.cos(currentAngle);
}
public double getTipY() {
return pivotY + length * Math.sin(currentAngle);
}
/**
* 获取当前线段的碰撞几何信息
*/
public LineSegment getLineSegment() {
return new LineSegment(pivotX, pivotY, getTipX(), getTipY(), 0.95);
}
/**
* 计算挡板击球时对弹珠的额外冲量
* 基于挡板末端线速度:v = ω × r
*/
public double[] getImpulseAtTip() {
double tipVx = -angularVelocity * length * Math.sin(currentAngle);
double tipVy = angularVelocity * length * Math.cos(currentAngle);
return new double[]{tipVx, tipVy};
}
}
完整物理引擎主循环
/**
* 弹珠台物理引擎主类
*/
public class PinballPhysicsEngine {
private Ball ball;
private List<LineSegment> walls;
private List<CollisionResponse.Bumper> bumpers;
private List<Flipper> flippers;
private double gravity;
private double dt;
public PinballPhysicsEngine() {
this.gravity = 500.0; // 像素/秒²
this.dt = 1.0 / 60.0; // 60FPS
this.walls = new ArrayList<>();
this.bumpers = new ArrayList<>();
this.flippers = new ArrayList<>();
}
/**
* 初始化弹珠台场景
*/
public void initTable() {
// 创建弹珠
ball = new Ball(400, 100, 8);
ball.vx = 200;
ball.vy = 100;
// 添加边界墙壁
walls.add(new LineSegment(50, 50, 750, 50, 0.8)); // 顶墙
walls.add(new LineSegment(50, 50, 50, 550, 0.8)); // 左墙
walls.add(new LineSegment(750, 50, 750, 550, 0.8)); // 右墙
walls.add(new LineSegment(50, 550, 300, 550, 0.8)); // 底左
walls.add(new LineSegment(500, 550, 750, 550, 0.8)); // 底右
// 添加倾斜导向板
walls.add(new LineSegment(50, 300, 150, 450, 0.7));
walls.add(new LineSegment(750, 300, 650, 450, 0.7));
// 添加圆形缓冲器
bumpers.add(new CollisionResponse.Bumper(400, 250, 25, 0.3));
bumpers.add(new CollisionResponse.Bumper(300, 200, 20, 0.25));
bumpers.add(new CollisionResponse.Bumper(500, 200, 20, 0.25));
// 添加挡板(左、右)
flippers.add(new Flipper(300, 550, 80, Math.PI * 0.8, Math.PI * 0.3));
flippers.add(new Flipper(500, 550, 80, Math.PI * 0.2, Math.PI * 0.7));
}
/**
* 物理更新主循环(每帧调用)
*/
public void update() {
// 1. 更新弹珠运动
ball.update(dt, gravity);
// 2. 更新挡板
for (Flipper flipper : flippers) {
flipper.update(dt);
}
// 3. 碰撞检测与响应 - 墙壁
for (LineSegment wall : walls) {
CollisionDetector.CollisionInfo info =
CollisionDetector.checkBallLineCollision(ball, wall);
if (info != null) {
CollisionResponse.resolveBallLineCollision(info, wall.elasticity);
}
}
// 4. 碰撞检测与响应 - 挡板(动态线段)
for (Flipper flipper : flippers) {
LineSegment flipperLine = flipper.getLineSegment();
CollisionDetector.CollisionInfo info =
CollisionDetector.checkBallLineCollision(ball, flipperLine);
if (info != null) {
CollisionResponse.resolveBallLineCollision(info, flipperLine.elasticity);
// 挡板击球额外冲量
if (flipper.isActive) {
double[] impulse = flipper.getImpulseAtTip();
ball.vx += impulse[0] * 0.3; // 传递部分动量
ball.vy += impulse[1] * 0.3;
}
}
}
// 5. 碰撞检测与响应 - 缓冲器
for (CollisionResponse.Bumper bumper : bumpers) {
CollisionResponse.resolveBallBumperCollision(ball, bumper);
}
// 6. 球落入底部区域判定(游戏结束/重新发射)
if (ball.y > 600) {
resetBall();
}
}
private void resetBall() {
ball.x = 400;
ball.y = 100;
ball.vx = (Math.random() - 0.5) * 300;
ball.vy = 100;
}
public void setFlipperActive(boolean left, boolean active) {
if (left && flippers.size() > 0) flippers.get(0).isActive = active;
if (!left && flippers.size() > 1) flippers.get(1).isActive = active;
}
public static void main(String[] args) {
PinballPhysicsEngine engine = new PinballPhysicsEngine();
engine.initTable();
// 模拟运行1000帧
for (int frame = 0; frame < 1000; frame++) {
// 模拟按键(每200帧触发一次左挡板)
if (frame % 200 == 0) {
engine.setFlipperActive(true, true);
}
if (frame % 200 == 10) {
engine.setFlipperActive(true, false);
}
engine.update();
if (frame % 60 == 0) {
System.out.printf("Frame %d: Ball at (%.1f, %.1f), Speed=%.1f%n",
frame, engine.ball.x, engine.ball.y, engine.ball.getSpeed());
}
}
}
}
高级特性:多步碰撞与CCD
当弹珠速度过快时,单帧内可能穿透薄墙壁(隧道效应)。采用连续碰撞检测(CCD)解决:
/**
* 连续碰撞检测(简化版)
* 通过限制单帧最大移动距离,或采用射线投射检测
*/
public void updateWithCCD() {
double maxStep = ball.radius * 0.8; // 每子步最大移动距离
double remainingDist = ball.getSpeed() * dt;
int subSteps = (int) Math.ceil(remainingDist / maxStep);
subSteps = Math.max(1, Math.min(subSteps, 10)); // 限制1-10个子步
double subDt = dt / subSteps;
for (int i = 0; i < subSteps; i++) {
ball.update(subDt, gravity);
// 碰撞检测...
}
}
性能优化总结
| 技术 | 效果 | 适用场景 |
|---|---|---|
| AABB粗检测 | 排除90%以上无碰撞对 | 所有碰撞检测 |
| 空间哈希/网格 | O(1)邻域查询 | 大量缓冲器场景 |
| 子步积分 | 防止高速穿透 | 弹珠速度 > 半径/帧 |
| 休眠机制 | 静止弹珠停止更新 | 弹珠长时间静止 |
总结
本文构建了一个完整的2D弹珠台物理引擎,核心算法包括:
- 欧拉积分:基于离散时间步的位置-速度更新
- AABB粗检测 + 精确距离检测:高效且准确的碰撞检测流水线
- 向量反射公式:基于法线的精确碰撞响应
- 弹簧-阻尼系统:挡板的逼真摆动物理
- 连续碰撞检测:防止高速物体的隧道穿透
读者可以在此基础上扩展:加入旋转障碍物(力矩计算)、多球碰撞(动量守恒)、磁场/风力等外力场,以及完整的计分与关卡系统。弹珠台物理引擎的核心思想同样适用于 breakout、台球、弹球等众多经典游戏。