每日算法 — 使用java实现纸飞机:抛体运动模拟与最优投掷角度计算

引言

纸飞机是每个人童年都玩过的经典游戏。看似简单的一掷,背后却蕴含着丰富的物理学原理:重力、空气阻力、升力与俯仰力矩共同决定了纸飞机的飞行轨迹。本文将用 Java 构建一个简化的纸飞机飞行物理模型,通过数值积分模拟飞行轨迹,并计算给定条件下的 最优投掷角度 —— 让纸飞机飞得最远、留空时间最长或飞行高度最高。

物理模型建立

对纸飞机进行受力分析,主要考虑以下作用力:

  1. 重力:$F_g = mg$,方向竖直向下
  2. 升力(Lift):$F_L = \frac{1}{2} \rho v^2 S C_L$,垂直于飞行方向向上
  3. 阻力(Drag):$F_D = \frac{1}{2} \rho v^2 S C_D$,与飞行方向相反

其中:
– $\rho$ 为空气密度(约 1.225 kg/m³)
– $v$ 为飞行速度
– $S$ 为机翼参考面积
– $C_L$ 为升力系数(与攻角相关,典型值 0.1~0.5)
– $C_D$ 为阻力系数(典型值 0.01~0.1)

简化假设:
– 纸飞机质量集中于重心
– 忽略侧滑与偏航,仅考虑纵向平面运动
– 升力系数与攻角呈线性关系:$C_L = C_{L0} + C_{L\alpha} \cdot \alpha$
– 俯仰力矩由重心位置自动稳定(静态稳定)

运动方程

将力分解到水平和竖直方向:

$$F_x = -F_D \cos\theta – F_L \sin\theta$$
$$F_y = F_L \cos\theta – F_D \sin\theta – mg$$

其中 $\theta$ 为飞行方向与水平面的夹角(俯仰角)。

通过牛顿第二定律得到加速度,再使用 龙格-库塔法(RK4) 进行数值积分。

/**
 * 纸飞机飞行物理模拟器
 * 基于简化空气动力学模型,使用RK4数值积分
 */
public class PaperAirplaneSimulator {

    // 物理常数
    static final double RHO = 1.225;        // 空气密度 kg/m³
    static final double G = 9.81;           // 重力加速度 m/s²

    // 纸飞机参数(以标准A4纸折成的飞机为参考)
    double mass;           // 质量 kg
    double wingArea;       // 机翼面积 m²
    double wingSpan;       // 翼展 m
    double cl0;            // 零攻角升力系数
    double clAlpha;        // 升力线斜率 /rad
    double cd0;            // 零升阻力系数
    double cdAlpha;        // 攻角阻力增量

    public PaperAirplaneSimulator() {
        // 默认参数:A4纸飞机
        this.mass = 0.005;           // 5克
        this.wingArea = 0.015;       // 150 cm²
        this.wingSpan = 0.20;        // 20 cm
        this.cl0 = 0.15;
        this.clAlpha = 2.0;          // 每弧度
        this.cd0 = 0.02;
        this.cdAlpha = 0.5;
    }

    /**
     * 状态向量 [x, y, vx, vy]
     * x: 水平位移, y: 高度, vx: 水平速度, vy: 竖直速度
     */
    static class State {
        double x, y, vx, vy;
        State(double x, double y, double vx, double vy) {
            this.x = x; this.y = y; this.vx = vx; this.vy = vy;
        }
        State copy() {
            return new State(x, y, vx, vy);
        }
    }

    /**
     * 计算空气动力学系数
     * @param alpha 攻角 (rad)
     * @return [CL, CD]
     */
    double[] computeAeroCoefficients(double alpha) {
        double cl = cl0 + clAlpha * alpha;
        double cd = cd0 + cdAlpha * alpha * alpha;
        // 限制合理范围
        cl = Math.max(-1.0, Math.min(1.5, cl));
        cd = Math.max(0.01, Math.min(2.0, cd));
        return new double[]{cl, cd};
    }

