每日算法 — 使用java实现弹弓:弹性势能与弹道轨迹优化

引言

弹弓是一种利用弹性形变储存势能并转化为动能的简易投射工具,其历史可追溯至史前时代。在弹弓游戏中,玩家拉伸橡皮筋将弹丸射向目标,核心挑战在于如何控制拉伸距离和发射角度,使弹丸精确命中目标。这一问题融合了胡克定律的弹性力学、抛物线运动学以及多变量函数优化。本文将用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实现了弹弓游戏的物理模拟与参数优化系统,核心要点包括:

  • 胡克定律描述弹性形变与势能的关系,建立初速度计算模型
  • 龙格-库塔四阶方法精确数值积分带空气阻力的弹道微分方程
  • 两阶段搜索策略先通过粗网格定位最优区域,再用坐标下降法精细调整拉伸距离和发射角度

弹弓将经典力学与数值优化有机结合,是理解物理模拟和多变量搜索的绝佳案例。通过扩展移动目标追踪和风偏修正,还能进一步探索工程实践中的弹道学应用。