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

引言

跑酷(Parkour)类游戏要求角色在高速移动中实时躲避障碍物、收集道具并选择最优路径。本文将用Java实现一个简化版跑酷游戏的AI决策系统,核心挑战在于:面对连续生成的障碍物序列,如何通过贪心策略与动态规划计算角色的最优跳跃/滑行动作序列,最大化生存距离与得分。

问题建模

将跑酷游戏抽象为离散时间步的决策问题:

  • 跑道分为上、中、下三条轨道
  • 每个时间步,角色可以选择:跳跃(躲避地面障碍)、滑行(躲避空中障碍)、左右移动(切换轨道)、什么都不做
  • 障碍物以固定速度向角色靠近
  • 角色有能量限制,连续动作消耗更多能量

贪心策略设计

基础贪心策略遵循以下优先级:

  1. 生存优先:如果当前轨道前方有不可躲避的障碍,立即切换轨道
  2. 道具优先:如果相邻轨道有得分道具且无风险,优先移动收集
  3. 能量管理:能量低于阈值时,减少非必要动作

Java实现

import java.util.*;

/**
 * 跑酷游戏AI决策系统
 * 基于贪心策略与前瞻搜索实现动态路径规划
 */
public class ParkourAI {

    // 跑道常量
    public static final int LANES = 3; // 上中下三条轨道
    public static final int NONE = 0;
    public static final int GROUND_OBSTACLE = 1; // 需跳跃躲避
    public static final int AIR_OBSTACLE = 2;    // 需滑行躲避
    public static final int COIN = 3;            // 金币道具
    public static final int POWER_UP = 4;        // 能量道具

    // 动作枚举
    enum Action { STAY, JUMP, SLIDE, MOVE_LEFT, MOVE_RIGHT }

    /**
     * 游戏状态类
     */
    static class GameState {
        int[][] track; // 跑道网格,track[lane][distance]
        int playerLane; // 角色当前轨道 0-2
        int playerPos;  // 角色当前位置
        int energy;     // 当前能量 0-100
        int score;      // 当前得分

        public GameState(int[][] track, int lane, int pos, int energy, int score) {
            this.track = track;
            this.playerLane = lane;
            this.playerPos = pos;
            this.energy = energy;
            this.score = score;
        }
    }

    /**
     * 检查指定位置是否安全(无障碍物)
     */
    public static boolean isSafe(int[][] track, int lane, int pos) {
        if (lane < 0 || lane >= LANES || pos < 0 || pos >= track[0].length) {
            return false;
        }
        int cell = track[lane][pos];
        return cell != GROUND_OBSTACLE && cell != AIR_OBSTACLE;
    }

    /**
     * 执行动作并返回新状态(不修改原状态)
     */
    public static GameState applyAction(GameState state, Action action) {
        int newLane = state.playerLane;
        int newPos = state.playerPos + 1; // 每步自动前进
        int newEnergy = state.energy - 1; // 基础能量消耗
        int newScore = state.score;

        switch (action) {
            case JUMP:
                newEnergy -= 2;
                // 跳跃时忽略地面障碍,但仍受空中障碍影响
                break;
            case SLIDE:
                newEnergy -= 2;
                // 滑行时忽略空中障碍,但仍受地面障碍影响
                break;
            case MOVE_LEFT:
                newLane--;
                newEnergy -= 3;
                break;
            case MOVE_RIGHT:
                newLane++;
                newEnergy -= 3;
                break;
            case STAY:
                break;
        }

        // 能量下限
        newEnergy = Math.max(0, newEnergy);

        // 碰撞检测
        boolean alive = true;
        if (newLane >= 0 && newLane < LANES && newPos < state.track[0].length) {
            int cell = state.track[newLane][newPos];
            if (action == Action.JUMP && cell == AIR_OBSTACLE) {
                alive = false; // 跳跃撞到空中障碍
            } else if (action == Action.SLIDE && cell == GROUND_OBSTACLE) {
                alive = false; // 滑行撞到地面障碍
            } else if (action != Action.JUMP && cell == GROUND_OBSTACLE) {
                alive = false; // 正常状态撞到地面障碍
            } else if (action != Action.SLIDE && cell == AIR_OBSTACLE) {
                alive = false; // 正常状态撞到空中障碍
            } else if (cell == COIN) {
                newScore += 10;
            } else if (cell == POWER_UP) {
                newEnergy = Math.min(100, newEnergy + 20);
            }
        }

        if (!alive) {
            newEnergy = 0; // 标记死亡
        }

        // 创建新track(前进一格的效果)
        int[][] newTrack = new int[LANES][state.track[0].length];
        for (int i = 0; i < LANES; i++) {
            System.arraycopy(state.track[i], 0, newTrack[i], 0, state.track[0].length);
        }

        return new GameState(newTrack, newLane, newPos, newEnergy, newScore);
    }

    /**
     * 贪心决策:基于当前状态选择最优动作
     * @param state 当前游戏状态
     * @param lookahead 前瞻步数
     * @return 推荐动作
     */
    public static Action greedyDecision(GameState state, int lookahead) {
        Action bestAction = Action.STAY;
        double bestScore = Double.NEGATIVE_INFINITY;

        for (Action action : Action.values()) {
            double score = evaluateAction(state, action, lookahead);
            if (score > bestScore) {
                bestScore = score;
                bestAction = action;
            }
        }

        return bestAction;
    }