    /**
     * 计算状态导数 d(state)/dt
     */
    State derivative(State s) {
        double v = Math.sqrt(s.vx * s.vx + s.vy * s.vy);
        if (v < 0.01) v = 0.01;  // 避免除零

        double theta = Math.atan2(s.vy, s.vx);  // 飞行方向角
        double alpha = 0.05;  // 简化:固定小攻角(静态稳定飞机自调节)

        double[] coeff = computeAeroCoefficients(alpha);
        double cl = coeff[0];
        double cd = coeff[1];

        double dynamicPressure = 0.5 * RHO * v * v * wingArea;
        double lift = dynamicPressure * cl;
        double drag = dynamicPressure * cd;

        // 分解到水平和竖直方向
        double fx = -drag * Math.cos(theta) - lift * Math.sin(theta);
        double fy = lift * Math.cos(theta) - drag * Math.sin(theta) - mass * G;

        return new State(s.vx, s.vy, fx / mass, fy / mass);
    }

    /**
     * RK4 单步积分
     * @param s 当前状态
     * @param dt 时间步长
     * @return 下一步状态
     */
    State rk4Step(State s, double dt) {
        State k1 = derivative(s);
        State k2 = derivative(add(s, multiply(k1, dt * 0.5)));
        State k3 = derivative(add(s, multiply(k2, dt * 0.5)));
        State k4 = derivative(add(s, multiply(k3, dt)));

        State result = s.copy();
        result.x += dt * (k1.x + 2*k2.x + 2*k3.x + k4.x) / 6.0;
        result.y += dt * (k1.y + 2*k2.y + 2*k3.y + k4.y) / 6.0;
        result.vx += dt * (k1.vx + 2*k2.vx + 2*k3.vx + k4.vx) / 6.0;
        result.vy += dt * (k1.vy + 2*k2.vy + 2*k3.vy + k4.vy) / 6.0;
        return result;
    }

    State add(State a, State b) {
        return new State(a.x + b.x, a.y + b.y, a.vx + b.vx, a.vy + b.vy);
    }

    State multiply(State s, double scalar) {
        return new State(s.x * scalar, s.y * scalar, s.vx * scalar, s.vy * scalar);
    }

    /**
     * 模拟单次飞行
     * @param v0 初始速度 m/s
     * @param angleDeg 投掷角度(度)
     * @param dt 时间步长
     * @return 飞行轨迹点列表
     */
    public FlightResult simulate(double v0, double angleDeg, double dt) {
        double angleRad = Math.toRadians(angleDeg);
        State state = new State(0, 1.5,  // 从1.5米高度投掷
                                v0 * Math.cos(angleRad),
                                v0 * Math.sin(angleRad));

        List<double[]> trajectory = new ArrayList<>();
        trajectory.add(new double[]{state.x, state.y});

        double maxHeight = state.y;
        double flightTime = 0;

        while (state.y >= 0 && flightTime < 30) {  // 最多模拟30秒
            state = rk4Step(state, dt);
            flightTime += dt;
            trajectory.add(new double[]{state.x, state.y});
            if (state.y > maxHeight) maxHeight = state.y;
        }

        // 线性插值找到精确落地点
        int last = trajectory.size() - 1;
        double[] p1 = trajectory.get(last - 1);
        double[] p2 = trajectory.get(last);
        double exactDistance = p1[0] + (p2[0] - p1[0]) * p1[1] / (p1[1] - p2[1]);

        return new FlightResult(exactDistance, maxHeight, flightTime, trajectory);
    }

    static class FlightResult {
        double distance;      // 水平飞行距离 m
        double maxHeight;     // 最大高度 m
        double flightTime;    // 留空时间 s
        List<double[]> trajectory;

        FlightResult(double d, double h, double t, List<double[]> traj) {
            this.distance = d;
            this.maxHeight = h;
            this.flightTime = t;
            this.trajectory = traj;
        }

        @Override
        public String toString() {
            return String.format("距离=%.2fm, 最大高度=%.2fm, 留空时间=%.2fs",
                                 distance, maxHeight, flightTime);
        }
    }

    // ========== 最优角度搜索 ==========

    /**
     * 三分搜索寻找最优投掷角度(最大化飞行距离)
     * 假设飞行距离关于角度是单峰函数(在合理范围内)
     */
    public double findOptimalAngle(double v0, double target) {
        double left = 0, right = 60;  // 搜索范围 [0°, 60°]
        double eps = 0.01;

        while (right - left > eps) {
            double m1 = left + (right - left) / 3.0;
            double m2 = right - (right - left) / 3.0;

            double d1 = simulate(v0, m1, 0.01).distance;
            double d2 = simulate(v0, m2, 0.01).distance;

            if (d1 < d2) {
                left = m1;
            } else {
                right = m2;
            }
        }
        return (left + right) / 2.0;
    }

