弹珠游戏(Pinball/Marble Game)是街机厅的经典代表,其核心魅力在于逼真的物理碰撞和不可预测的弹道轨迹。本文将使用Java实现一个弹珠物理引擎,重点讲解二维向量运算、圆形碰撞检测以及弹性碰撞后的速度分解与反弹计算。
物理模型
弹珠游戏中的核心物理交互包括:
- 运动学更新:弹珠受重力加速,位置每帧更新
- 边界碰撞:弹珠与挡板、墙壁的碰撞检测
- 圆形碰撞:弹珠与圆形障碍物(缓冲器)的碰撞
- 弹性反弹:碰撞后的速度向量分解与反射
向量运算基础
所有物理计算基于二维向量:
位置: p = (x, y)
速度: v = (vx, vy)
加速度: a = (ax, ay) = (0, g) // 仅重力
核心类实现
二维向量类
/**
* 二维向量类
* 封装所有向量运算,是物理引擎的数学基础
*/
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 multiply(double scalar) {
return new Vector2D(this.x * scalar, this.y * scalar);
}
/**
* 点积
* a·b = |a||b|cos(θ),用于计算投影和夹角
*/
public double dot(Vector2D other) {
return this.x * other.x + this.y * other.y;
}
/**
* 向量长度(模)
*/
public double magnitude() {
return Math.sqrt(x * x + y * y);
}
/**
* 向量长度的平方(避免开方运算,用于比较)
*/
public double magnitudeSquared() {
return x * x + y * y;
}
/**
* 单位向量(方向不变,长度为1)
*/
public Vector2D normalize() {
double mag = magnitude();
if (mag < 1e-10) return new Vector2D(0, 0);
return new Vector2D(x / mag, y / mag);
}
/**
* 向量在另一向量上的投影
* proj_b(a) = (a·b / |b|^2) * b
*/
public Vector2D projectOnto(Vector2D other) {
double dot = this.dot(other);
double otherMagSq = other.magnitudeSquared();
if (otherMagSq < 1e-10) return new Vector2D(0, 0);
return other.multiply(dot / otherMagSq);
}
/**
* 获取垂直向量(顺时针旋转90度)
*/
public Vector2D perpendicular() {
return new Vector2D(-y, x);
}
/**
* 向量反射
* r = d - 2(d·n)n,其中d为入射方向,n为法线单位向量
*/
public Vector2D reflect(Vector2D normal) {
Vector2D n = normal.normalize();
double dot = this.dot(n);
return this.subtract(n.multiply(2 * dot));
}
@Override
public String toString() {
return String.format("(%.3f, %.3f)", x, y);
}
}
弹珠实体
/**
* 弹珠实体
* 包含位置、速度、半径和质量
*/
public class Marble {
private Vector2D position;
private Vector2D velocity;
private final double radius;
private final double mass;
private final double restitution; // 弹性系数 (0-1)
private final double friction; // 摩擦系数
public Marble(double x, double y, double radius) {
this.position = new Vector2D(x, y);
this.velocity = new Vector2D(0, 0);
this.radius = radius;
this.mass = radius * radius; // 质量与面积成正比
this.restitution = 0.85; // 保留85%速度
this.friction = 0.995; // 每帧速度衰减
}
/**
* 物理更新
* @param gravity 重力加速度向量
* @param dt 时间步长
*/
public void update(Vector2D gravity, double dt) {
// 应用重力
velocity = velocity.add(gravity.multiply(dt));
// 应用摩擦
velocity = velocity.multiply(friction);
// 更新位置: p = p + v * dt
position = position.add(velocity.multiply(dt));
}
/**
* 应用冲量改变速度
*/
public void applyImpulse(Vector2D impulse) {
// v = v + J / m
velocity = velocity.add(impulse.multiply(1.0 / mass));
}
// Getters and setters
public Vector2D getPosition() { return position; }
public Vector2D getVelocity() { return velocity; }
public double getRadius() { return radius; }
public double getMass() { return mass; }
public double getRestitution() { return restitution; }
public void setPosition(Vector2D pos) { this.position = pos; }
public void setVelocity(Vector2D vel) { this.velocity = vel; }
@Override
public String toString() {
return String.format("Marble(pos=%s, vel=%s, r=%.1f)", position, velocity, radius);
}
}
碰撞检测系统
圆形与圆形碰撞
/**
* 碰撞检测工具类
*/
public class CollisionDetector {
/**
* 检测两个弹珠是否碰撞
* 原理:两球心距离 < 半径之和
*
* @return 碰撞信息,若未碰撞返回null
*/
public static CollisionResult checkCircleCollision(Marble a, Marble b) {
Vector2D diff = b.getPosition().subtract(a.getPosition());
double distSq = diff.magnitudeSquared();
double radiusSum = a.getRadius() + b.getRadius();
if (distSq >= radiusSum * radiusSum) {
return null; // 未碰撞
}
double dist = Math.sqrt(distSq);
Vector2D normal = dist > 1e-10 ? diff.multiply(1.0 / dist) : new Vector2D(1, 0);
double penetration = radiusSum - dist; // 穿透深度
return new CollisionResult(normal, penetration);
}
/**
* 检测弹珠与线段的碰撞
* 用于弹珠与挡板/墙壁的碰撞
*
* @param marble 弹珠
* @param lineStart 线段起点
* @param lineEnd 线段终点
* @return 碰撞信息
*/
public static CollisionResult checkLineCollision(Marble marble,
Vector2D lineStart,
Vector2D lineEnd) {
Vector2D lineVec = lineEnd.subtract(lineStart);
Vector2D toCircle = marble.getPosition().subtract(lineStart);
// 计算圆心在线段上的投影点
double lineLenSq = lineVec.magnitudeSquared();
if (lineLenSq < 1e-10) {
// 线段退化为点
double dist = toCircle.magnitude();
if (dist < marble.getRadius()) {
Vector2D normal = toCircle.normalize();
return new CollisionResult(normal, marble.getRadius() - dist);
}
return null;
}
// 投影参数 t = ((C - A) · (B - A)) / |B - A|^2
double t = toCircle.dot(lineVec) / lineLenSq;
t = Math.max(0, Math.min(1, t)); // 限制在线段范围内
// 最近点 = A + t * (B - A)
Vector2D closest = lineStart.add(lineVec.multiply(t));
Vector2D diff = marble.getPosition().subtract(closest);
double distSq = diff.magnitudeSquared();
if (distSq >= marble.getRadius() * marble.getRadius()) {
return null;
}
double dist = Math.sqrt(distSq);
Vector2D normal = dist > 1e-10 ? diff.multiply(1.0 / dist) : lineVec.perpendicular().normalize();
double penetration = marble.getRadius() - dist;
return new CollisionResult(normal, penetration);
}
/**
* 碰撞结果
*/
public static class CollisionResult {
public final Vector2D normal; // 碰撞法线(指向弹珠外部)
public final double penetration; // 穿透深度
public CollisionResult(Vector2D normal, double penetration) {
this.normal = normal;
this.penetration = penetration;
}
}
}
碰撞响应与反弹
弹性碰撞求解
/**
* 碰撞响应处理器
* 处理碰撞后的速度更新和位置修正
*/
public class CollisionResolver {
/**
* 处理两个弹珠之间的弹性碰撞
* 基于动量守恒和能量守恒(考虑弹性系数)
*
* 一维弹性碰撞公式:
* v1' = (m1-m2)/(m1+m2)*v1 + 2*m2/(m1+m2)*v2
* v2' = 2*m1/(m1+m2)*v1 + (m2-m1)/(m1+m2)*v2
*
* 二维情况:将速度分解为法向和切向分量,仅法向分量交换
*/
public static void resolveMarbleCollision(Marble a, Marble b,
CollisionDetector.CollisionResult collision) {
Vector2D normal = collision.normal;
// 1. 位置修正(防止重叠)
double totalMass = a.getMass() + b.getMass();
Vector2D correction = normal.multiply(collision.penetration);
a.setPosition(a.getPosition().subtract(correction.multiply(b.getMass() / totalMass)));
b.setPosition(b.getPosition().add(correction.multiply(a.getMass() / totalMass)));
// 2. 相对速度
Vector2D relativeVel = b.getVelocity().subtract(a.getVelocity());
double velAlongNormal = relativeVel.dot(normal);
// 如果物体正在分离,不处理
if (velAlongNormal > 0) return;
// 3. 计算冲量标量
double e = Math.min(a.getRestitution(), b.getRestitution());
double j = -(1 + e) * velAlongNormal;
j /= (1.0 / a.getMass() + 1.0 / b.getMass());
// 4. 应用冲量
Vector2D impulse = normal.multiply(j);
a.applyImpulse(impulse.multiply(-1));
b.applyImpulse(impulse);
}
/**
* 处理弹珠与静态边界的碰撞
* 静态物体质量视为无穷大,仅弹珠速度改变
*/
public static void resolveBoundaryCollision(Marble marble,
CollisionDetector.CollisionResult collision) {
// 1. 位置修正
Vector2D correction = collision.normal.multiply(collision.penetration);
marble.setPosition(marble.getPosition().add(correction));
// 2. 速度反射
Vector2D reflected = marble.getVelocity().reflect(collision.normal);
marble.setVelocity(reflected.multiply(marble.getRestitution()));
}
/**
* 处理弹珠与圆形缓冲器( bumper )的碰撞
* 缓冲器通常带有额外的"推力"效果
*/
public static void resolveBumperCollision(Marble marble,
Vector2D bumperCenter,
double bumperForce,
CollisionDetector.CollisionResult collision) {
// 先进行标准边界碰撞处理
resolveBoundaryCollision(marble, collision);
// 额外施加缓冲器推力(沿法线方向加速)
Vector2D extraForce = collision.normal.multiply(bumperForce / marble.getMass());
marble.setVelocity(marble.getVelocity().add(extraForce));
}
}
弹珠台场景
import java.util.*;
/**
* 弹珠台游戏场景
* 管理所有物理实体和碰撞检测
*/
public class PinballTable {
// 物理常量
private static final Vector2D GRAVITY = new Vector2D(0, 300.0); // 向下重力
private static final double TABLE_WIDTH = 400;
private static final double TABLE_HEIGHT = 600;
private final List<Marble> marbles;
private final List<Wall> walls;
private final List<Bumper> bumpers;
public PinballTable() {
this.marbles = new ArrayList<>();
this.walls = new ArrayList<>();
this.bumpers = new ArrayList<>();
setupDefaultTable();
}
/**
* 设置默认弹珠台布局
*/
private void setupDefaultTable() {
// 边界墙壁
walls.add(new Wall(new Vector2D(0, 0), new Vector2D(TABLE_WIDTH, 0))); // 上
walls.add(new Wall(new Vector2D(0, TABLE_HEIGHT), new Vector2D(TABLE_WIDTH, TABLE_HEIGHT))); // 下
walls.add(new Wall(new Vector2D(0, 0), new Vector2D(0, TABLE_HEIGHT))); // 左
walls.add(new Wall(new Vector2D(TABLE_WIDTH, 0), new Vector2D(TABLE_WIDTH, TABLE_HEIGHT))); // 右
// 三角形导向坡
walls.add(new Wall(new Vector2D(0, 400), new Vector2D(80, 500)));
walls.add(new Wall(new Vector2D(TABLE_WIDTH, 400), new Vector2D(TABLE_WIDTH - 80, 500)));
// 圆形缓冲器
bumpers.add(new Bumper(new Vector2D(150, 200), 25, 150));
bumpers.add(new Bumper(new Vector2D(250, 200), 25, 150));
bumpers.add(new Bumper(new Vector2D(200, 300), 20, 200));
}
/**
* 发射新弹珠
*/
public Marble launchMarble(double launchX) {
Marble marble = new Marble(launchX, 50, 8);
marble.setVelocity(new Vector2D(0, 100)); // 初始向下速度
marbles.add(marble);
return marble;
}
/**
* 物理帧更新
* @param dt 时间步长(秒)
*/
public void update(double dt) {
// 1. 更新所有弹珠位置
for (Marble marble : marbles) {
marble.update(GRAVITY, dt);
}
// 2. 弹珠间碰撞
for (int i = 0; i < marbles.size(); i++) {
for (int j = i + 1; j < marbles.size(); j++) {
Marble a = marbles.get(i);
Marble b = marbles.get(j);
CollisionDetector.CollisionResult collision =
CollisionDetector.checkCircleCollision(a, b);
if (collision != null) {
CollisionResolver.resolveMarbleCollision(a, b, collision);
}
}
}
// 3. 弹珠与墙壁碰撞
for (Marble marble : marbles) {
for (Wall wall : walls) {
CollisionDetector.CollisionResult collision =
CollisionDetector.checkLineCollision(marble, wall.start, wall.end);
if (collision != null) {
CollisionResolver.resolveBoundaryCollision(marble, collision);
}
}
}
// 4. 弹珠与缓冲器碰撞
for (Marble marble : marbles) {
for (Bumper bumper : bumpers) {
// 缓冲器视为静态圆形
double dist = marble.getPosition().subtract(bumper.center).magnitude();
if (dist < marble.getRadius() + bumper.radius) {
Vector2D normal = marble.getPosition().subtract(bumper.center).normalize();
double penetration = marble.getRadius() + bumper.radius - dist;
CollisionDetector.CollisionResult collision =
new CollisionDetector.CollisionResult(normal, penetration);
CollisionResolver.resolveBumperCollision(
marble, bumper.center, bumper.force, collision
);
}
}
}
// 5. 移除落出底边的弹珠
marbles.removeIf(m -> m.getPosition().y > TABLE_HEIGHT + 20);
}
public List<Marble> getMarbles() { return marbles; }
public List<Wall> getWalls() { return walls; }
public List<Bumper> getBumpers() { return bumpers; }
/**
* 墙壁线段
*/
public static class Wall {
public final Vector2D start;
public final Vector2D end;
public Wall(Vector2D start, Vector2D end) {
this.start = start;
this.end = end;
}
}
/**
* 圆形缓冲器
*/
public static class Bumper {
public final Vector2D center;
public final double radius;
public final double force; // 弹力加成
public Bumper(Vector2D center, double radius, double force) {
this.center = center;
this.radius = radius;
this.force = force;
}
}
}
控制台模拟演示
/**
* 弹珠物理引擎控制台演示
*/
public class PinballDemo {
public static void main(String[] args) {
PinballTable table = new PinballTable();
// 发射3个弹珠
Marble m1 = table.launchMarble(100);
m1.setVelocity(new Vector2D(50, 80));
Marble m2 = table.launchMarble(200);
m2.setVelocity(new Vector2D(-30, 120));
Marble m3 = table.launchMarble(300);
m3.setVelocity(new Vector2D(0, 60));
System.out.println("===== 弹珠物理引擎模拟 =====\n");
System.out.println("弹珠台: 400x600 | 重力: 300 | 弹性: 0.85 | 摩擦: 0.995\n");
double dt = 0.016; // 约60FPS
int steps = 600; // 模拟10秒
int printInterval = 20;
for (int i = 0; i < steps; i++) {
table.update(dt);
if (i % printInterval == 0) {
double time = i * dt;
System.out.printf("t=%4.2fs | 弹珠数: %d%n", time, table.getMarbles().size());
for (int j = 0; j < table.getMarbles().size(); j++) {
Marble m = table.getMarbles().get(j);
System.out.printf(" [%d] pos=(%6.1f, %6.1f) vel=(%6.1f, %6.1f) speed=%5.1f%n",
j + 1, m.getPosition().x, m.getPosition().y,
m.getVelocity().x, m.getVelocity().y,
m.getVelocity().magnitude());
}
System.out.println();
}
if (table.getMarbles().isEmpty()) {
System.out.println("所有弹珠已落出,模拟结束");
break;
}
}
System.out.println("模拟完成");
}
}
复杂度分析
| 操作 | 时间复杂度 | 空间复杂度 | 说明 |
|---|---|---|---|
| 运动更新 | O(m) | O(1) | m为弹珠数 |
| 弹珠间碰撞 | O(m^2) | O(1) | 两两检测 |
| 边界碰撞 | O(m * w) | O(1) | w为墙壁数 |
| 缓冲器碰撞 | O(m * b) | O(1) | b为缓冲器数 |
| 碰撞响应 | O(1) | O(1) | 每对碰撞常量计算 |
扩展方向
- 空间分区优化:使用四叉树或均匀网格减少碰撞检测复杂度从O(n^2)到O(n)
- 挡板交互:添加可控制的底部挡板,根据按键时机改变弹珠反弹角度
- 旋转物体:处理弹珠与旋转圆形物体的碰撞(角动量守恒)
- 多边形碰撞:扩展至任意多边形障碍物的碰撞检测(Separating Axis Theorem)
总结
本文从向量运算出发,构建了完整的弹珠物理引擎,涵盖圆形碰撞检测、弹性碰撞响应和场景管理。通过将物理问题转化为向量计算,读者可以直观地理解碰撞背后的数学原理,并将这些技术应用于更复杂的游戏物理系统开发中。