每日算法 — 使用java实现套圈游戏:抛体运动模拟与最优角度决策

套圈是公园和集市中常见的经典游戏,玩家将圆环抛出,套中目标立柱即可得分。看似简单的投掷动作,实际上涉及复杂的物理计算与最优决策问题。本文用Java实现套圈游戏的物理引擎,并探讨如何通过数值方法求解最优投掷角度与力度。

一、物理模型建立

1.1 圆环飞行轨迹

圆环被抛出后,其质心沿抛物线轨迹运动。与实心球体不同,圆环在空气中会受到额外的空气阻力与转动惯量影响。为简化模型,本文将圆环质心运动视为标准抛体运动,落地判定则考虑圆环的半径尺寸。

质心运动方程:
– x(t) = x0 + v0 * cos(θ) * t
– y(t) = y0 + v0 * sin(θ) * t - 0.5 * g * t²

其中 v0 为初速度,θ 为投掷角度,g = 9.8 m/s²。

1.2 套中判定条件

设目标立柱中心坐标为 (tx, ty),半径为 tr。圆环落地时,若圆环中心与立柱中心的水平距离小于圆环内径与立柱半径之差,则判定为套中。

套中条件:distance(center, pole) < (ring_inner_radius - pole_radius)

同时需要满足圆环落地时的倾斜角度在一定范围内,否则圆环会弹开或立起。

二、核心算法设计

2.1 飞行时间计算

给定初始高度 y0、目标高度 y_target、初速度 v0 和角度 θ,求解飞行时间 t:

y_target = y0 + v0*sin(θ)*t - 0.5*g*t²

这是一个关于 t 的一元二次方程:

0.5*g*t² - v0*sin(θ)*t + (y_target - y0) = 0

使用求根公式求解,取正根中的较小值(首次到达目标高度的时间)。

2.2 最优角度搜索

对于固定的目标位置 (tx, ty) 和初速度 v0,可能存在0个、1个或2个投掷角度使圆环恰好落在目标位置。本文采用三分搜索在角度区间 [0, π/2] 上寻找使落点最接近目标的角度。

目标函数:f(θ) = |x_actual(tx, θ) - tx| + |y_actual - ty|

由于目标函数在有效区间内近似单峰,三分搜索可在 O(log(precision)) 时间内收敛到最优解。

2.3 考虑风速的补偿算法

实际游戏中可能存在横向风速 wind_x。风速会影响圆环的水平位移:

x(t) = x0 + v0*cos(θ)*t + 0.5*wind_x*t²

最优投掷角度需要根据风速进行补偿修正。本文采用牛顿迭代法求解带风速条件下的非线性方程,精确计算补偿角度。

三、Java完整实现

import java.util.ArrayList;
import java.util.List;

/**
 * 套圈游戏核心算法实现
 * 包含抛体运动模拟、最优角度决策与风速补偿
 */
public class RingToss {

    // 物理常量
    private static final double G = 9.8;           // 重力加速度 (m/s²)
    private static final double PRECISION = 1e-6;  // 数值精度

    /**
     * 目标立柱
     */
    static class Pole {
        double x, y;      // 中心坐标 (m)
        double radius;    // 立柱半径 (m)
        double height;    // 立柱高度 (m)
        int score;        // 分值

        Pole(double x, double y, double radius, double height, int score) {
            this.x = x;
            this.y = y;
            this.radius = radius;
            this.height = height;
            this.score = score;
        }
    }

    /**
     * 圆环参数
     */
    static class Ring {
        double outerRadius;  // 外径 (m)
        double innerRadius;  // 内径 (m)
        double mass;         // 质量 (kg)

        Ring(double outerRadius, double innerRadius, double mass) {
            this.outerRadius = outerRadius;
            this.innerRadius = innerRadius;
            this.mass = mass;
        }
    }

    /**
     * 投掷结果
     */
    static class ThrowResult {
        double angle;       // 投掷角度 (弧度)
        double velocity;    // 初速度 (m/s)
        double landX;       // 落点X坐标
        double landY;       // 落点Y坐标
        boolean success;    // 是否套中
        double score;       // 得分