    /**
     * 穷举搜索不同初始速度下的最优角度
     */
    public void analyzeOptimalAngles() {
        System.out.println("===== 不同初始速度下的最优投掷角度 =====");
        System.out.println("速度(m/s) | 最优角度 | 飞行距离 | 最大高度 | 留空时间");
        System.out.println("----------|----------|----------|----------|----------");

        for (double v0 = 3.0; v0 <= 12.0; v0 += 1.0) {
            double optimalAngle = findOptimalAngle(v0, 0);
            FlightResult result = simulate(v0, optimalAngle, 0.01);
            System.out.printf("  %.1f     |  %.1f°   |  %.2fm  |  %.2fm  |  %.2fs\n",
                              v0, optimalAngle, result.distance,
                              result.maxHeight, result.flightTime);
        }
    }

    // ========== 可视化轨迹输出 ==========

    public void printTrajectory(FlightResult result, int samples) {
        List<double[]> traj = result.trajectory;
        int step = Math.max(1, traj.size() / samples);
        System.out.println("\n飞行轨迹采样 (x, y):");
        for (int i = 0; i < traj.size(); i += step) {
            double[] p = traj.get(i);
            System.out.printf("  (%.2f, %.2f)\n", p[0], p[1]);
        }
    }

    public static void main(String[] args) {
        PaperAirplaneSimulator sim = new PaperAirplaneSimulator();

        // 示例1: 单次飞行模拟
        System.out.println("===== 纸飞机飞行模拟 =====\n");
        double v0 = 6.0;      // 初始速度 6 m/s
        double angle = 25.0;  // 投掷角度 25°

        System.out.println("参数: 初始速度=" + v0 + "m/s, 投掷角度=" + angle + "°");
        FlightResult result = sim.simulate(v0, angle, 0.005);
        System.out.println("结果: " + result);
        sim.printTrajectory(result, 10);

        // 示例2: 寻找最优角度
        System.out.println("\n===== 最优投掷角度搜索 =====");
        double optimal = sim.findOptimalAngle(v0, 0);
        System.out.println("初始速度 " + v0 + " m/s 时,最优投掷角度: " +
                           String.format("%.2f°", optimal));
        FlightResult optimalResult = sim.simulate(v0, optimal, 0.005);
        System.out.println("最优结果: " + optimalResult);

        // 示例3: 不同速度下的分析
        System.out.println();
        sim.analyzeOptimalAngles();

        // 示例4: 角度对比
        System.out.println("\n===== 不同投掷角度对比 (v0=6m/s) =====");
        System.out.println("角度 | 飞行距离 | 最大高度 | 留空时间");
        System.out.println("-----|----------|----------|----------");
        for (double a = 0; a <= 60; a += 5) {
            FlightResult r = sim.simulate(6.0, a, 0.01);
            System.out.printf(" %2.0f° |  %6.2fm |  %6.2fm |  %6.2fs\n",
                              a, r.distance, r.maxHeight, r.flightTime);
        }
    }
}

无阻力理想模型对比

在真空中,抛体运动的最优角度为 45°。但在空气中,由于升力和阻力的存在,最优角度通常更小(约 25°~35°),这与我们的模拟结果一致。

    /**
     * 理想抛体运动(无空气阻力)计算
     * 用于与真实模型对比
     */
    public static double idealRange(double v0, double angleDeg, double h0) {
        double theta = Math.toRadians(angleDeg);
        double vx = v0 * Math.cos(theta);
        double vy = v0 * Math.sin(theta);
        // 解方程: h0 + vy*t - 0.5*g*t^2 = 0
        double discriminant = vy * vy + 2 * G * h0;
        double t = (vy + Math.sqrt(discriminant)) / G;
        return vx * t;
    }

模型参数影响分析

参数 增大影响 实际调整方式
质量 惯性增大,轨迹更平直 使用更重的纸或加配重
翼展 升力面积增大,滑翔比提高 折出更宽的机翼
CL(升力系数) 留空时间增加,距离可能减少 调整机翼弯度(dihedral)
CD(阻力系数) 减速更快,距离减少 使飞机更流线型
投掷速度 距离和高度均增加 更用力投掷

总结

通过 Java 实现的纸飞机物理模拟,我们展示了如何将空气动力学简化为可计算的模型。数值积分(RK4)让我们能精确追踪非线性运动,而最优角度搜索则揭示了空气阻力对经典 45° 结论的修正。这一项目不仅是物理与编程的有趣结合,也是数值方法与优化算法在实际问题中的生动应用。