每日算法 — 使用java实现射箭游戏:抛物线轨迹预测与风速补偿算法

引言

射箭游戏的核心挑战在于:给定目标距离、高度差与风速,计算最佳发射角度与力度。这本质上是一个物理抛物线问题,加入空气阻力与横风后演变为数值求解问题。本文用Java实现完整的射箭物理引擎与自动瞄准算法。

核心算法设计

1. 抛物线运动学模型

在无风理想条件下,箭的轨迹是标准抛物线。但考虑重力、空气阻力和风速后,需要使用数值积分方法模拟轨迹。

import java.util.*;

/**
 * 二维向量类,用于表示位置、速度、力
 */
class Vector2D {
    double x, y;
    public Vector2D(double x, double y) { this.x = x; this.y = y; }
    public Vector2D add(Vector2D v) { return new Vector2D(x + v.x, y + v.y); }
    public Vector2D mul(double s) { return new Vector2D(x * s, y * s); }
    public double magnitude() { return Math.sqrt(x * x + y * y); }
    public Vector2D normalize() {
        double m = magnitude();
        if (m == 0) return new Vector2D(0, 0);
        return new Vector2D(x / m, y / m);
    }
    @Override
    public String toString() { return String.format("(%.2f, %.2f)", x, y); }
}

/**
 * 箭矢物理对象
 */
class Arrow {
    Vector2D position;   // 位置
    Vector2D velocity;   // 速度
    double mass;         // 质量(kg)
    double dragCoeff;    // 阻力系数
    double crossArea;    // 截面积(m²)

    public Arrow(Vector2D pos, Vector2D vel, double mass, double dragCoeff, double crossArea) {
        this.position = pos;
        this.velocity = vel;
        this.mass = mass;
        this.dragCoeff = dragCoeff;
        this.crossArea = crossArea;
    }
}

/**
 * 环境参数
 */
class Environment {
    double gravity = 9.8;      // 重力加速度(m/s²)
    double airDensity = 1.225; // 空气密度(kg/m³)
    Vector2D wind;             // 风速向量(m/s)

    public Environment(Vector2D wind) {
        this.wind = wind;
    }
}

/**
 * 物理引擎:使用欧拉法数值积分模拟箭矢轨迹
 */
class PhysicsEngine {
    double timeStep = 0.001; // 积分步长(s)

    /**
     * 计算当前时刻箭矢受到的合力
     * 包含:重力、空气阻力、风力
     */
    public Vector2D computeForce(Arrow arrow, Environment env) {
        // 重力:向下
        Vector2D gravity = new Vector2D(0, -env.gravity * arrow.mass);

        // 空气阻力:F = 0.5 * ρ * v² * Cd * A,方向与速度相反
        Vector2D relVelocity = arrow.velocity.add(env.wind.mul(-1));
        double v = relVelocity.magnitude();
        double dragMagnitude = 0.5 * env.airDensity * v * v * arrow.dragCoeff * arrow.crossArea;
        Vector2D drag = relVelocity.normalize().mul(-dragMagnitude);

        return gravity.add(drag);
    }

    /**
     * 模拟箭矢飞行轨迹,直到落地(y<=0)
     * @return 轨迹点列表与最终落点
     */
    public List<Vector2D> simulate(Arrow arrow, Environment env) {
        List<Vector2D> trajectory = new ArrayList<>();
        trajectory.add(new Vector2D(arrow.position.x, arrow.position.y));

        while (arrow.position.y >= 0) {
            Vector2D force = computeForce(arrow, env);
            Vector2D acceleration = force.mul(1.0 / arrow.mass);

            // 欧拉积分更新速度与位置
            arrow.velocity = arrow.velocity.add(acceleration.mul(timeStep));
            arrow.position = arrow.position.add(arrow.velocity.mul(timeStep));

            trajectory.add(new Vector2D(arrow.position.x, arrow.position.y));

            // 安全退出:飞行超过5秒或超出合理范围
            if (trajectory.size() > 5000 || Math.abs(arrow.position.x) > 1000) break;
        }
        return trajectory;
    }
}

