引言
弹弓是一种利用弹性形变储存势能并转化为动能的简易投射工具,其历史可追溯至史前时代。在弹弓游戏中,玩家拉伸橡皮筋将弹丸射向目标,核心挑战在于如何控制拉伸距离和发射角度,使弹丸精确命中目标。这一问题融合了胡克定律的弹性力学、抛物线运动学以及多变量函数优化。本文将用Java实现弹弓游戏的物理模拟系统,讲解弹性势能计算与弹道轨迹优化算法,帮助读者在趣味场景中理解力学建模与数值搜索的结合。
算法原理
弹性势能与初速度
根据胡克定律,橡皮筋的弹力与形变量成正比:$F = k \cdot \Delta x$,其中 $k$ 为弹性系数,$\Delta x$ 为拉伸距离。拉伸过程中储存的弹性势能为:
$$E_p = \frac{1}{2} k (\Delta x)^2$$
释放时,势能转化为弹丸的动能:$E_k = \frac{1}{2} m v_0^2$。假设能量转化效率为 $\eta$(考虑摩擦和热损耗),则弹丸初速度为:
$$v_0 = \sqrt{\frac{\eta \cdot k \cdot (\Delta x)^2}{m}}$$
带空气阻力的弹道模型
实际飞行中,空气阻力不可忽略。阻力大小与速度平方成正比:$F_d = \frac{1}{2} \rho C_d A v^2$,其中 $\rho$ 为空气密度,$C_d$ 为阻力系数,$A$ 为弹丸截面积。
运动微分方程:
– $\frac{dv_x}{dt} = -\frac{F_d}{m} \cdot \frac{v_x}{v}$
– $\frac{dv_y}{dt} = -g – \frac{F_d}{m} \cdot \frac{v_y}{v}$
由于解析解复杂,采用龙格-库塔四阶方法(RK4)进行数值积分求解轨迹。
多变量优化
需要同时优化拉伸距离 $\Delta x$ 和发射角度 $\theta$。采用网格搜索 + 局部精细搜索的两阶段策略:
1. 在参数空间进行粗粒度网格搜索,定位最优解大致区域
2. 在最优区域附近进行细粒度搜索(可采用坐标下降法或梯度下降法)
Java实现
1. 弹弓物理引擎
/**
* 弹弓物理引擎:模拟弹性形变与弹道飞行
*/
public class SlingshotPhysics {
// 物理常量
private static final double G = 9.8; // 重力加速度
private static final double RHO = 1.225; // 空气密度 (kg/m^3)
private static final double DT = 0.001; // 数值积分步长 (s)
// 弹弓参数
private final double k; // 弹性系数 (N/m)
private final double maxStretch; // 最大拉伸距离 (m)
private final double efficiency; // 能量转化效率
private final double projectileMass; // 弹丸质量 (kg)
private final double projectileRadius; // 弹丸半径 (m)
private final double dragCoeff; // 阻力系数
public SlingshotPhysics(double k, double maxStretch, double efficiency,
double projectileMass, double projectileRadius, double dragCoeff) {
this.k = k;
this.maxStretch = maxStretch;
this.efficiency = efficiency;
this.projectileMass = projectileMass;
this.projectileRadius = projectileRadius;
this.dragCoeff = dragCoeff;
}
/**
* 计算给定拉伸距离下的弹丸初速度
*/
public double calculateVelocity(double stretch) {
double energy = 0.5 * k * stretch * stretch * efficiency;
return Math.sqrt(2.0 * energy / projectileMass);
}
/**
* 计算空气阻力产生的加速度
*/
private double[] dragAcceleration(double vx, double vy) {
double v = Math.hypot(vx, vy);
if (v < 1e-6) return new double[]{0, 0};
double area = Math.PI * projectileRadius * projectileRadius;
double dragForce = 0.5 * RHO * dragCoeff * area * v * v;
double ax = -(dragForce / projectileMass) * (vx / v);
double ay = -(dragForce / projectileMass) * (vy / v);
return new double[]{ax, ay};
}
/**
* 使用RK4方法模拟弹道轨迹
* @param stretch 拉伸距离 (m)
* @param angle 发射角度(弧度)
* @param targetX 目标水平距离 (m)
* @return 轨迹点列表 [[x,y], ...] 和落点信息
*/
public TrajectoryResult simulate(double stretch, double angle, double targetX) {
double v0 = calculateVelocity(stretch);
double vx = v0 * Math.cos(angle);
double vy = v0 * Math.sin(angle);
double x = 0, y = 0; // 从原点发射
java.util.List<double[]> points = new java.util.ArrayList<>();
points.add(new double[]{x, y});
// RK4积分
while (y >= 0 || vy > 0) {
// k1
double[] drag1 = dragAcceleration(vx, vy);
double k1vx = drag1[0];
double k1vy = drag1[1] - G;
// k2
double[] drag2 = dragAcceleration(vx + k1vx * DT / 2, vy + k1vy * DT / 2);
double k2vx = drag2[0];
double k2vy = drag2[1] - G;
// k3
double[] drag3 = dragAcceleration(vx + k2vx * DT / 2, vy + k2vy * DT / 2);
double k3vx = drag3[0];
double k3vy = drag3[1] - G;
// k4
double[] drag4 = dragAcceleration(vx + k3vx * DT, vy + k3vy * DT);
double k4vx = drag4[0];
double k4vy = drag4[1] - G;
vx += (k1vx + 2 * k2vx + 2 * k3vx + k4vx) * DT / 6.0;
vy += (k1vy + 2 * k2vy + 2 * k3vy + k4vy) * DT / 6.0;
x += vx * DT;
y += vy * DT;
if (points.size() % 100 == 0) {
points.add(new double[]{x, y});
}
// 安全退出条件
if (x > targetX * 3 || points.size() > 100000) break;
}
// 精确计算落点(线性插值到y=0)
double lastX = points.get(points.size() - 1)[0];
double lastY = points.get(points.size() - 1)[1];
double prevX = points.size() >= 2 ? points.get(points.size() - 2)[0] : 0;
double prevY = points.size() >= 2 ? points.get(points.size() - 2)[1] : 0;
double landingX = lastX;
if (lastY < 0 && prevY > 0) {
landingX = prevX + (lastX - prevX) * (0 - prevY) / (lastY - prevY);
}
// 计算目标处的弹道高度
double heightAtTarget = interpolateHeight(points, targetX);
return new TrajectoryResult(points, landingX, heightAtTarget, v0);
}
/**
* 在轨迹点中插值计算指定x处的高度
*/
private double interpolateHeight(java.util.List<double[]> points, double targetX) {
for (int i = 0; i < points.size() - 1; i++) {
double x1 = points.get(i)[0];
double x2 = points.get(i + 1)[0];
if (x1 <= targetX && targetX <= x2) {
double y1 = points.get(i)[1];
double y2 = points.get(i + 1)[1];
return y1 + (y2 - y1) * (targetX - x1) / (x2 - x1);
}
}
return -1; // 目标在射程之外
}
/** 轨迹结果封装 */
public static class TrajectoryResult {
public final java.util.List<double[]> points;
public final double landingX;
public final double heightAtTarget;
public final double initialVelocity;
public TrajectoryResult(java.util.List<double[]> points, double landingX,
double heightAtTarget, double initialVelocity) {
this.points = points;
this.landingX = landingX;
this.heightAtTarget = heightAtTarget;
this.initialVelocity = initialVelocity;
}
}
}
2. 最优参数搜索器
/**
* 弹弓参数优化器:搜索最佳拉伸距离和发射角度
*/
public class SlingshotOptimizer {
private final SlingshotPhysics physics;
private final double targetX; // 目标水平距离
private final double targetY; // 目标高度
private final double tolerance; // 命中容差半径
public SlingshotOptimizer(SlingshotPhysics physics, double targetX,
double targetY, double tolerance) {
this.physics = physics;
this.targetX = targetX;
this.targetY = targetY;
this.tolerance = tolerance;
}
/**
* 评估一次射击的质量:距离目标点的欧几里得偏差
*/
public double evaluateShot(double stretch, double angle) {
SlingshotPhysics.TrajectoryResult result = physics.simulate(stretch, angle, targetX);
if (result.heightAtTarget < 0) return Double.MAX_VALUE;
double dx = 0; // x方向假设恰好到达targetX
double dy = result.heightAtTarget - targetY;
return Math.sqrt(dx * dx + dy * dy);
}
/**
* 两阶段搜索:粗网格 + 细搜索
*/
public OptimalResult optimize() {
// 第一阶段:粗粒度网格搜索
double bestStretch = 0, bestAngle = 0, bestError = Double.MAX_VALUE;
double maxStretch = physics.maxStretch;
for (double s = 0.1; s <= maxStretch; s += 0.05) {
for (double a = Math.toRadians(10); a <= Math.toRadians(80); a += Math.toRadians(2)) {
double error = evaluateShot(s, a);
if (error < bestError) {
bestError = error;
bestStretch = s;
bestAngle = a;
}
}
}
System.out.printf("粗搜索最优: 拉伸=%.3fm, 角度=%.2f°, 偏差=%.4fm%n",
bestStretch, Math.toDegrees(bestAngle), bestError);
// 第二阶段:在最优解附近精细搜索(坐标下降法)
double stepS = 0.01, stepA = Math.toRadians(0.5);
for (int iter = 0; iter < 50; iter++) {
boolean improved = false;
// 沿stretch方向搜索
for (double delta : new double[]{-stepS, 0, stepS}) {
double ns = bestStretch + delta;
if (ns < 0.01 || ns > maxStretch) continue;
double err = evaluateShot(ns, bestAngle);
if (err < bestError) {
bestError = err;
bestStretch = ns;
improved = true;
}
}
// 沿angle方向搜索
for (double delta : new double[]{-stepA, 0, stepA}) {
double na = bestAngle + delta;
if (na < Math.toRadians(5) || na > Math.toRadians(85)) continue;
double err = evaluateShot(bestStretch, na);
if (err < bestError) {
bestError = err;
bestAngle = na;
improved = true;
}
}
if (!improved) {
stepS *= 0.5;
stepA *= 0.5;
if (stepS < 1e-4) break;
}
}
boolean canHit = bestError <= tolerance;
return new OptimalResult(bestStretch, bestAngle, bestError, canHit);
}
/** 最优结果封装 */
public static class OptimalResult {
public final double stretch;
public final double angle;
public final double error;
public final boolean canHit;
public OptimalResult(double stretch, double angle, double error, boolean canHit) {
this.stretch = stretch;
this.angle = angle;
this.error = error;
this.canHit = canHit;
}
}
}
3. 主程序演示
/**
* 弹弓游戏主程序
*/
public class SlingshotGameDemo {
public static void main(String[] args) {
// 弹弓物理参数
double k = 150.0; // 弹性系数 150 N/m
double maxStretch = 0.4; // 最大拉伸 0.4m
double efficiency = 0.75; // 能量转化效率 75%
double mass = 0.01; // 弹丸质量 10g
double radius = 0.008; // 弹丸半径 8mm
double dragCoeff = 0.47; // 球形阻力系数
SlingshotPhysics physics = new SlingshotPhysics(k, maxStretch, efficiency,
mass, radius, dragCoeff);
// 目标参数
double targetX = 15.0; // 目标距离 15m
double targetY = 1.5; // 目标高度 1.5m
double tolerance = 0.3; // 命中容差 30cm
System.out.println("=== 弹弓物理模拟 ===");
System.out.printf("弹弓参数: k=%.1fN/m, 最大拉伸=%.2fm, 效率=%.0f%%%n",
k, maxStretch, efficiency * 100);
System.out.printf("弹丸参数: 质量=%.3fkg, 半径=%.3fm%n", mass, radius);
System.out.printf("目标位置: (%.1fm, %.1fm), 容差=%.2fm%n%n", targetX, targetY, tolerance);
// 最优参数搜索
SlingshotOptimizer optimizer = new SlingshotOptimizer(physics, targetX, targetY, tolerance);
SlingshotOptimizer.OptimalResult optimal = optimizer.optimize();
System.out.printf("\n=== 最优射击参数 ===%n");
System.out.printf("最佳拉伸距离: %.4fm (%.1f%% of max)%n",
optimal.stretch, optimal.stretch / maxStretch * 100);
System.out.printf("最佳发射角度: %.4f°%n", Math.toDegrees(optimal.angle));
System.out.printf("预期偏差: %.4fm%n", optimal.error);
System.out.printf("能否命中: %s%n", optimal.canHit ? "是" : "否");
// 展示最优轨迹
SlingshotPhysics.TrajectoryResult traj = physics.simulate(optimal.stretch, optimal.angle, targetX);
System.out.printf("\n弹丸初速度: %.2f m/s%n", traj.initialVelocity);
System.out.printf("实际落点距离: %.2f m%n", traj.landingX);
System.out.printf("目标处弹道高度: %.3f m%n", traj.heightAtTarget);
// 不同拉伸距离下的表现
System.out.println("\n=== 不同拉伸距离下的弹道高度(固定角度45°)===");
System.out.printf("%-12s %-12s %-12s %-12s%n", "拉伸距离", "初速度", "目标高度", "落点距离");
System.out.println("-".repeat(50));
double fixedAngle = Math.toRadians(45);
for (double s = 0.1; s <= maxStretch; s += 0.05) {
SlingshotPhysics.TrajectoryResult r = physics.simulate(s, fixedAngle, targetX);
System.out.printf("%-12.2f %-12.2f %-12.3f %-12.2f%n",
s, physics.calculateVelocity(s), r.heightAtTarget, r.landingX);
}
}
}
复杂度分析
| 操作 | 时间复杂度 | 空间复杂度 | 说明 |
|---|---|---|---|
| 初速度计算 | $O(1)$ | $O(1)$ | 直接公式 |
| 单步RK4积分 | $O(1)$ | $O(1)$ | 固定计算量 |
| 轨迹模拟 | $O(T/\Delta t)$ | $O(T/\Delta t)$ | $T$ 为飞行时间,$\Delta t$ 为步长 |
| 粗网格搜索 | $O(n_s \cdot n_a \cdot T/\Delta t)$ | $O(T/\Delta t)$ | $n_s$, $n_a$ 为网格点数 |
| 细搜索(坐标下降) | $O(k \cdot T/\Delta t)$ | $O(T/\Delta t)$ | $k$ 为迭代次数 |
扩展思考
- 移动目标追踪:若目标在运动,需要预测目标未来位置。引入提前量计算,根据弹丸飞行时间和目标速度预测拦截点。
- 多段橡皮筋:实际弹弓使用Y形结构,两侧橡皮筋的合力方向随拉伸变化。可建立更精确的二维弹性力模型。
- 风偏修正:加入横向风速影响,通过调整发射角度或拉伸距离进行风偏补偿。
总结
本文通过Java实现了弹弓游戏的物理模拟与参数优化系统,核心要点包括:
- 胡克定律描述弹性形变与势能的关系,建立初速度计算模型
- 龙格-库塔四阶方法精确数值积分带空气阻力的弹道微分方程
- 两阶段搜索策略先通过粗网格定位最优区域,再用坐标下降法精细调整拉伸距离和发射角度
弹弓将经典力学与数值优化有机结合,是理解物理模拟和多变量搜索的绝佳案例。通过扩展移动目标追踪和风偏修正,还能进一步探索工程实践中的弹道学应用。