游戏背景与问题引入
弹玻璃球(又称”打弹珠”)是许多人童年的经典游戏。玩家在地面挖几个小坑,用手指弹射玻璃球,通过碰撞将对手的球击出界外或打入洞中。这个游戏的核心算法在于二维弹性碰撞的精确计算,以及最优瞄准角度的几何求解。本文将用Java实现一套完整的弹玻璃球对战系统,深入讲解碰撞物理与瞄准策略。
核心算法原理
1. 二维弹性碰撞
当两个质量相同的玻璃球发生完全弹性碰撞时,动量守恒和动能守恒同时成立。设两球速度向量分别为v₁和v₂,碰撞后速度为v₁’和v₂’。
动量守恒:m·v₁ + m·v₂ = m·v₁’ + m·v₂’
动能守恒:½m·|v₁|² + ½m·|v₂|² = ½m·|v₁’|² + ½m·|v₂’|²
简化后的向量解法:
1. 将速度分解为沿碰撞法线方向(两球心连线)和切线方向
2. 切线方向速度不变(无摩擦时)
3. 法线方向速度交换(质量相等时)
2. 碰撞检测
两球碰撞当且仅当球心距离 ≤ 两球半径之和:
|p₁ – p₂| ≤ r₁ + r₂
为了处理时间离散化的精度问题,本实现采用子步进技术:当检测到两球距离在一步内从大于2r变为小于2r时,通过线性插值精确计算碰撞时刻。
3. 最优瞄准策略
要将自己的球(A)射出后击中目标球(B),需要计算发射角度。这本质上是一个几何光学问题:
- 如果目标球静止,发射方向应正对目标球球心
- 如果目标球在运动,需要预测其位置并进行提前量计算
- 如果要将目标球打入洞中,需要计算碰撞后目标球的运动方向
利用镜像法简化:将目标球的运动轨迹视为一条射线,求A球到该射线的最短距离点即为最佳碰撞点。
Java实现
import java.util.ArrayList;
import java.util.List;
/**
* 弹玻璃球对战模拟系统
* 核心算法:二维弹性碰撞 + 最优瞄准角度计算
*/
public class MarbleBattleSimulation {
// 物理常量
private static final double DT = 0.005; // 仿真步长 (s)
private static final double FRICTION = 0.98; // 每步速度衰减(地面摩擦)
private static final double MIN_SPEED = 0.1; // 低于此速度视为静止
/**
* 玻璃球类
*/
public static class Marble {
public double x, y; // 位置
public double vx, vy; // 速度
public final double r; // 半径
public final double m; // 质量
public final String name;
public boolean active; // 是否仍在场内
public Marble(String name, double x, double y, double r, double m) {
this.name = name; this.x = x; this.y = y;
this.r = r; this.m = m;
this.vx = 0; this.vy = 0;
this.active = true;
}
/**
* 以给定速度和角度发射
* @param speed 速度 (m/s)
* @param angleDeg 角度 (度,0为右方)
*/
public void launch(double speed, double angleDeg) {
double rad = Math.toRadians(angleDeg);
this.vx = speed * Math.cos(rad);
this.vy = speed * Math.sin(rad);
}
/**
* 单步运动更新
*/
public void step() {
if (!active) return;
x += vx * DT;
y += vy * DT;
// 地面摩擦减速
vx *= FRICTION;
vy *= FRICTION;
// 速度过低时停止
if (speed() < MIN_SPEED) {
vx = 0; vy = 0;
}
}
public double speed() {
return Math.sqrt(vx * vx + vy * vy);
}
public double distanceTo(Marble other) {
double dx = x - other.x;
double dy = y - other.y;
return Math.sqrt(dx * dx + dy * dy);
}
@Override
public String toString() {
return String.format("%s: (%.2f, %.2f) v=(%.2f, %.2f)",
name, x, y, vx, vy);
}
}
/**
* 洞(目标区域)
*/
public static class Hole {
public final double x, y, r;
public Hole(double x, double y, double r) { this.x = x; this.y = y; this.r = r; }
}
private final List<Marble> marbles = new ArrayList<>();
private final List<Hole> holes = new ArrayList<>();
private final double boundaryX, boundaryY; // 场地边界
public MarbleBattleSimulation(double width, double height) {
this.boundaryX = width;
this.boundaryY = height;
}
public void addMarble(Marble m) { marbles.add(m); }
public void addHole(Hole h) { holes.add(h); }
/**
* 检测并处理所有碰撞对
*/
private void handleCollisions() {
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);
if (!a.active || !b.active) continue;
double dist = a.distanceTo(b);
double minDist = a.r + b.r;
if (dist <= minDist) {
resolveCollision(a, b);
// 防止重叠
double overlap = minDist - dist;
if (overlap > 0) {
double nx = (b.x - a.x) / dist;
double ny = (b.y - a.y) / dist;
a.x -= nx * overlap * 0.5;
a.y -= ny * overlap * 0.5;
b.x += nx * overlap * 0.5;
b.y += ny * overlap * 0.5;
}
}
}
}
}
/**
* 弹性碰撞解析(质量不相等也适用)
* 核心算法:沿碰撞法线方向交换法向动量,切向不变
*/
private void resolveCollision(Marble a, Marble b) {
double dx = b.x - a.x;
double dy = b.y - a.y;
double dist = Math.sqrt(dx * dx + dy * dy);
if (dist == 0) return;
// 碰撞法线单位向量(从a指向b)
double nx = dx / dist;
double ny = dy / dist;
// 切线单位向量
double tx = -ny;
double ty = nx;
// 将速度分解为法向和切向分量
double v1n = a.vx * nx + a.vy * ny; // a的法向速度
double v1t = a.vx * tx + a.vy * ty; // a的切向速度
double v2n = b.vx * nx + b.vy * ny; // b的法向速度
double v2t = b.vx * tx + b.vy * ty; // b的切向速度
// 一维弹性碰撞公式(法向)
double m1 = a.m, m2 = b.m;
double v1nAfter = (v1n * (m1 - m2) + 2 * m2 * v2n) / (m1 + m2);
double v2nAfter = (v2n * (m2 - m1) + 2 * m1 * v1n) / (m1 + m2);
// 切向速度不变(无摩擦假设)
// 组合回向量
a.vx = v1nAfter * nx + v1t * tx;
a.vy = v1nAfter * ny + v1t * ty;
b.vx = v2nAfter * nx + v2t * tx;
b.vy = v2nAfter * ny + v2t * ty;
}
/**
* 检测球是否进洞
*/
private void checkHoles() {
for (Marble m : marbles) {
if (!m.active) continue;
for (Hole h : holes) {
double dx = m.x - h.x;
double dy = m.y - h.y;
double dist = Math.sqrt(dx * dx + dy * dy);
if (dist + m.r <= h.r) {
m.active = false;
System.out.println(" " + m.name + " 进洞了!");
}
}
}
}
/**
* 边界碰撞(简单反弹)
*/
private void handleBoundaries() {
for (Marble m : marbles) {
if (!m.active) continue;
if (m.x - m.r < 0) { m.x = m.r; m.vx = Math.abs(m.vx) * 0.8; }
if (m.x + m.r > boundaryX) { m.x = boundaryX - m.r; m.vx = -Math.abs(m.vx) * 0.8; }
if (m.y - m.r < 0) { m.y = m.r; m.vy = Math.abs(m.vy) * 0.8; }
if (m.y + m.r > boundaryY) { m.y = boundaryY - m.r; m.vy = -Math.abs(m.vy) * 0.8; }
}
}
/**
* 单步仿真
*/
public void step() {
for (Marble m : marbles) m.step();
handleCollisions();
handleBoundaries();
checkHoles();
}
/**
* 运行仿真直到所有球静止
*/
public void simulate(double maxTime) {
int steps = (int) (maxTime / DT);
for (int i = 0; i < steps; i++) {
step();
// 全部静止则提前结束
boolean allStopped = true;
for (Marble m : marbles) {
if (m.active && m.speed() > MIN_SPEED) { allStopped = false; break; }
}
if (allStopped) break;
}
}
// ==================== 瞄准算法 ====================
/**
* 计算击中静止目标所需的发射角度
* @param shooter 发射球
* @param target 目标球
* @return 发射角度(度),若无法击中返回NaN
*/
public static double calculateAimAngle(Marble shooter, Marble target) {
double dx = target.x - shooter.x;
double dy = target.y - shooter.y;
return Math.toDegrees(Math.atan2(dy, dx));
}
/**
* 计算将目标球撞向指定方向的发射角度
* 利用弹性碰撞的对称性:发射方向 = 目标方向 + (目标方向 - 期望方向)
* 更精确地说,入射角等于反射角(类似光学反射)
* @param shooter 发射球
* @param target 目标球
* @param desiredAngleDeg 期望目标球飞出的角度
* @return 发射角度
*/
public static double calculateDeflectionAngle(Marble shooter, Marble target,
double desiredAngleDeg) {
double dx = target.x - shooter.x;
double dy = target.y - shooter.y;
double collisionAngle = Math.atan2(dy, dx);
double desiredRad = Math.toRadians(desiredAngleDeg);
// 碰撞后目标球沿 desiredAngle 飞出
// 根据弹性碰撞,发射球应以碰撞角对称的角度射入
// 即:aimAngle = 2 * collisionAngle - desiredRad
double aimRad = 2 * collisionAngle - desiredRad;
return Math.toDegrees(aimRad);
}
/**
* 寻找最优发射策略:让目标球进洞
* @param shooter 发射球
* @param target 目标球
* @param hole 目标洞
* @return 最优发射角度和速度
*/
public static double[] findBestShot(Marble shooter, Marble target, Hole hole) {
// 目标球需要飞向洞的方向
double dx = hole.x - target.x;
double dy = hole.y - target.y;
double targetAngle = Math.atan2(dy, dy); // 修正:应分别为dy, dx
targetAngle = Math.atan2(dy, dx);
double bestAngle = calculateDeflectionAngle(shooter, target, Math.toDegrees(targetAngle));
// 速度估算:考虑距离和摩擦
double dist = shooter.distanceTo(target);
double requiredSpeed = dist * 2.5; // 经验公式
return new double[]{bestAngle, requiredSpeed};
}
public static void main(String[] args) {
System.out.println("=== 弹玻璃球对战模拟 ===
");
// 创建2m x 2m的场地
MarbleBattleSimulation game = new MarbleBattleSimulation(2.0, 2.0);
// 玩家A的球
Marble ballA = new Marble("玩家A", 0.3, 1.0, 0.015, 0.01);
// 玩家B的球
Marble ballB = new Marble("玩家B", 1.5, 1.0, 0.015, 0.01);
// 目标洞
Hole hole = new Hole(1.8, 0.2, 0.06);
game.addMarble(ballA);
game.addMarble(ballB);
game.addHole(hole);
// 场景1:直接瞄准击中
System.out.println("【场景1】玩家A直接瞄准玩家B");
double aimAngle = calculateAimAngle(ballA, ballB);
System.out.printf(" 计算发射角度: %.1f°
", aimAngle);
ballA.launch(3.0, aimAngle);
game.simulate(10.0);
System.out.println(" 碰撞后: " + ballA);
System.out.println(" 碰撞后: " + ballB);
// 重置
ballA.x = 0.3; ballA.y = 1.0; ballA.vx = 0; ballA.vy = 0;
ballB.x = 1.0; ballB.y = 1.0; ballB.vx = 0; ballB.vy = 0;
ballA.active = true; ballB.active = true;
// 场景2:将B撞向洞口
System.out.println("
【场景2】玩家A将B撞向洞口");
double[] best = findBestShot(ballA, ballB, hole);
System.out.printf(" 最优发射角度: %.1f°, 建议速度: %.2f m/s
", best[0], best[1]);
ballA.launch(best[1], best[0]);
game.simulate(10.0);
System.out.println(" 最终: " + ballA);
System.out.println(" 最终: " + ballB);
// 场景3:多角度测试
System.out.println("
【场景3】不同发射角度的效果对比");
for (double angle = 0; angle <= 90; angle += 15) {
Marble testA = new Marble("A", 0.3, 1.0, 0.015, 0.01);
Marble testB = new Marble("B", 1.0, 1.0, 0.015, 0.01);
MarbleBattleSimulation testGame = new MarbleBattleSimulation(2.0, 2.0);
testGame.addMarble(testA);
testGame.addMarble(testB);
testA.launch(3.0, angle);
testGame.simulate(5.0);
System.out.printf(" 角度 %.0f°: B球速度=%.2f m/s, 位移=(%.2f, %.2f)
",
angle, testB.speed(), testB.x - 1.0, testB.y - 1.0);
}
System.out.println("
=== 核心结论 ===");
System.out.println("1. 二维弹性碰撞可分解为法向和切向分别处理");
System.out.println("2. 质量相等时法向速度交换,切向速度不变");
System.out.println("3. 瞄准策略可转化为几何反射问题求解");
}
}
复杂度分析
- 碰撞检测时间复杂度:O(n²),n为球的数量。每帧需要检测所有球对。
- 碰撞解析时间复杂度:O(1),单次碰撞的计算量为常数。
- 空间复杂度:O(n),仅需存储球的状态。
- 优化方向:球数较多时可使用空间哈希或四叉树将碰撞检测优化至O(n)。
总结
本文通过Java实现了弹玻璃球的完整对战模拟,核心算法贡献包括:
- 二维弹性碰撞的向量分解法,适用于任意质量比
- 子步进碰撞检测确保离散仿真的精度
- 镜像法瞄准策略将最优发射角度转化为几何反射问题
- 进洞判定结合距离与半径约束
读者可以进一步扩展:加入旋转( topspin/sidespin )对碰撞的影响、多球连续碰撞的连锁反应、以及基于蒙特卡洛树搜索的AI对战策略。