弹球游戏是物理模拟与几何算法的经典结合。球体在边界和障碍物之间反弹,需要实时计算碰撞检测、反射方向与能量衰减。本文用Java实现一个完整的2D弹球物理引擎,涵盖圆形碰撞检测、法线反射向量计算和能量衰减系统。
算法核心思路
1. 圆形与线段碰撞检测
将边界抽象为线段。球心到线段的最短距离若小于球半径,则发生碰撞。通过向量投影计算垂足位置,判断垂足是否落在线段范围内。
2. 反射向量计算
碰撞后速度向量的反射遵循入射角等于反射角定律。公式为:v’ = v – 2(v·n)n,其中n是碰撞点处的单位法向量。该公式来源于向量在法线方向上的分量取反。
3. 能量衰减模拟
真实碰撞存在能量损失。引入弹性系数e(0≤e≤1),碰撞后速度大小乘以e。e=1为完全弹性碰撞,e<1时球体逐渐减速停止。
4. 球与球碰撞
两球碰撞时,先检测球心距离是否小于半径之和。碰撞法线方向为两球心连线的单位向量,速度按动量守恒和能量守恒分解到法线方向和切线方向后分别处理。
Java实现
import java.util.ArrayList;
import java.util.List;
public class BallPhysicsEngine {
// 画布尺寸
private final double width;
private final double height;
// 重力加速度
private final double gravity = 0.15;
// 边界弹性系数
private final double wallRestitution = 0.85;
static class Ball {
double x, y; // 位置
double vx, vy; // 速度
double radius; // 半径
double mass; // 质量
double restitution; // 弹性系数
Ball(double x, double y, double vx, double vy, double r, double m, double e) {
this.x = x; this.y = y; this.vx = vx; this.vy = vy;
this.radius = r; this.mass = m; this.restitution = e;
}
}
static class LineSegment {
double x1, y1, x2, y2;
LineSegment(double x1, double y1, double x2, double y2) {
this.x1 = x1; this.y1 = y1; this.x2 = x2; this.y2 = y2;
}
}
private List<Ball> balls = new ArrayList<>();
private List<LineSegment> walls = new ArrayList<>();
public BallPhysicsEngine(double w, double h) {
this.width = w; this.height = h;
// 初始化四周边界
walls.add(new LineSegment(0, 0, w, 0)); // 上
walls.add(new LineSegment(w, 0, w, h)); // 右
walls.add(new LineSegment(w, h, 0, h)); // 下
walls.add(new LineSegment(0, h, 0, 0)); // 左
}
public void addBall(Ball b) { balls.add(b); }
public void addWall(LineSegment w) { walls.add(w); }
/**
* 计算点(px,py)到线段的最短距离及垂足参数t
* 返回值[距离, 垂足x, 垂足y, t]
*/
private double[] pointToSegment(double px, double py, LineSegment seg) {
double dx = seg.x2 - seg.x1;
double dy = seg.y2 - seg.y1;
double lenSq = dx * dx + dy * dy;
double t = lenSq == 0 ? 0 : Math.max(0, Math.min(1, ((px - seg.x1) * dx + (py - seg.y1) * dy) / lenSq));
double projX = seg.x1 + t * dx;
double projY = seg.y1 + t * dy;
double dist = Math.hypot(px - projX, py - projY);
return new double[]{dist, projX, projY, t};
}
/**
* 计算反射后的速度向量
* v: 原速度向量 (vx, vy)
* nx, ny: 单位法向量(指向球外侧)
* e: 弹性系数
*/
private double[] reflect(double vx, double vy, double nx, double ny, double e) {
double dot = vx * nx + vy * ny;
double rx = vx - 2 * dot * nx;
double ry = vy - 2 * dot * ny;
return new double[]{rx * e, ry * e};
}
/**
* 更新一帧物理状态
*/
public void update() {
// 1. 应用重力并更新位置
for (Ball b : balls) {
b.vy += gravity;
b.x += b.vx;
b.y += b.vy;
}
// 2. 球与边界/墙壁碰撞检测与响应
for (Ball b : balls) {
for (LineSegment wall : walls) {
double[] res = pointToSegment(b.x, b.y, wall);
double dist = res[0];
if (dist < b.radius) {
// 计算法向量(从线段指向球心)
double nx = (b.x - res[1]) / dist;
double ny = (b.y - res[2]) / dist;
// 将球推出墙壁
double overlap = b.radius - dist;
b.x += nx * overlap;
b.y += ny * overlap;
// 反射速度
double[] rv = reflect(b.vx, b.vy, nx, ny, b.restitution * wallRestitution);
b.vx = rv[0];
b.vy = rv[1];
}
}
}
// 3. 球与球碰撞
for (int i = 0; i < balls.size(); i++) {
for (int j = i + 1; j < balls.size(); j++) {
Ball a = balls.get(i);
Ball c = balls.get(j);
double dx = c.x - a.x;
double dy = c.y - a.y;
double dist = Math.hypot(dx, dy);
double minDist = a.radius + c.radius;
if (dist < minDist && dist > 0) {
// 法线方向(从a指向c)
double nx = dx / dist;
double ny = dy / dist;
// 推出重叠
double overlap = (minDist - dist) / 2;
a.x -= nx * overlap; a.y -= ny * overlap;
c.x += nx * overlap; c.y += ny * overlap;
// 相对速度在法线方向的分量
double dvx = a.vx - c.vx;
double dvy = a.vy - c.vy;
double velAlongNormal = dvx * nx + dvy * ny;
if (velAlongNormal > 0) continue; // 正在分离
double e = Math.min(a.restitution, c.restitution);
double impulse = -(1 + e) * velAlongNormal / (1 / a.mass + 1 / c.mass);
double ix = impulse * nx;
double iy = impulse * ny;
a.vx += ix / a.mass; a.vy += iy / a.mass;
c.vx -= ix / c.mass; c.vy -= iy / c.mass;
}
}
}
}
public void printState() {
for (int i = 0; i < balls.size(); i++) {
Ball b = balls.get(i);
System.out.printf("Ball%d: pos=(%.2f, %.2f) vel=(%.2f, %.2f)%n", i, b.x, b.y, b.vx, b.vy);
}
}
public static void main(String[] args) {
BallPhysicsEngine engine = new BallPhysicsEngine(800, 600);
engine.addBall(new Ball(400, 100, 3, 0, 20, 1.0, 0.9));
engine.addBall(new Ball(500, 300, -2, 1, 15, 0.8, 0.85));
System.out.println("=== 弹球物理模拟 ===");
for (int frame = 0; frame < 20; frame++) {
System.out.println("Frame " + frame + ":");
engine.printState();
engine.update();
}
}
}
复杂度分析
- 时间复杂度:每帧更新为O(B² + B·W),B为球数量,W为墙壁线段数。球与球碰撞需要双重循环,球与墙碰撞为单重循环。
- 空间复杂度:O(B + W),仅需存储球和墙壁的状态信息。
扩展方向
- 空间分区优化:使用四叉树或均匀网格加速碰撞检测,将O(B²)降为O(B·logB)或O(B)。
- 旋转体碰撞:扩展为带角速度的刚体,引入转动惯量和冲量矩计算。
- 连续碰撞检测(CCD):高速运动时避免穿透,计算TOI(Time of Impact)。