每日算法 — 使用java实现踢毽子:物理抛物线模拟与动态平衡控制

踢毽子是中国传统民间体育活动,玩家用脚将毽子踢起并控制其不落地。本文用Java实现踢毽子的物理模拟与AI控制系统,重点讲解抛体运动学建模、空气阻力数值积分,以及基于PID控制器的动态平衡调节算法。

问题建模

将毽子简化为质点,在二维平面内运动(忽略水平偏航,仅考虑垂直和前后运动):
– 位置(x, y):x为水平位移,y为高度
– 速度(vx, vy):水平和垂直分量
– 受力:重力、空气阻力、脚部踢击力

玩家需要预测毽子的落点,移动脚部到合适位置施加踢击力,使毽子保持在可控高度范围内。

核心算法

1. 抛体运动与空气阻力

空气阻力与速度平方成正比,方向与速度相反。使用四阶龙格-库塔法(RK4)进行高精度数值积分。

2. 落点预测

基于当前位置和速度,求解毽子到达地面(y=0)的时间和水平位置。

3. PID控制器

控制脚部移动速度,使落点位置与脚部位置误差收敛到零。

完整Java实现

import java.util.*;

/**
 * 踢毽子 — 物理抛物线模拟与动态平衡控制
 * 包含RK4数值积分、落点预测、PID控制器
 */
public class JianziGame {

    // 物理常量
    private static final double GRAVITY = 9.8;           // 重力加速度 (m/s²)
    private static final double AIR_DENSITY = 1.225;     // 空气密度 (kg/m³)
    private static final double DRAG_COEFF = 0.47;       // 球形阻力系数
    private static final double JIANZI_AREA = 0.005;     // 毽子截面积 (m²)
    private static final double JIANZI_MASS = 0.015;     // 毽子质量 (kg)
    private static final double DT = 0.01;               // 模拟时间步长 (s)

    // PID参数
    private double kp = 2.0;   // 比例增益
    private double ki = 0.1;   // 积分增益
    private double kd = 0.5;   // 微分增益

    private double integral = 0;
    private double prevError = 0;

    /**
     * 毽子状态向量
     */
    public static class State {
        double x, y;      // 位置 (m)
        double vx, vy;    // 速度 (m/s)
        double t;         // 时间 (s)

        public State(double x, double y, double vx, double vy) {
            this.x = x; this.y = y; this.vx = vx; this.vy = vy; this.t = 0;
        }

        public State clone() {
            State s = new State(x, y, vx, vy);
            s.t = t;
            return s;
        }
    }

    /**
     * 计算空气阻力加速度
     * F_drag = 0.5 * ρ * C_d * A * v²
     * a_drag = F_drag / m,方向与速度相反
     */
    private double[] dragAcceleration(double vx, double vy) {
        double v = Math.sqrt(vx * vx + vy * vy);
        if (v < 0.001) return new double[]{0, 0};

        double fDrag = 0.5 * AIR_DENSITY * DRAG_COEFF * JIANZI_AREA * v * v;
        double aDrag = fDrag / JIANZI_MASS;

        return new double[]{
            -aDrag * vx / v,  // x方向阻力加速度
            -aDrag * vy / v   // y方向阻力加速度
        };
    }

    /**
     * 计算状态导数(用于RK4)
     * 返回 [dx/dt, dy/dt, dvx/dt, dvy/dt]
     */
    private double[] derivatives(State s) {
        double[] aDrag = dragAcceleration(s.vx, s.vy);
        return new double[]{
            s.vx,                    // dx/dt = vx
            s.vy,                    // dy/dt = vy
            aDrag[0],                // dvx/dt = 空气阻力x分量
            aDrag[1] - GRAVITY       // dvy/dt = 空气阻力y分量 - 重力
        };
    }

