每日算法 — 使用java实现地铁跑酷:贪心避障决策与动态路径规划

地铁跑酷是一款经典的 endless runner(无尽跑酷)游戏,玩家控制角色在三条轨道上奔跑,通过左右切换和跳跃翻滚来躲避障碍物并收集金币。本文用Java实现跑酷游戏的核心AI决策系统。

游戏状态建模

跑酷游戏的状态可以抽象为一个时序决策问题:在每个时间步,AI需要基于当前视野内的障碍物分布选择最优动作。

public class SubwaySurfEngine {

    // 轨道编号
    enum Track { LEFT, CENTER, RIGHT }

    // 动作类型
    enum Action { LEFT, RIGHT, JUMP, ROLL, STAY }

    // 障碍物类型
    enum ObstacleType {
        NONE,        // 空地
        BLOCK_LOW,   // 低障碍(需跳跃)
        BLOCK_HIGH,  // 高障碍(需翻滚)
        BLOCK_BOTH,  // 高低都有(需切换轨道)
        TRAIN,       // 火车(需切换轨道)
        COIN,        // 金币
        COIN_LINE,   // 金币线(连续金币)
        MAGNET,      // 磁铁道具
        BOOST        // 加速道具
    }

    static class Cell {
        ObstacleType type;
        int z;           // 纵向距离(前方距离)
        Track track;     // 所属轨道

        Cell(ObstacleType type, int z, Track track) {
            this.type = type;
            this.z = z;
            this.track = track;
        }
    }

    static class GameState {
        Track currentTrack;     // 当前所在轨道
        int score;
        int speed;              // 当前速度(像素/帧)
        boolean isJumping;
        boolean isRolling;
        int jumpFrame;          // 跳跃已持续帧数

        // 视野内前方网格(z从0到VISUAL_RANGE)
        Map<Track, List<Cell>> visibleCells;

        GameState() {
            this.currentTrack = Track.CENTER;
            this.visibleCells = new HashMap<>();
            for (Track t : Track.values()) {
                visibleCells.put(t, new ArrayList<>());
            }
        }
    }

    static final int VISUAL_RANGE = 30;    // 视野范围(格数)
    static final int JUMP_DURATION = 12;   // 跳跃持续帧数
    static final int ROLL_DURATION = 8;    // 翻滚持续帧数
    static final int SWITCH_COOLDOWN = 3;  // 切换轨道冷却帧数

    private GameState state;

    public SubwaySurfEngine() {
        this.state = new GameState();
    }
}

贪心避障决策

最基础的策略是贪心法:在每个决策点选择当前视野内最优的动作。我们设计一个综合评分函数来评估每个动作的价值。

public class SubwaySurfEngine {

    /**
     * 评估在指定轨道上前进的前景得分
     * 综合考虑:障碍物威胁、金币收益、道具价值
     */
    private double evaluateTrack(Track track, int lookAhead) {
        double score = 0;
        List<Cell> cells = state.visibleCells.get(track);

        for (Cell cell : cells) {
            if (cell.z > lookAhead) continue;

            double distanceWeight = 1.0 / (cell.z + 1); // 越近的权重越高

            switch (cell.type) {
                case NONE -> score += 1 * distanceWeight; // 安全通行奖励
                case BLOCK_LOW -> {
                    if (cell.z < 5) score -= 100 * distanceWeight; // 近处低障碍致命
                    else score -= 10 * distanceWeight;
                }
                case BLOCK_HIGH -> {
                    if (cell.z < 4) score -= 100 * distanceWeight;
                    else score -= 10 * distanceWeight;
                }
                case BLOCK_BOTH -> score -= 150 * distanceWeight; // 必须切换
                case TRAIN -> score -= 200 * distanceWeight; // 火车最危险
                case COIN -> score += 15 * distanceWeight; // 金币奖励
                case COIN_LINE -> score += 30 * distanceWeight; // 金币线更值
                case MAGNET -> score += 50 * distanceWeight; // 道具高价值
                case BOOST -> score += 60 * distanceWeight;
            }
        }

        return score;
    }

    /**
     * 贪心决策:选择立即收益最高的动作
     */
    public Action greedyDecision() {
        Action bestAction = Action.STAY;
        double bestScore = Double.NEGATIVE_INFINITY;

        for (Action action : Action.values()) {
            if (!isActionValid(action)) continue;

            double score = evaluateAction(action);
            if (score > bestScore) {
                bestScore = score;
                bestAction = action;
            }
        }

        return bestAction;
    }

