每日算法 — 使用java实现Hurdle:抛物线运动模拟与最优起跳时机决策

游戏简介

Hurdle(跨栏)是一款经典的田径体育游戏,玩家控制运动员在跑道上奔跑并跨越一系列栏架。游戏的核心挑战在于:如何在正确的时机起跳,使运动员以最优的抛物线轨迹越过栏架,同时保持最快的速度冲向终点。

本文用Java实现Hurdle跨栏游戏的核心物理引擎,重点讲解抛体运动学建模、AABB碰撞检测以及最优起跳时机决策算法。

核心算法设计

1. 物理实体与运动学模型

跨栏运动员的运动可分解为水平方向的匀速直线运动和竖直方向的匀变速抛物线运动。我们用Runner类封装运动员的状态,用Hurdle类表示栏架。

public class HurdleGame {
    // 跑道参数
    private static final double TRACK_LENGTH = 110.0; // 米
    private static final double LANE_WIDTH = 1.22;    // 米

    // 物理参数
    private static final double GRAVITY = 9.8;        // 重力加速度 m/s²
    private static final double GROUND_Y = 0.0;       // 地面Y坐标

    private Runner runner;
    private List<Hurdle> hurdles;
    private double trackProgress; // 运动员在跑道上的进度(米)

    /**
     * 运动员实体,包含位置、速度、状态
     */
    public static class Runner {
        double x, y;          // 位置(米)
        double vx, vy;        // 速度(米/秒)
        double width = 0.5;   // 碰撞盒宽度
        double height = 1.8;  // 碰撞盒高度
        boolean isJumping;    // 是否处于跳跃状态
        boolean isGrounded;   // 是否在地面上

        public Runner(double startX) {
            this.x = startX;
            this.y = GROUND_Y;
            this.vx = 0;
            this.vy = 0;
            this.isJumping = false;
            this.isGrounded = true;
        }
    }

    /**
     * 栏架实体
     */
    public static class Hurdle {
        double x;           // 栏架位置
        double height;      // 栏架高度(男子1.067米,女子0.84米)
        double width = 0.2; // 栏架厚度

        public Hurdle(double x, double height) {
            this.x = x;
            this.height = height;
        }
    }

    public HurdleGame() {
        this.runner = new Runner(0.0);
        this.hurdles = new ArrayList<>();
        this.trackProgress = 0.0;
        initializeHurdles();
    }

    /**
     * 初始化标准110米栏的栏架布局
     * 起跑线到第一栏13.72米,栏间距9.14米,最后一栏到终点14.02米
     */
    private void initializeHurdles() {
        double hurdleHeight = 1.067; // 男子标准栏高
        double firstHurdle = 13.72;
        double spacing = 9.14;
        int count = 10;

        for (int i = 0; i < count; i++) {
            hurdles.add(new Hurdle(firstHurdle + i * spacing, hurdleHeight));
        }
    }
}

2. 抛体运动模拟

运动员起跳后,在水平和竖直方向分别遵循不同的运动规律。水平方向近似匀速(忽略空气阻力),竖直方向受重力影响做匀减速运动。

public class HurdleGame {
    private static final double TIME_STEP = 0.016; // 模拟步长:16ms(约60fps)

    /**
     * 更新运动员一帧的物理状态
     * 使用欧拉积分进行运动学更新
     */
    public void updateRunnerPhysics() {
        Runner r = runner;

        // 更新位置:x = x + vx * dt
        r.x += r.vx * TIME_STEP;
        r.y += r.vy * TIME_STEP;

        // 竖直方向受重力影响:vy = vy - g * dt
        r.vy -= GRAVITY * TIME_STEP;

        // 地面碰撞检测
        if (r.y <= GROUND_Y) {
            r.y = GROUND_Y;
            r.vy = 0;
            r.isJumping = false;
            r.isGrounded = true;
        }

        trackProgress = r.x;
    }

    /**
     * 运动员起跳动作
     * @param jumpVelocity 起跳初速度(竖直方向)
     */
    public void jump(double jumpVelocity) {
        if (runner.isGrounded) {
            runner.vy = jumpVelocity;
            runner.isJumping = true;
            runner.isGrounded = false;
        }
    }

    /**
     * 加速奔跑:每帧增加水平速度(有上限)
     * @param acceleration 水平加速度
     * @param maxSpeed 最大速度
     */
    public void accelerate(double acceleration, double maxSpeed) {
        runner.vx += acceleration * TIME_STEP;
        if (runner.vx > maxSpeed) {
            runner.vx = maxSpeed;
        }
    }

    /**
     * 计算给定起跳速度下,运动员的抛物线轨迹最高点
     * @param jumpV 起跳竖直速度
     * @return 最高点高度
     */
    public double calculatePeakHeight(double jumpV) {
        // v² = 2gh => h = v² / (2g)
        return (jumpV * jumpV) / (2 * GRAVITY);
    }