    /**
     * RK4单步积分
     */
    public State rk4Step(State s, double dt) {
        double[] k1 = derivatives(s);

        State s2 = s.clone();
        s2.x += k1[0] * dt / 2;
        s2.y += k1[1] * dt / 2;
        s2.vx += k1[2] * dt / 2;
        s2.vy += k1[3] * dt / 2;
        double[] k2 = derivatives(s2);

        State s3 = s.clone();
        s3.x += k2[0] * dt / 2;
        s3.y += k2[1] * dt / 2;
        s3.vx += k2[2] * dt / 2;
        s3.vy += k2[3] * dt / 2;
        double[] k3 = derivatives(s3);

        State s4 = s.clone();
        s4.x += k3[0] * dt;
        s4.y += k3[1] * dt;
        s4.vx += k3[2] * dt;
        s4.vy += k3[3] * dt;
        double[] k4 = derivatives(s4);

        State next = s.clone();
        next.x += (k1[0] + 2*k2[0] + 2*k3[0] + k4[0]) * dt / 6;
        next.y += (k1[1] + 2*k2[1] + 2*k3[1] + k4[1]) * dt / 6;
        next.vx += (k1[2] + 2*k2[2] + 2*k3[2] + k4[2]) * dt / 6;
        next.vy += (k1[3] + 2*k2[3] + 2*k3[3] + k4[3]) * dt / 6;
        next.t += dt;

        return next;
    }

    /**
     * 模拟自由飞行直到落地或超时
     */
    public List<State> simulateFlight(State initial, double maxTime) {
        List<State> trajectory = new ArrayList<>();
        State current = initial.clone();
        trajectory.add(current.clone());

        int steps = (int)(maxTime / DT);
        for (int i = 0; i < steps; i++) {
            current = rk4Step(current, DT);
            trajectory.add(current.clone());

            // 落地检测
            if (current.y <= 0 && current.vy < 0) {
                break;
            }
        }

        return trajectory;
    }