    /**
     * 评估执行某个动作后的预期收益
     */
    private double evaluateAction(Action action) {
        Track targetTrack = state.currentTrack;
        boolean willJump = state.isJumping;
        boolean willRoll = state.isRolling;

        switch (action) {
            case LEFT -> targetTrack = getLeftTrack(state.currentTrack);
            case RIGHT -> targetTrack = getRightTrack(state.currentTrack);
            case JUMP -> willJump = true;
            case ROLL -> willRoll = true;
            case STAY -> {} // 保持当前
        }

        if (targetTrack == null) return Double.NEGATIVE_INFINITY;

        // 基础轨道评分
        double score = evaluateTrack(targetTrack, VISUAL_RANGE);

        // 动作额外评分
        if (action == Action.JUMP && !state.isJumping) {
            // 跳跃可以越过近处的低障碍
            score += handleJumpBonus(targetTrack);
        }
        if (action == Action.ROLL && !state.isRolling) {
            // 翻滚可以穿过近处的高障碍
            score += handleRollBonus(targetTrack);
        }

        // 避免频繁切换轨道(动作连贯性奖励)
        if (action == Action.STAY) score += 2;

        return score;
    }

    /**
     * 跳跃的额外收益:评估可以越过的障碍物
     */
    private double handleJumpBonus(Track track) {
        double bonus = 0;
        List<Cell> cells = state.visibleCells.get(track);
        for (Cell cell : cells) {
            if (cell.z >= 1 && cell.z <= JUMP_DURATION && cell.type == ObstacleType.BLOCK_LOW) {
                bonus += 50; // 成功越过低障碍
            }
        }
        return bonus;
    }

    /**
     * 翻滚的额外收益
     */
    private double handleRollBonus(Track track) {
        double bonus = 0;
        List<Cell> cells = state.visibleCells.get(track);
        for (Cell cell : cells) {
            if (cell.z >= 1 && cell.z <= ROLL_DURATION && cell.type == ObstacleType.BLOCK_HIGH) {
                bonus += 50;
            }
        }
        return bonus;
    }

    private boolean isActionValid(Action action) {
        if (state.isJumping && (action == Action.JUMP || action == Action.ROLL)) return false;
        if (state.isRolling && (action == Action.ROLL || action == Action.JUMP)) return false;
        if (action == Action.LEFT && state.currentTrack == Track.LEFT) return false;
        if (action == Action.RIGHT && state.currentTrack == Track.RIGHT) return false;
        return true;
    }

    private Track getLeftTrack(Track t) {
        return switch (t) {
            case LEFT -> null;
            case CENTER -> Track.LEFT;
            case RIGHT -> Track.CENTER;
        };
    }

    private Track getRightTrack(Track t) {
        return switch (t) {
            case LEFT -> Track.CENTER;
            case CENTER -> Track.RIGHT;
            case RIGHT -> null;
        };
    }
}

动态规划路径规划

贪心策略只考虑一步收益,在复杂场景下可能陷入局部最优。动态规划可以规划未来多步的最优动作序列。

public class SubwaySurfEngine {

    // DP状态:(帧数, 轨道, 是否在跳跃, 是否在翻滚)
    static class DPState {
        int frame;
        Track track;
        boolean jumping;
        boolean rolling;

        DPState(int f, Track t, boolean j, boolean r) {
            this.frame = f; this.track = t;
            this.jumping = j; this.rolling = r;
        }

        @Override
        public boolean equals(Object o) {
            if (this == o) return true;
            if (!(o instanceof DPState)) return false;
            DPState other = (DPState) o;
            return frame == other.frame && track == other.track 
                && jumping == other.jumping && rolling == other.rolling;
        }

        @Override
        public int hashCode() {
            return Objects.hash(frame, track, jumping, rolling);
        }
    }