    /**
     * 计算给定起跳速度下,运动员从起跳到落地的总时间
     * @param jumpV 起跳竖直速度
     * @return 飞行总时间
     */
    public double calculateFlightTime(double jumpV) {
        // t = 2 * v / g(上升和下降时间对称)
        return 2.0 * jumpV / GRAVITY;
    }

    /**
     * 计算给定起跳速度下,运动员的水平飞行距离
     * @param jumpV 起跳竖直速度
     * @param horizontalV 水平速度
     * @return 水平飞行距离
     */
    public double calculateFlightDistance(double jumpV, double horizontalV) {
        return horizontalV * calculateFlightTime(jumpV);
    }
}

3. AABB碰撞检测与栏架碰撞判定

运动员与栏架的碰撞采用轴对齐包围盒(AABB)检测。栏架可视为一个竖直的矩形障碍物,运动员为一个矩形。

public class HurdleGame {
    /**
     * AABB碰撞检测:判断运动员是否与指定栏架发生碰撞
     * @param hurdle 栏架
     * @return 是否发生碰撞
     */
    public boolean checkHurdleCollision(Hurdle hurdle) {
        Runner r = runner;

        // 运动员的AABB
        double runnerLeft = r.x - r.width / 2;
        double runnerRight = r.x + r.width / 2;
        double runnerBottom = r.y;
        double runnerTop = r.y + r.height;

        // 栏架的AABB
        double hurdleLeft = hurdle.x - hurdle.width / 2;
        double hurdleRight = hurdle.x + hurdle.width / 2;
        double hurdleBottom = GROUND_Y;
        double hurdleTop = hurdle.height;

        // AABB相交判断:两个矩形在X轴和Y轴上的投影都重叠
        boolean overlapX = runnerLeft < hurdleRight && runnerRight > hurdleLeft;
        boolean overlapY = runnerBottom < hurdleTop && runnerTop > hurdleBottom;

        return overlapX && overlapY;
    }

    /**
     * 扫描所有栏架,检测是否有碰撞
     * @return 发生碰撞的栏架索引,无碰撞返回-1
     */
    public int checkAllHurdlesCollision() {
        for (int i = 0; i < hurdles.size(); i++) {
            if (checkHurdleCollision(hurdles.get(i))) {
                return i;
            }
        }
        return -1;
    }

    /**
     * 检测运动员是否已经越过某栏架(用于计分)
     * @param hurdle 栏架
     * @return 是否已越过
     */
    public boolean hasClearedHurdle(Hurdle hurdle) {
        return runner.x > hurdle.x + hurdle.width / 2;
    }
}

4. 最优起跳时机决策算法

跨栏的核心是:在当前水平速度下,何时起跳才能恰好越过栏架?这是一个抛物线轨迹规划问题。

public class HurdleGame {
    /**
     * 计算越过指定栏架所需的最小起跳速度
     * 假设运动员在栏架正上方时达到轨迹最高点
     * @param hurdle 目标栏架
     * @param horizontalV 当前水平速度
     * @return 最小竖直起跳速度,若无法越过返回-1
     */
    public double calculateMinJumpVelocity(Hurdle hurdle, double horizontalV) {
        double safetyMargin = 0.15; // 安全余量(米)
        double requiredHeight = hurdle.height + safetyMargin;

        // h = v² / (2g) => v = sqrt(2gh)
        return Math.sqrt(2 * GRAVITY * requiredHeight);
    }

    /**
     * 计算最优起跳距离(在栏架前多远起跳)
     * 策略:让轨迹最高点正好位于栏架上方
     * @param hurdle 目标栏架
     * @param horizontalV 水平速度
     * @param jumpV 起跳竖直速度
     * @return 起跳点距离栏架的距离
     */
    public double calculateOptimalTakeoffDistance(Hurdle hurdle, 
                                                    double horizontalV, 
                                                    double jumpV) {
        // 飞行总时间
        double flightTime = calculateFlightTime(jumpV);
        // 最高点出现在一半时间
        double timeToPeak = flightTime / 2.0;
        // 起跳点到栏架的距离 = 水平速度 × 到最高点的时间
        return horizontalV * timeToPeak;
    }