2. 自动瞄准算法

给定目标位置,需要反推出最佳发射角度。由于空气阻力的存在,无法使用解析解,我们采用数值方法:先忽略阻力求解析初值,再用牛顿迭代法精修。

/**
 * 自动瞄准系统
 */
class AutoAimSystem {
    PhysicsEngine physics = new PhysicsEngine();
    double initialSpeed = 50.0; // 初速度(m/s)

    /**
     * 计算击中目标所需的最佳发射角度
     * @param targetX 目标水平距离(m)
     * @param targetY 目标高度(m)
     * @param env 环境参数
     * @return 推荐发射角度(弧度)
     */
    public double calculateAimAngle(double targetX, double targetY, Environment env) {
        // 步骤1:用理想抛物线公式求初始估计
        double angleGuess = idealAngleEstimate(targetX, targetY, initialSpeed, env.gravity);

        // 步骤2:牛顿迭代法精修,考虑空气阻力与风速
        double angle = newtonRefine(targetX, targetY, angleGuess, env);
        return angle;
    }

    /**
     * 理想抛物线角度估计(无阻力解析解)
     * 由公式:y = x*tan(θ) - g*x²/(2*v²*cos²(θ))
     * 转化为关于tan(θ)的二次方程求解
     */
    private double idealAngleEstimate(double x, double y, double v, double g) {
        double discriminant = v * v * v * v - g * (g * x * x + 2 * y * v * v);
        if (discriminant < 0) return Math.PI / 4; // 无法到达,返回45度

        double tanTheta1 = (v * v + Math.sqrt(discriminant)) / (g * x);
        double tanTheta2 = (v * v - Math.sqrt(discriminant)) / (g * x);

        // 选择较小的角度(更平坦的轨迹,受风影响小)
        double angle1 = Math.atan(tanTheta1);
        double angle2 = Math.atan(tanTheta2);
        return Math.min(angle1, angle2);
    }

    /**
     * 牛顿迭代法精修角度
     * 目标:使落点与目标位置的误差趋于零
     */
    private double newtonRefine(double targetX, double targetY, double guess, Environment env) {
        double angle = guess;
        double delta = 0.001; // 数值微分步长

        for (int iter = 0; iter < 20; iter++) {
            Vector2D impact = simulateImpact(angle, env);
            double errorX = impact.x - targetX;
            double errorY = impact.y - targetY;
            double error = Math.sqrt(errorX * errorX + errorY * errorY);

            if (error < 0.01) break; // 收敛

            // 数值微分计算误差对角度的导数
            Vector2D impactPlus = simulateImpact(angle + delta, env);
            double dError = (Math.sqrt(
                (impactPlus.x - targetX) * (impactPlus.x - targetX) +
                (impactPlus.y - targetY) * (impactPlus.y - targetY)
            ) - error) / delta;

            if (Math.abs(dError) < 1e-10) break;
            angle = angle - error / dError; // 牛顿迭代步
        }
        return angle;
    }

    /**
     * 模拟给定角度下的落点
     */
    private Vector2D simulateImpact(double angle, Environment env) {
        Vector2D startPos = new Vector2D(0, 1.5); // 假设射手身高1.5m
        Vector2D startVel = new Vector2D(
            initialSpeed * Math.cos(angle),
            initialSpeed * Math.sin(angle)
        );
        Arrow arrow = new Arrow(startPos, startVel, 0.02, 0.3, 0.0003);
        List<Vector2D> traj = physics.simulate(arrow, env);
        return traj.get(traj.size() - 1);
    }
}

3. 风速补偿策略

横风会导致箭矢偏离目标。我们通过预瞄偏移量来补偿。

/**
 * 风速补偿计算器
 */