    /**
     * 使用动态规划规划未来PLANNING_HORIZON步的最优动作序列
     */
    public List<Action> planOptimalPath(int planningHorizon) {
        // dp[state] = 从该状态出发的最大未来收益
        Map<DPState, Double> dp = new HashMap<>();
        Map<DPState, Action> policy = new HashMap<>();

        // 逆向DP:从最后一步往前推
        for (int f = planningHorizon; f >= 0; f--) {
            for (Track t : Track.values()) {
                for (boolean j : new boolean[]{false, true}) {
                    for (boolean r : new boolean[]{false, true}) {
                        if (j && r) continue; // 不能同时跳跃和翻滚

                        DPState s = new DPState(f, t, j, r);
                        if (f == planningHorizon) {
                            dp.put(s, 0.0);
                            continue;
                        }

                        double bestValue = Double.NEGATIVE_INFINITY;
                        Action bestAction = Action.STAY;

                        for (Action action : getValidActions(t, j, r)) {
                            DPState next = simulateAction(s, action);
                            if (next == null) continue;

                            double immediate = getImmediateReward(s, action);
                            double future = dp.getOrDefault(next, 0.0);
                            double total = immediate + future * 0.95; // 折扣因子

                            if (total > bestValue) {
                                bestValue = total;
                                bestAction = action;
                            }
                        }

                        dp.put(s, bestValue);
                        policy.put(s, bestAction);
                    }
                }
            }
        }

        // 回溯构造最优动作序列
        List<Action> plan = new ArrayList<>();
        DPState current = new DPState(0, state.currentTrack, state.isJumping, state.isRolling);

        for (int f = 0; f < planningHorizon; f++) {
            Action action = policy.get(current);
            if (action == null) break;
            plan.add(action);
            current = simulateAction(current, action);
        }

        return plan;
    }

    /**
     * 模拟执行一个动作,返回下一状态
     */
    private DPState simulateAction(DPState s, Action action) {
        Track newTrack = s.track;
        boolean newJump = s.jumping;
        boolean newRoll = s.rolling;
        int newJumpFrame = s.jumping ? 1 : 0;
        int newRollFrame = s.rolling ? 1 : 0;

        switch (action) {
            case LEFT -> {
                newTrack = getLeftTrack(s.track);
                if (newTrack == null) return null;
            }
            case RIGHT -> {
                newTrack = getRightTrack(s.track);
                if (newTrack == null) return null;
            }
            case JUMP -> {
                if (s.jumping || s.rolling) return null;
                newJump = true;
            }
            case ROLL -> {
                if (s.rolling || s.jumping) return null;
                newRoll = true;
            }
            case STAY -> {}
        }

        // 更新跳跃/翻滚状态
        if (s.jumping && newJumpFrame >= JUMP_DURATION) newJump = false;
        if (s.rolling && newRollFrame >= ROLL_DURATION) newRoll = false;

        // 碰撞检测
        if (isColliding(newTrack, s.frame + 1, newJump, newRoll)) return null;

        return new DPState(s.frame + 1, newTrack, newJump, newRoll);
    }

    /**
     * 碰撞检测
     */
    private boolean isColliding(Track track, int frame, boolean jumping, boolean rolling) {
        List<Cell> cells = state.visibleCells.get(track);
        for (Cell cell : cells) {
            if (cell.z != frame) continue;

            switch (cell.type) {
                case BLOCK_LOW -> { if (!jumping) return true; }
                case BLOCK_HIGH -> { if (!rolling) return true; }
                case BLOCK_BOTH, TRAIN -> return true; // 无法通过动作躲避
                default -> {}
            }
        }
        return false;
    }

    /**
     * 计算立即奖励
     */
    private double getImmediateReward(DPState s, Action action) {
        Track track = s.track;
        if (action == Action.LEFT) track = getLeftTrack(track);
        if (action == Action.RIGHT) track = getRightTrack(track);

        double reward = 1; // 存活奖励
        List<Cell> cells = state.visibleCells.get(track);
        for (Cell cell : cells) {
            if (cell.z == s.frame + 1) {
                switch (cell.type) {
                    case COIN -> reward += 10;
                    case COIN_LINE -> reward += 20;
                    case MAGNET -> reward += 30;
                    case BOOST -> reward += 40;
                    default -> {}
                }
            }
        }
        return reward;
    }

    private List<Action> getValidActions(Track t, boolean j, boolean r) {
        List<Action> valid = new ArrayList<>();
        valid.add(Action.STAY);
        if (!j && !r) {
            valid.add(Action.JUMP);
            valid.add(Action.ROLL);
        }
        if (t != Track.LEFT) valid.add(Action.LEFT);
        if (t != Track.RIGHT) valid.add(Action.RIGHT);
        return valid;
    }
}