    /**
     * AI决策:判断当前是否应该起跳以越过下一个栏架
     * @return 如果需要起跳返回建议的起跳速度,否则返回0
     */
    public double decideJump() {
        // 找到下一个尚未越过的栏架
        Hurdle nextHurdle = null;
        for (Hurdle h : hurdles) {
            if (!hasClearedHurdle(h)) {
                nextHurdle = h;
                break;
            }
        }

        if (nextHurdle == null) {
            return 0; // 没有更多栏架
        }

        double horizontalV = runner.vx;
        double minJumpV = calculateMinJumpVelocity(nextHurdle, horizontalV);
        double optimalDist = calculateOptimalTakeoffDistance(nextHurdle, horizontalV, minJumpV);

        // 当前距离栏架的距离
        double distToHurdle = nextHurdle.x - runner.x;

        // 起跳窗口:在 optimalDist ± 0.3米 范围内起跳都是可以接受的
        double window = 0.3;

        if (distToHurdle <= optimalDist + window && distToHurdle >= optimalDist - window) {
            return minJumpV;
        }

        // 如果已经很近了但还没起跳,紧急起跳
        if (distToHurdle < optimalDist - window && distToHurdle > 0.5) {
            // 增加起跳速度以补偿延迟
            double delay = (optimalDist - distToHurdle) / horizontalV;
            return minJumpV + GRAVITY * delay * 0.5;
        }

        return 0; // 暂时不起跳
    }
}

5. 完整游戏循环与模拟

public class HurdleGame {
    private boolean gameOver;
    private double finishTime;
    private int hurdlesCleared;
    private static final double MAX_SPEED = 10.0; // 最大速度 m/s
    private static final double ACCEL = 3.0;      // 加速度 m/s²

    public HurdleGame() {
        this.runner = new Runner(0.0);
        this.hurdles = new ArrayList<>();
        this.trackProgress = 0.0;
        this.gameOver = false;
        this.hurdlesCleared = 0;
        initializeHurdles();
    }

    /**
     * 游戏主循环的一帧更新
     * @param autoPlay 是否启用AI自动起跳
     * @return 当前游戏状态信息
     */
    public String updateFrame(boolean autoPlay) {
        if (gameOver) return "游戏已结束";

        // 持续加速
        accelerate(ACCEL, MAX_SPEED);

        // AI自动起跳决策
        if (autoPlay && runner.isGrounded) {
            double jumpV = decideJump();
            if (jumpV > 0) {
                jump(jumpV);
            }
        }

        // 更新物理
        updateRunnerPhysics();

        // 碰撞检测
        int collisionIdx = checkAllHurdlesCollision();
        if (collisionIdx >= 0) {
            gameOver = true;
            return "撞栏!游戏结束。已越过" + hurdlesCleared + "个栏架";
        }

        // 统计已越过的栏架
        hurdlesCleared = 0;
        for (Hurdle h : hurdles) {
            if (hasClearedHurdle(h)) hurdlesCleared++;
        }

        // 到达终点
        if (runner.x >= TRACK_LENGTH) {
            gameOver = true;
            return "到达终点!成绩:已越过全部" + hurdlesCleared + "个栏架";
        }

        return String.format("位置:%.2fm 速度:%.2fm/s 已越:%d/10", 
                             runner.x, runner.vx, hurdlesCleared);
    }

    /**
     * 手动起跳(供玩家控制)
     * @param jumpV 起跳速度
     */
    public void manualJump(double jumpV) {
        jump(jumpV);
    }

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

        System.out.println("=== Hurdle跨栏模拟(AI自动模式)===");
        int frame = 0;
        while (!game.isGameOver() && frame < 10000) {
            String status = game.updateFrame(true);
            if (game.isGameOver()) {
                System.out.println(status);
                break;
            }
            // 每30帧打印一次状态
            if (frame % 30 == 0) {
                System.out.println(status);
            }
            frame++;
        }
    }

    public boolean isGameOver() {
        return gameOver;
    }
}

算法复杂度分析

操作 时间复杂度 空间复杂度
物理更新(每帧) O(1) O(1)
AABB碰撞检测 O(n),n为栏架数量 O(1)
最优起跳计算 O(1) O(1)
完整比赛模拟 O(frames × n) O(n)

由于栏架数量固定为10个,实际每帧运算量极小,可轻松达到60fps以上的实时性能。

扩展方向

  1. Runge-Kutta积分:将欧拉积分替换为RK4,获得更精确的抛物线轨迹模拟。
  2. 风阻建模:引入与速度平方成正比的空气阻力项,使高速奔跑时的轨迹更符合真实物理。
  3. 栏间节奏优化:将栏间距与运动员的步频结合,寻找最优的”三步一栏”或”五步一栏”节奏。

小结

Hurdle跨栏游戏的核心算法在于抛体运动学建模与最优起跳时机决策。通过将运动分解为水平匀速和竖直匀变速两个分量,可以精确预测运动员的飞行轨迹;AABB碰撞检测则高效判定是否成功越栏。起跳决策算法本质上是求解一个”给定初速度,何时起跳使轨迹最高点恰好位于栏架上方”的几何规划问题,这一思路同样适用于平台跳跃类游戏的自动寻路。