        ThrowResult(double angle, double velocity, double landX, double landY,
                    boolean success, double score) {
            this.angle = angle;
            this.velocity = velocity;
            this.landX = landX;
            this.landY = landY;
            this.success = success;
            this.score = score;
        }
    }

    /**
     * 计算圆环飞行轨迹的落点
     * @param x0 初始X坐标
     * @param y0 初始Y坐标
     * @param v0 初速度
     * @param angle 投掷角度(与水平面夹角,弧度)
     * @param windX 横向风速
     * @return 落点坐标 [x, y]
     */
    static double[] simulateFlight(double x0, double y0, double v0,
                                    double angle, double windX) {
        double vx = v0 * Math.cos(angle);
        double vy = v0 * Math.sin(angle);

        // 解一元二次方程求落地时间:y0 + vy*t - 0.5*G*t² = 0
        double a = -0.5 * G;
        double b = vy;
        double c = y0;

        double discriminant = b * b - 4 * a * c;
        if (discriminant < 0) {
            return new double[]{x0, y0}; // 不会落地(向上抛且初速不足)
        }

        double t1 = (-b + Math.sqrt(discriminant)) / (2 * a);
        double t2 = (-b - Math.sqrt(discriminant)) / (2 * a);
        double t = Math.max(t1, t2); // 取落地时刻(正根中的较大者)
        if (t < 0) t = Math.min(t1, t2);
        if (t < 0) return new double[]{x0, y0};

        double landX = x0 + vx * t + 0.5 * windX * t * t;
        double landY = 0; // 假设地面为 y=0

        return new double[]{landX, landY};
    }

    /**
     * 三分搜索求解最优投掷角度
     * 目标:使落点尽可能接近目标立柱中心
     */
    static double findOptimalAngle(double x0, double y0, double targetX,
                                    double targetY, double v0, double windX) {
        double left = 0.0;
        double right = Math.PI / 2 - 0.01; // 略小于90度

        // 目标函数:落点与目标的距离平方
        java.util.function.Function<Double, Double> objective = angle -> {
            double[] land = simulateFlight(x0, y0, v0, angle, windX);
            double dx = land[0] - targetX;
            double dy = land[1] - targetY;
            return dx * dx + dy * dy;
        };

        // 三分搜索
        for (int i = 0; i < 100; i++) {
            double m1 = left + (right - left) / 3;
            double m2 = right - (right - left) / 3;

            if (objective.apply(m1) < objective.apply(m2)) {
                right = m2;
            } else {
                left = m1;
            }
        }

        return (left + right) / 2;
    }

    /**
     * 牛顿迭代法求解精确角度(无风速情况)
     * 给定目标 (tx, ty),求满足 x(tx, angle) = tx 且 y(tx, angle) = ty 的角度
     */
    static double solveExactAngle(double x0, double y0, double tx, double ty, double v0) {
        double angle = Math.atan2(ty - y0, tx - x0); // 初始猜测

        for (int i = 0; i < 50; i++) {
            double[] land = simulateFlight(x0, y0, v0, angle, 0);
            double f = land[0] - tx; // 误差函数

            if (Math.abs(f) < PRECISION) break;

            // 数值计算导数
            double delta = 1e-8;
            double[] landPlus = simulateFlight(x0, y0, v0, angle + delta, 0);
            double df = (landPlus[0] - land[0]) / delta;

            if (Math.abs(df) < PRECISION) break;
            angle -= f / df;
        }
        return angle;
    }

    /**
     * 判定套中条件
     */
    static boolean isSuccess(double landX, double landY, Pole pole, Ring ring) {
        // 水平距离判定
        double dist = Math.sqrt((landX - pole.x) * (landX - pole.x)
                               + (landY - pole.y) * (landY - pole.y));
        // 圆环内径必须大于立柱半径,且落点偏差在允许范围内
        double tolerance = ring.innerRadius - pole.radius;
        return tolerance > 0 && dist < tolerance * 0.8; // 0.8为容错系数
    }