障碍物生成与视野更新

public class SubwaySurfEngine {

    private final Random random = new Random();

    /**
     * 生成新的视野内容(模拟游戏世界向前滚动)
     */
    public void advanceFrame() {
        // 更新跳跃/翻滚状态
        if (state.isJumping) {
            state.jumpFrame++;
            if (state.jumpFrame >= JUMP_DURATION) {
                state.isJumping = false;
                state.jumpFrame = 0;
            }
        }

        // 所有障碍物向前移动一格
        for (List<Cell> cells : state.visibleCells.values()) {
            cells.forEach(c -> c.z--);
            cells.removeIf(c -> c.z < 0);
        }

        // 在视野远端生成新内容
        generateNewContent(VISUAL_RANGE);

        // 速度逐渐提升
        state.speed = Math.min(state.speed + 1, 300);
    }

    /**
     * 在指定距离生成新障碍物/金币
     */
    private void generateNewContent(int z) {
        // 确保至少有一条安全轨道
        boolean[] safe = {true, true, true};

        for (int i = 0; i < 3; i++) {
            Track track = Track.values()[i];
            double obstacleProb = 0.3 + state.speed / 1000.0; // 速度越快障碍越多

            if (random.nextDouble() < obstacleProb) {
                ObstacleType type = generateObstacleType();
                state.visibleCells.get(track).add(new Cell(type, z, track));
                safe[i] = (type == ObstacleType.NONE);
            } else if (random.nextDouble() < 0.4) {
                // 生成金币
                ObstacleType coinType = random.nextBoolean() ? ObstacleType.COIN : ObstacleType.COIN_LINE;
                state.visibleCells.get(track).add(new Cell(coinType, z, track));
            }
        }

        // 如果三条轨道都有致命障碍,随机清除一条
        if (!safe[0] && !safe[1] && !safe[2]) {
            int clearTrack = random.nextInt(3);
            state.visibleCells.get(Track.values()[clearTrack]).removeIf(c -> c.z == z);
        }
    }

    private ObstacleType generateObstacleType() {
        double r = random.nextDouble();
        if (r < 0.4) return ObstacleType.BLOCK_LOW;
        if (r < 0.7) return ObstacleType.BLOCK_HIGH;
        if (r < 0.85) return ObstacleType.BLOCK_BOTH;
        return ObstacleType.TRAIN;
    }
}

运行演示

public class SubwaySurfDemo {
    public static void main(String[] args) {
        SubwaySurfEngine engine = new SubwaySurfEngine();

        System.out.println("=== 地铁跑酷AI演示 ===");
        int frames = 0;
        int totalCoins = 0;

        while (frames < 1000) { // 模拟1000帧
            frames++;

            // AI决策
            SubwaySurfEngine.Action action = engine.greedyDecision();
            System.out.printf("帧%4d: 动作=%s | ", frames, action);

            // 执行动作并前进
            engine.executeAction(action);
            engine.advanceFrame();

            // 统计
            totalCoins += engine.getCoinsCollectedThisFrame();

            if (engine.isGameOver()) {
                System.out.println("\n游戏结束!存活帧数: " + frames);
                break;
            }
        }

        System.out.println("\n总存活帧数: " + frames);
        System.out.println("收集金币: " + totalCoins);
    }
}

算法复杂度分析

  • 贪心决策:每帧评估5个动作,每个动作扫描视野内约 VISUAL_RANGE 个格子,时间复杂度 O(VISUAL_RANGE × ACTIONS),即 O(150) 每帧,完全满足实时性要求。
  • 动态规划路径规划:状态空间为 planningHorizon × tracks × jumpStates × rollStates,时间复杂度 O(horizon × 3 × 2 × 2 × 5)。对于 horizon=20,约 1200 次操作,可每几帧执行一次更新长期策略。
  • 碰撞检测:每帧每个轨道检查前方一格, O(tracks)。

扩展方向

  1. 强化学习升级:使用PPO或DQN训练端到端策略网络,学习超越规则引擎的复杂操作
  2. 多目标优化:同时优化存活时间、金币收集、道具使用效率,使用NSGA-II求解帕累托前沿
  3. 预测模型:基于LSTM预测障碍物的生成模式,提前规划数秒后的路径
  4. 物理精细模拟:引入真实的重力、惯性和碰撞体积,提升AI操作的精确度