    /**
     * 预测落点:基于当前状态计算落地时间和位置
     * 使用二分搜索在轨迹中精确找到y=0的点
     */
    public double[] predictLanding(State current) {
        List<State> traj = simulateFlight(current, 10.0);

        // 找到第一个y<=0的点
        int idx = -1;
        for (int i = 1; i < traj.size(); i++) {
            if (traj.get(i).y <= 0) {
                idx = i;
                break;
            }
        }

        if (idx == -1) {
            // 未落地,返回预测终点
            State last = traj.get(traj.size() - 1);
            return new double[]{last.t, last.x};
        }

        // 线性插值精确落点
        State before = traj.get(idx - 1);
        State after = traj.get(idx);
        double ratio = before.y / (before.y - after.y);
        double landTime = before.t + ratio * (after.t - before.t);
        double landX = before.x + ratio * (after.x - before.x);

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

    /**
     * 脚部踢击:施加瞬时冲量改变毽子速度
     * 目标是将毽子踢到(targetHeight, 0水平偏移)附近
     */
    public State kick(State current, double footX, double footY,
                      double targetHeight, double targetX) {
        State after = current.clone();

        // 计算需要的垂直速度:vy² = 2gh(忽略阻力近似)
        double neededVy = Math.sqrt(2 * GRAVITY * targetHeight);
        // 更精确:考虑空气阻力的修正(经验系数)
        neededVy *= 1.15;

        // 计算需要的水平速度:使毽子落回目标位置
        double flightTime = 2 * neededVy / GRAVITY;
        double neededVx = (targetX - current.x) / flightTime;

        // 脚部碰撞导致的速度变化(冲量模型)
        double deltaVx = neededVx - current.vx;
        double deltaVy = neededVy - current.vy;

        // 限制最大踢击力度
        double maxKick = 8.0;
        double kickMag = Math.sqrt(deltaVx * deltaVx + deltaVy * deltaVy);
        if (kickMag > maxKick) {
            double scale = maxKick / kickMag;
            deltaVx *= scale;
            deltaVy *= scale;
        }

        after.vx += deltaVx;
        after.vy += deltaVy;
        after.y = footY + 0.1; // 踢击后略微升高

        return after;
    }

    /**
     * PID控制器:计算脚部水平移动速度
     * error = 目标落点 - 当前脚部位置
     */
    public double pidControl(double footX, double landingX, double dt) {
        double error = landingX - footX;
        integral += error * dt;
        double derivative = (error - prevError) / dt;
        prevError = error;

        double output = kp * error + ki * integral + kd * derivative;

        // 限制最大移动速度 (m/s)
        double maxSpeed = 3.0;
        return Math.max(-maxSpeed, Math.min(maxSpeed, output));
    }

    /**
     * 完整游戏模拟:AI自动踢毽子
     */
    public void simulateGame(int maxKicks) {
        // 初始状态:从空中下落
        State jianzi = new State(0, 1.5, 0.3, -1.0);
        double footX = 0;
        double footY = 0;

        List<Double> heights = new ArrayList<>();
        List<Double> footPositions = new ArrayList<>();
        int kickCount = 0;

        System.out.println("===== 踢毽子AI模拟 =====");
        System.out.println("时间(s)\t高度(m)\t水平(m)\t脚位置(m)\t状态");

        double simTime = 0;
        double printInterval = 0.1;
        double nextPrint = 0;

        while (kickCount < maxKicks && simTime < 30) {
            // 模拟飞行
            List<State> traj = simulateFlight(jianzi, 5.0);

            // 检查是否落地
            if (traj.get(traj.size() - 1).y <= 0) {
                // 预测落点
                double[] landing = predictLanding(jianzi);
                double landTime = landing[0];
                double landX = landing[1];

                // 打印飞行过程关键点
                for (State s : traj) {
                    if (s.t >= nextPrint) {
                        System.out.printf("%.2f\t%.3f\t%.3f\t%.3f\t%s%n",
                            simTime + s.t, s.y, s.x, footX,
                            s.y > 0.1 ? "飞行" : "即将落地");
                        nextPrint += printInterval;
                    }
                    if (s.y <= 0) break;
                }

                // PID控制脚部移动
                double moveSpeed = pidControl(footX, landX, landTime);
                footX += moveSpeed * landTime;

                // 踢击
                jianzi = kick(jianzi, footX, footY, 1.5 + Math.random() * 0.5, footX);
                kickCount++;
                simTime += landTime;

                // 重置PID积分项(每次踢击后重新适应)
                integral *= 0.5;

                System.out.printf("%.2f\t---\t---\t%.3f\t踢击 #%d (目标高度=%.2f)%n",
                    simTime, footX, kickCount, 1.5);

                nextPrint = simTime + printInterval;
            } else {
                // 异常:未落地但超时
                break;
            }
        }

        System.out.println("\n===== 统计 =====");
        System.out.println("总踢击次数: " + kickCount);
        System.out.println("模拟时长: " + String.format("%.2f", simTime) + "秒");
    }

    /**
     * 对比实验:有空气阻力 vs 无空气阻力
     */
    public void compareDragEffect() {
        System.out.println("\n===== 空气阻力影响对比 =====");
        State noDrag = new State(0, 2.0, 2.0, 4.0);
        State withDrag = noDrag.clone();

        // 无阻力模拟(简化为解析解)
        double flightTimeNoDrag = (noDrag.vy + Math.sqrt(noDrag.vy * noDrag.vy + 2 * GRAVITY * noDrag.y)) / GRAVITY;
        double landXNoDrag = noDrag.x + noDrag.vx * flightTimeNoDrag;

        // 有阻力模拟
        List<State> traj = simulateFlight(withDrag, 10.0);
        State last = traj.get(traj.size() - 1);

        System.out.printf("初始状态: 高度=%.2fm, vx=%.2f, vy=%.2f%n", noDrag.y, noDrag.vx, noDrag.vy);
        System.out.printf("无阻力预测: 飞行%.2fs, 落点%.2fm%n", flightTimeNoDrag, landXNoDrag);
        System.out.printf("有阻力模拟: 飞行%.2fs, 落点%.2fm%n", last.t, last.x);
        System.out.printf("水平偏差: %.2fm (%.1f%%)%n",
            landXNoDrag - last.x, (landXNoDrag - last.x) / landXNoDrag * 100);
    }

    /**
     * 参数调优:扫描PID参数组合
     */
    public void tunePidParameters() {
        System.out.println("\n===== PID参数扫描 =====");
        double bestKp = 0, bestKi = 0, bestKd = 0;
        double bestScore = Double.NEGATIVE_INFINITY;

        for (double p = 0.5; p <= 5.0; p += 0.5) {
            for (double i = 0.0; i <= 0.5; i += 0.1) {
                for (double d = 0.0; d <= 2.0; d += 0.5) {
                    this.kp = p; this.ki = i; this.kd = d;
                    this.integral = 0; this.prevError = 0;

                    double score = evaluatePid();
                    if (score > bestScore) {
                        bestScore = score;
                        bestKp = p; bestKi = i; bestKd = d;
                    }
                }
            }
        }

        System.out.printf("最优参数: Kp=%.1f, Ki=%.1f, Kd=%.1f, 评分=%.2f%n",
            bestKp, bestKi, bestKd, bestScore);

        // 恢复最优参数
        this.kp = bestKp;
        this.ki = bestKi;
        this.kd = bestKd;
        this.integral = 0;
        this.prevError = 0;
    }

    private double evaluatePid() {
        // 简化的评分:模拟5次踢击,计算落点误差累积
        State jianzi = new State(0, 1.5, 0.2, -0.8);
        double footX = 0;
        double totalError = 0;

        for (int kick = 0; kick < 5; kick++) {
            double[] landing = predictLanding(jianzi);
            double landX = landing[1];

            double error = landX - footX;
            double moveSpeed = pidControl(footX, landX, landing[0]);
            footX += moveSpeed * landing[0];
            totalError += Math.abs(error);

            jianzi = kick(jianzi, footX, 0, 1.5, footX);
        }

        return -totalError; // 误差越小,评分越高
    }

    public static void main(String[] args) {
        JianziGame game = new JianziGame();

        // PID参数调优
        game.tunePidParameters();

        // 空气阻力对比
        game.compareDragEffect();

        // 完整游戏模拟
        game.simulateGame(20);
    }
}

物理模型详解

空气阻力建模

空气阻力公式采用经典形式:

F_drag = ½ × ρ × C_d × A × v²

对于毽子这类轻小物体,空气阻力与重力的比值可达0.3-0.5,不可忽略。RK4数值积分以O(dt⁴)的精度追踪运动轨迹,10ms步长即可达到厘米级精度。

PID控制器设计

控制器输入为”预测落点与脚部位置的水平偏差”,输出为”脚部水平移动速度”。三个增益的作用:
– Kp(比例):快速响应大偏差
– Ki(积分):消除稳态误差
– Kd(微分):抑制振荡、提前减速

复杂度分析

模块 时间复杂度 空间复杂度
RK4单步积分 O(1) O(1)
完整飞行模拟 O(maxTime/DT) O(maxTime/DT)
落点预测 O(maxTime/DT) O(maxTime/DT)
PID控制 O(1) O(1)
参数扫描 O(Kp_range × Ki_range × Kd_range × 模拟代价) O(1)

扩展方向

  1. 风力扰动:引入随机水平力,测试控制器鲁棒性
  2. 多毽子协同:马尔可夫链建模多个毽子的运动模式
  3. 神经网络替代PID:用强化学习训练端到端控制器
  4. 三维扩展:加入前后深度方向的控制

总结

踢毽子AI将经典控制理论与物理仿真结合。RK4数值积分精确追踪非线性运动,PID控制器实现实时平衡调节,落点预测提供前瞻决策能力。这种”预测-控制-执行”的架构是机器人控制、无人机姿态稳定和自动驾驶路径跟踪的核心方法论。