    /**
     * 执行一次投掷,返回结果
     */
    static ThrowResult throwRing(double x0, double y0, Pole target,
                                  Ring ring, double v0, double windX) {
        double angle = findOptimalAngle(x0, y0, target.x, target.y, v0, windX);
        double[] land = simulateFlight(x0, y0, v0, angle, windX);
        boolean success = isSuccess(land[0], land[1], target, ring);
        double score = success ? target.score : 0;

        return new ThrowResult(angle, v0, land[0], land[1], success, score);
    }

    /**
     * 多目标最优选择:在多个立柱中选择期望得分最高的目标
     */
    static Pole selectBestTarget(double x0, double y0, List<Pole> poles,
                                  Ring ring, double v0, double windX) {
        Pole best = null;
        double bestScore = -1;

        for (Pole p : poles) {
            ThrowResult result = throwRing(x0, y0, p, ring, v0, windX);
            // 考虑距离惩罚:距离越远,误差越大,成功率越低
            double dist = Math.sqrt((p.x - x0) * (p.x - x0) + (p.y - y0) * (p.y - y0));
            double successProb = Math.max(0, 1 - dist / 10.0); // 简化的成功率模型
            double expectedScore = result.score * successProb;

            if (expectedScore > bestScore) {
                bestScore = expectedScore;
                best = p;
            }
        }
        return best;
    }

    public static void main(String[] args) {
        // 初始化场景
        double x0 = 0, y0 = 1.5; // 投掷点:原点,高度1.5米
        double v0 = 8.0;         // 初速度 8m/s
        double windX = -0.5;     // 横向逆风 0.5m/s²

        Ring ring = new Ring(0.15, 0.12, 0.05); // 外径15cm,内径12cm

        List<Pole> poles = new ArrayList<>();
        poles.add(new Pole(3.0, 0, 0.03, 0.5, 10));   // 近处低分柱
        poles.add(new Pole(5.0, 0, 0.02, 0.3, 30));   // 远处高分柱
        poles.add(new Pole(7.0, 0, 0.015, 0.2, 50));  // 最远超高分布柱

        System.out.println("=== 套圈游戏模拟 ===");
        System.out.printf("投掷点: (%.1f, %.1f), 初速: %.1fm/s%n", x0, y0, v0);
        System.out.printf("风速: %.1fm/s²%n%n", windX);

        for (Pole p : poles) {
            ThrowResult r = throwRing(x0, y0, p, ring, v0, windX);
            System.out.printf("目标 [%dm, %d分]: 角度=%.2f°, 落点=(%.2f, %.2f), 套中=%s%n",
                (int)p.x, p.score,
                Math.toDegrees(r.angle), r.landX, r.landY, r.success);
        }

        // 自动选择最优目标
        Pole best = selectBestTarget(x0, y0, poles, ring, v0, windX);
        System.out.printf("%nAI推荐最优目标: %dm处 %d分立柱%n", (int)best.x, best.score);
    }
}

四、算法复杂度分析

算法模块 时间复杂度 空间复杂度 说明
抛体模拟 O(1) O(1) 解一元二次方程
三分搜索 O(log(1/ε)) O(1) ε为精度,约50次迭代
牛顿迭代 O(log(1/ε)) O(1) 二次收敛,约10次迭代
最优目标选择 O(m·log(1/ε)) O(1) m为目标数量

五、扩展方向

  1. 三维场景:引入Z轴深度,考虑投掷线的横向偏移
  2. 旋转动力学:模拟圆环自转与进动对落点的影响
  3. 物理引擎集成:使用欧拉积分或Runge-Kutta方法处理更复杂的受力
  4. 机器学习优化:通过历史投掷数据训练神经网络,预测玩家力度偏差

六、总结

套圈游戏将经典物理问题与决策优化相结合。本文实现的核心算法包括:

  • 抛体运动方程 精确模拟圆环飞行轨迹
  • 一元二次方程求根 计算飞行时间
  • 三分搜索 在单峰函数上快速寻找最优投掷角度
  • 牛顿迭代法 精确求解非线性方程
  • 期望收益最大化 在多目标中选择最优投掷对象

这些算法不仅适用于游戏开发,也是机器人投掷、投篮机器人等实际工程问题的核心基础。