引言
弹球游戏(Pinball)融合了物理碰撞、精确反射与得分机制,核心难点在于球体与各种挡板、障碍物之间的弹性碰撞与向量反射计算。本文用Java实现弹球物理引擎,重点讲解向量反射公式、碰撞响应以及刚体碰撞检测算法。
核心算法概览
- 向量反射:球碰到挡板或墙壁时,按入射角等于反射角反弹
- 弹性碰撞:考虑恢复系数,模拟能量损失与反弹衰减
- 碰撞检测:球与圆形/矩形障碍物的相交检测
向量数学基础
/**
* 二维向量类,封装向量运算
*/
public class Vector2D {
public double x, y;
public Vector2D(double x, double y) {
this.x = x; this.y = y;
}
public Vector2D add(Vector2D v) {
return new Vector2D(x + v.x, y + v.y);
}
public Vector2D sub(Vector2D v) {
return new Vector2D(x - v.x, y - v.y);
}
public Vector2D mul(double s) {
return new Vector2D(x * s, y * s);
}
public double dot(Vector2D v) {
return x * v.x + y * v.y;
}
public double length() {
return Math.sqrt(x * x + y * y);
}
public Vector2D normalize() {
double len = length();
if (len == 0) return new Vector2D(0, 0);
return new Vector2D(x / len, y / len);
}
/** 向量在法线方向上的反射:r = v - 2(v·n)n */
public Vector2D reflect(Vector2D normal) {
Vector2D n = normal.normalize();
double dot = this.dot(n);
return this.sub(n.mul(2 * dot));
}
@Override
public String toString() {
return String.format("(%.3f, %.3f)", x, y);
}
}
弹球物理实体
/**
* 弹球实体,包含位置、速度、半径等物理属性
*/
public class Ball {
private Vector2D position; // 位置
private Vector2D velocity; // 速度向量
private double radius; // 半径
private double mass; // 质量
private double restitution; // 恢复系数(0=完全非弹性,1=完全弹性)
public Ball(double x, double y, double radius, double vx, double vy) {
this.position = new Vector2D(x, y);
this.velocity = new Vector2D(vx, vy);
this.radius = radius;
this.mass = radius * radius; // 质量与面积成正比
this.restitution = 0.85; // 85%弹性恢复
}
/**
* 每帧更新位置(欧拉积分)
*/
public void update(double dt) {
position = position.add(velocity.mul(dt));
}
/**
* 应用重力加速度
*/
public void applyGravity(double g, double dt) {
velocity.y += g * dt;
}
/**
* 沿法线方向反射速度向量
*/
public void bounce(Vector2D normal) {
velocity = velocity.reflect(normal).mul(restitution);
}
public Vector2D getPosition() { return position; }
public Vector2D getVelocity() { return velocity; }
public double getRadius() { return radius; }
public double getMass() { return mass; }
public void setPosition(Vector2D p) { this.position = p; }
public void setVelocity(Vector2D v) { this.velocity = v; }
}
碰撞检测器
球与线段(挡板/墙壁)的碰撞
public class CollisionDetector {
/**
* 检测球与线段的碰撞
* @param ball 弹球
* @param p1, p2 线段两端点
* @return 碰撞信息,无碰撞返回null
*/
public CollisionResult checkBallSegment(Ball ball, Vector2D p1, Vector2D p2) {
Vector2D center = ball.getPosition();
double r = ball.getRadius();
// 线段方向向量
Vector2D seg = p2.sub(p1);
// 球心到线段起点的向量
Vector2D toCenter = center.sub(p1);
// 投影系数 t = (toCenter · seg) / (seg · seg)
double segLenSq = seg.dot(seg);
if (segLenSq == 0) return null; // p1==p2,退化为点
double t = Math.max(0, Math.min(1, toCenter.dot(seg) / segLenSq));
// 线段上离球心最近的点
Vector2D closest = p1.add(seg.mul(t));
Vector2D distVec = center.sub(closest);
double dist = distVec.length();
if (dist < r) {
// 计算法线方向(从最近点指向球心)
Vector2D normal = distVec.normalize();
// 穿透深度
double penetration = r - dist;
return new CollisionResult(normal, penetration, closest);
}
return null;
}
/**
* 检测球与圆的碰撞(圆形障碍物/缓冲器)
*/
public CollisionResult checkBallCircle(Ball ball, Vector2D center, double radius) {
Vector2D distVec = ball.getPosition().sub(center);
double dist = distVec.length();
double minDist = ball.getRadius() + radius;
if (dist < minDist) {
Vector2D normal = distVec.normalize();
double penetration = minDist - dist;
return new CollisionResult(normal, penetration, center.add(normal.mul(radius)));
}
return null;
}
/**
* 检测球与矩形AABB的碰撞
*/
public CollisionResult checkBallAABB(Ball ball, double minX, double minY,
double maxX, double maxY) {
Vector2D center = ball.getPosition();
double r = ball.getRadius();
// 矩形上离球心最近的点
double closestX = Math.max(minX, Math.min(center.x, maxX));
double closestY = Math.max(minY, Math.min(center.y, maxY));
Vector2D closest = new Vector2D(closestX, closestY);
Vector2D distVec = center.sub(closest);
double dist = distVec.length();
if (dist < r) {
Vector2D normal = distVec.normalize();
// 若球心在矩形内部,法线取反
if (center.x >= minX && center.x <= maxX &&
center.y >= minY && center.y <= maxY) {
normal = new Vector2D(-normal.x, -normal.y);
}
double penetration = r - dist;
return new CollisionResult(normal, penetration, closest);
}
return null;
}
static class CollisionResult {
Vector2D normal; // 碰撞法线(指向球外)
double penetration; // 穿透深度
Vector2D contactPoint; // 接触点
CollisionResult(Vector2D normal, double penetration, Vector2D contactPoint) {
this.normal = normal;
this.penetration = penetration;
this.contactPoint = contactPoint;
}
}
}
碰撞响应与反射
public class PhysicsEngine {
private CollisionDetector detector = new CollisionDetector();
/**
* 处理球与静态障碍物的碰撞(墙壁、挡板)
* 核心:沿碰撞法线反射速度向量,并修正位置消除穿透
*/
public void resolveStaticCollision(Ball ball, CollisionDetector.CollisionResult result) {
// 1. 位置修正:将球沿法线方向推出穿透深度
Vector2D correction = result.normal.mul(result.penetration);
ball.setPosition(ball.getPosition().add(correction));
// 2. 速度反射:v' = v - 2(v·n)n
ball.bounce(result.normal);
}
/**
* 处理挡板击球:给球施加额外速度增量
* @param flipperVelocity 挡板末端的线速度向量
*/
public void resolveFlipperHit(Ball ball, Vector2D flipperVelocity,
CollisionDetector.CollisionResult result) {
// 先执行标准反射
resolveStaticCollision(ball, result);
// 再叠加挡板动量(击球加速效果)
Vector2D newVel = ball.getVelocity().add(flipperVelocity.mul(0.3));
// 限制最大速度
if (newVel.length() > 15) {
newVel = newVel.normalize().mul(15);
}
ball.setVelocity(newVel);
}
/**
* 处理球与圆形缓冲器(bumper)的碰撞,给予额外加速
*/
public void resolveBumperHit(Ball ball, CollisionDetector.CollisionResult result) {
resolveStaticCollision(ball, result);
// 缓冲器额外加速20%
ball.setVelocity(ball.getVelocity().mul(1.2));
}
}
弹球台场景与挡板控制
import java.util.*;
public class PinballTable {
private double width = 400, height = 700; // 弹球台尺寸
private Ball ball;
private List<Obstacle> obstacles;
private Flipper leftFlipper, rightFlipper;
private PhysicsEngine physics = new PhysicsEngine();
private CollisionDetector detector = new CollisionDetector();
// 障碍物基类
static abstract class Obstacle {
abstract CollisionDetector.CollisionResult checkCollision(Ball ball);
abstract void onHit(Ball ball, PhysicsEngine physics,
CollisionDetector.CollisionResult result);
}
// 墙壁
static class Wall extends Obstacle {
Vector2D p1, p2;
Wall(Vector2D p1, Vector2D p2) { this.p1 = p1; this.p2 = p2; }
@Override
CollisionDetector.CollisionResult checkCollision(Ball ball) {
return new CollisionDetector().checkBallSegment(ball, p1, p2);
}
@Override
void onHit(Ball ball, PhysicsEngine physics,
CollisionDetector.CollisionResult result) {
physics.resolveStaticCollision(ball, result);
}
}
// 圆形缓冲器
static class Bumper extends Obstacle {
Vector2D center;
double radius;
Bumper(Vector2D c, double r) { this.center = c; this.radius = r; }
@Override
CollisionDetector.CollisionResult checkCollision(Ball ball) {
return new CollisionDetector().checkBallCircle(ball, center, radius);
}
@Override
void onHit(Ball ball, PhysicsEngine physics,
CollisionDetector.CollisionResult result) {
physics.resolveBumperHit(ball, result);
}
}
// 挡板
static class Flipper {
Vector2D pivot; // 旋转轴心
double length; // 挡板长度
double angle; // 当前角度(弧度)
double restAngle; // 静止角度
double activeAngle; // 击发角度
boolean isActive; // 是否正在击发
double angularVelocity; // 角速度
Flipper(Vector2D pivot, double length, double restAngle, double activeAngle) {
this.pivot = pivot;
this.length = length;
this.restAngle = restAngle;
this.activeAngle = activeAngle;
this.angle = restAngle;
this.isActive = false;
}
/** 激活挡板(按键触发) */
void activate() { isActive = true; }
void deactivate() { isActive = false; }
/** 更新挡板角度 */
void update(double dt) {
double target = isActive ? activeAngle : restAngle;
double diff = target - angle;
angle += diff * 10 * dt; // 弹性恢复到目标角度
}
/** 获取挡板末端坐标 */
Vector2D getTip() {
return new Vector2D(
pivot.x + length * Math.cos(angle),
pivot.y + length * Math.sin(angle)
);
}
/** 获取末端线速度(用于击球加速计算) */
Vector2D getTipVelocity() {
// v = ω × r,简化计算
double omega = isActive ? 15 : -10; // 击发/收回角速度
return new Vector2D(
-omega * length * Math.sin(angle),
omega * length * Math.cos(angle)
);
}
}
public PinballTable() {
// 初始化弹球
ball = new Ball(200, 600, 8, 3, -8);
// 初始化障碍物
obstacles = new ArrayList<>();
// 左侧墙
obstacles.add(new Wall(new Vector2D(20, 50), new Vector2D(20, 650)));
// 右侧墙
obstacles.add(new Wall(new Vector2D(380, 50), new Vector2D(380, 650)));
// 顶部墙
obstacles.add(new Wall(new Vector2D(20, 50), new Vector2D(380, 50)));
// 圆形缓冲器
obstacles.add(new Bumper(new Vector2D(200, 200), 25));
obstacles.add(new Bumper(new Vector2D(150, 300), 20));
obstacles.add(new Bumper(new Vector2D(250, 300), 20));
// 初始化挡板
leftFlipper = new Flipper(new Vector2D(80, 620), 80, 0.3, -0.5);
rightFlipper = new Flipper(new Vector2D(320, 620), 80, Math.PI - 0.3, Math.PI + 0.5);
}
/**
* 游戏主循环,每帧调用
*/
public void update(double dt) {
// 1. 应用重力
ball.applyGravity(500, dt); // 向下的重力加速度
// 2. 更新位置
ball.update(dt);
// 3. 更新挡板
leftFlipper.update(dt);
rightFlipper.update(dt);
// 4. 检测并处理所有碰撞
checkCollisions();
// 5. 检测是否出界(球掉到底部)
if (ball.getPosition().y > height) {
resetBall();
}
}
private void checkCollisions() {
// 与静态障碍物碰撞
for (Obstacle obs : obstacles) {
CollisionDetector.CollisionResult result = obs.checkCollision(ball);
if (result != null) {
obs.onHit(ball, physics, result);
}
}
// 与左挡板碰撞
Vector2D leftTip = leftFlipper.getTip();
CollisionDetector.CollisionResult leftHit =
detector.checkBallSegment(ball, leftFlipper.pivot, leftTip);
if (leftHit != null) {
physics.resolveFlipperHit(ball, leftFlipper.getTipVelocity(), leftHit);
}
// 与右挡板碰撞
Vector2D rightTip = rightFlipper.getTip();
CollisionDetector.CollisionResult rightHit =
detector.checkBallSegment(ball, rightFlipper.pivot, rightTip);
if (rightHit != null) {
physics.resolveFlipperHit(ball, rightFlipper.getTipVelocity(), rightHit);
}
}
private void resetBall() {
ball.setPosition(new Vector2D(200, 600));
ball.setVelocity(new Vector2D(3, -8));
}
public Ball getBall() { return ball; }
public void activateLeftFlipper() { leftFlipper.activate(); }
public void deactivateLeftFlipper() { leftFlipper.deactivate(); }
public void activateRightFlipper() { rightFlipper.activate(); }
public void deactivateRightFlipper() { rightFlipper.deactivate(); }
}
主程序入口
public class PinballGame {
public static void main(String[] args) {
PinballTable table = new PinballTable();
double dt = 1.0 / 60.0; // 60FPS
// 模拟120帧
for (int i = 0; i < 120; i++) {
// 模拟按键:第30-40帧按左挡板
if (i == 30) table.activateLeftFlipper();
if (i == 40) table.deactivateLeftFlipper();
table.update(dt);
Vector2D pos = table.getBall().getPosition();
Vector2D vel = table.getBall().getVelocity();
if (i % 20 == 0) {
System.out.printf("Frame %d: pos=%s, vel=%s%n", i, pos, vel);
}
}
}
}
复杂度分析
| 模块 | 时间复杂度 | 说明 |
|---|---|---|
| 球与线段碰撞 | O(1) | 向量投影计算,常数时间 |
| 球与圆碰撞 | O(1) | 距离比较 |
| 球与AABB碰撞 | O(1) | 坐标范围比较 |
| 每帧全碰撞检测 | O(n) | n为障碍物数量 |
| 向量反射计算 | O(1) | 点积与向量加减 |
总结
本文以弹球游戏为载体,深入讲解了向量反射公式、弹性碰撞响应以及多种几何体的碰撞检测。读者可以扩展:
– 实现更精确的物理积分(如Verlet积分或RK4)
– 加入倾斜台板(tilt)机制,通过重力分量控制球路
– 实现多球并发与球-球碰撞处理