    /**
     * 评估动作得分:综合安全性、收益与能量效率
     */
    private static double evaluateAction(GameState state, Action action, int depth) {
        GameState next = applyAction(state, action);

        // 死亡惩罚
        if (next.energy <= 0 && next.playerPos > state.playerPos) {
            return -10000;
        }

        double score = 0;

        // 基础生存奖励
        score += 100;

        // 得分奖励
        score += (next.score - state.score) * 5;

        // 能量管理奖励
        if (next.energy > 30) {
            score += 10;
        } else if (next.energy < 10) {
            score -= 50; // 低能量警告
        }

        // 前瞻递归评估(简化版)
        if (depth > 0 && next.energy > 0) {
            Action futureBest = greedyDecision(next, depth - 1);
            score += evaluateAction(next, futureBest, depth - 1) * 0.8;
        }

        // 动作成本惩罚
        switch (action) {
            case JUMP: case SLIDE:
                score -= 3;
                break;
            case MOVE_LEFT: case MOVE_RIGHT:
                score -= 5;
                break;
            case STAY:
                score += 2; // 鼓励保持,节省能量
                break;
        }

        return score;
    }

    /**
     * 生成随机跑道
     * @param length 跑道长度
     */
    public static int[][] generateTrack(int length) {
        Random rand = new Random(42); // 固定种子便于复现
        int[][] track = new int[LANES][length];

        for (int d = 5; d < length - 5; d++) {
            // 每条轨道独立生成障碍物
            for (int l = 0; l < LANES; l++) {
                double r = rand.nextDouble();
                if (r < 0.15) {
                    track[l][d] = GROUND_OBSTACLE;
                } else if (r < 0.25) {
                    track[l][d] = AIR_OBSTACLE;
                } else if (r < 0.35) {
                    track[l][d] = COIN;
                } else if (r < 0.38) {
                    track[l][d] = POWER_UP;
                }
            }

            // 确保至少有一条轨道是安全的(避免全堵死)
            boolean allBlocked = true;
            for (int l = 0; l < LANES; l++) {
                if (track[l][d] != GROUND_OBSTACLE && track[l][d] != AIR_OBSTACLE) {
                    allBlocked = false;
                    break;
                }
            }
            if (allBlocked) {
                track[rand.nextInt(LANES)][d] = NONE;
            }
        }

        return track;
    }

    /**
     * 运行完整游戏模拟
     */
    public static void runSimulation(int trackLength, int lookahead) {
        int[][] track = generateTrack(trackLength);
        GameState state = new GameState(track, 1, 0, 100, 0); // 从中路开始

        System.out.printf("跑酷模拟开始:跑道长度%d,前瞻深度%d%n", trackLength, lookahead);

        List<Action> actionHistory = new ArrayList<>();

        while (state.energy > 0 && state.playerPos < trackLength - 1) {
            Action action = greedyDecision(state, lookahead);
            actionHistory.add(action);

            GameState next = applyAction(state, action);

            if (next.energy <= 0 && next.playerPos > state.playerPos) {
                System.out.printf("第%d步:动作%s,角色撞击障碍,游戏结束%n", 
                        state.playerPos, action);
                break;
            }

            state = next;

            if (state.playerPos % 20 == 0) {
                System.out.printf("位置%d:轨道%d,能量%d,得分%d,最近动作%s%n",
                        state.playerPos, state.playerLane, state.energy, 
                        state.score, action);
            }
        }

        System.out.printf("模拟结束:最终位置%d/%d,最终得分%d,存活步数%d%n",
                state.playerPos, trackLength, state.score, actionHistory.size());
        System.out.printf("动作分布:STAY=%d, JUMP=%d, SLIDE=%d, LEFT=%d, RIGHT=%d%n",
                countActions(actionHistory, Action.STAY),
                countActions(actionHistory, Action.JUMP),
                countActions(actionHistory, Action.SLIDE),
                countActions(actionHistory, Action.MOVE_LEFT),
                countActions(actionHistory, Action.MOVE_RIGHT));
    }

    private static long countActions(List<Action> actions, Action target) {
        return actions.stream().filter(a -> a == target).count();
    }

    public static void main(String[] args) {
        // 对比不同前瞻深度的效果
        System.out.println("=== 前瞻深度 1 ===");
        runSimulation(200, 1);

        System.out.println("\n=== 前瞻深度 3 ===");
        runSimulation(200, 3);

        System.out.println("\n=== 前瞻深度 5 ===");
        runSimulation(200, 5);
    }
}

复杂度分析

  • 单次贪心决策:O(A × D) 时间,其中A为动作数(5),D为前瞻深度
  • 完整游戏模拟:O(L × A^D) 时间,L为跑道长度。深度过大会导致指数爆炸
  • 空间复杂度:O(D × L) 用于状态存储

策略优化方向

当前实现采用贪心前瞻,可进一步优化为:

  1. 蒙特卡洛树搜索(MCTS):在决策节点模拟多条路径,选择期望收益最高的分支
  2. 动态规划:预计算每个位置的最优值函数,实现O(1)决策
  3. 神经网络估值:训练深度网络评估游戏状态的长期价值