class WindCompensator {
    PhysicsEngine physics = new PhysicsEngine();
    double initialSpeed = 50.0;

    /**
     * 计算给定风速下的瞄准偏移量
     * @param targetX 目标距离
     * @param targetY 目标高度
     * @param wind 风速向量
     * @return 水平方向瞄准偏移量(m)
     */
    public double calculateWindOffset(double targetX, double targetY, Vector2D wind) {
        // 对比无风与有风两种情况的落点差异
        Environment noWind = new Environment(new Vector2D(0, 0));
        Environment withWind = new Environment(wind);

        AutoAimSystem aim = new AutoAimSystem();
        double angleNoWind = aim.calculateAimAngle(targetX, targetY, noWind);
        double angleWithWind = aim.calculateAimAngle(targetX, targetY, withWind);

        // 计算两种角度下无风轨迹的落点差异
        Vector2D impactNoWind = simulateWithAngle(angleNoWind, noWind);
        Vector2D impactWithWindAim = simulateWithAngle(angleWithWind, noWind);

        return impactWithWindAim.x - impactNoWind.x;
    }

    private Vector2D simulateWithAngle(double angle, Environment env) {
        Vector2D startPos = new Vector2D(0, 1.5);
        Vector2D startVel = new Vector2D(
            initialSpeed * Math.cos(angle),
            initialSpeed * Math.sin(angle)
        );
        Arrow arrow = new Arrow(startPos, startVel, 0.02, 0.3, 0.0003);
        List<Vector2D> traj = physics.simulate(arrow, env);
        return traj.get(traj.size() - 1);
    }
}

演示程序

public class ArcheryGameDemo {
    public static void main(String[] args) {
        // 场景:目标在80米外,高度1.2m,有3m/s的右侧横风
        double targetX = 80.0;
        double targetY = 1.2;
        Vector2D wind = new Vector2D(3.0, 0); // 3m/s向右
        Environment env = new Environment(wind);

        AutoAimSystem aim = new AutoAimSystem();
        double angle = aim.calculateAimAngle(targetX, targetY, env);
        double degrees = Math.toDegrees(angle);

        System.out.printf("目标距离: %.1fm, 高度: %.1fm\n", targetX, targetY);
        System.out.printf("风速: %s\n", wind);
        System.out.printf("推荐发射角度: %.2f度\n", degrees);

        WindCompensator compensator = new WindCompensator();
        double offset = compensator.calculateWindOffset(targetX, targetY, wind);
        System.out.printf("风速补偿偏移: %.2fm\n", offset);

        // 验证:模拟射击
        PhysicsEngine physics = new PhysicsEngine();
        Vector2D startPos = new Vector2D(0, 1.5);
        Vector2D startVel = new Vector2D(
            50.0 * Math.cos(angle),
            50.0 * Math.sin(angle)
        );
        Arrow arrow = new Arrow(startPos, startVel, 0.02, 0.3, 0.0003);
        List<Vector2D> traj = physics.simulate(arrow, env);
        Vector2D impact = traj.get(traj.size() - 1);
        System.out.printf("模拟落点: %s\n", impact);
        System.out.printf("落点误差: %.3fm\n",
            Math.sqrt((impact.x - targetX) * (impact.x - targetX) +
                     (impact.y - targetY) * (impact.y - targetY)));
    }
}

复杂度分析

  • 轨迹模拟:O(T/Δt),T为飞行时间,Δt为积分步长
  • 角度迭代:O(k × T/Δt),k为牛顿迭代次数(通常<20)
  • 整体精度:可通过减小步长提高,但计算量增加

总结

本文从运动学建模、数值积分、非线性方程求解三个层面构建射箭AI。核心思路是将物理问题转化为数值计算:用欧拉法积分轨迹,用牛顿迭代法反解最优角度。读者可进一步扩展三维场景、移动目标预测与多箭连发策略。