每日算法 — 使用java实现神庙逃亡:无限地形生成与贪心避障路径规划

引言

神庙逃亡(Temple Run)是移动端最具影响力的跑酷游戏之一。玩家控制角色在无尽的道路上前奔,通过左右滑动和跳跃躲避障碍物。从算法角度看,神庙逃亡的核心挑战在于无限地形生成与实时避障决策。本文将用Java实现程序化地形生成算法与贪心避障路径规划,探索跑酷游戏背后的算法原理。

问题建模

神庙逃亡可抽象为以下算法问题:

  1. 无限地形生成:在有限内存中生成看似无限的多样化地形,包括道路弯曲、障碍物分布、金币位置
  2. 障碍物分类:矮障碍(可滑过)、高障碍(需跳跃)、左右墙壁(需转向)
  3. 实时避障:角色以恒定速度前进,AI需要在极短时间内决策下一个动作(左/右/跳/滑)
  4. 路径评分:评估每条可能路径的安全性(无障碍)与收益性(收集金币)

核心算法设计

1. 程序化地形生成

使用分块生成策略:将道路划分为固定长度的段(segment),每段包含:
– 道路类型(直道、左弯、右弯)
– 三条跑道上的障碍物分布
– 金币排列模式

使用加权随机选择下一段的类型,确保游戏难度渐进提升。

2. 前瞻窗口

由于角色高速移动,AI需要维护一个前瞻窗口(lookahead window),预加载前方N段的障碍物信息。窗口随角色前进而滑动更新。

3. 贪心避障策略

每个决策时刻,AI评估当前三条跑道在接下来M段内的:
– 危险度:该路径上是否有无法避开的障碍物
– 金币收益:该路径上可收集的金币数量
– 灵活性:该路径在后续段的转向选择空间

选择综合评分最高的跑道,并在需要时触发跳跃/滑动。

Java完整实现

import java.util.*;

/**
 * 神庙逃亡:无限地形生成与贪心避障路径规划
 */
public class TempleRunAI {

    // 游戏参数
    private static final int LANES = 3;           // 跑道数量(左中右)
    private static final int SEGMENT_LENGTH = 10; // 每段道路长度(单位)
    private static final int LOOKAHEAD = 15;      // 前瞻段数
    private static final int JUMP_DURATION = 3;   // 跳跃持续时间(段数)
    private static final int SLIDE_DURATION = 3;  // 滑动持续时间(段数)

    private Random random;
    private List<Segment> terrain; // 已生成的地形
    private int currentSegment;    // 当前所在段

    // 动作枚举
    enum Action { LEFT, RIGHT, JUMP, SLIDE, NONE }

    // 障碍物类型
    enum ObstacleType { NONE, LOW, HIGH, LEFT_WALL, RIGHT_WALL, COIN }

    public TempleRunAI() {
        this.random = new Random();
        this.terrain = new ArrayList<>();
        this.currentSegment = 0;
        generateInitialTerrain();
    }

    /**
     * 道路段:包含LANES个跑道上的障碍物
     */
    static class Segment {
        ObstacleType[] lanes;      // 每条跑道上的障碍物
        int roadType;              // 0=直道, 1=左弯, 2=右弯
        int difficulty;            // 难度等级

        Segment(int difficulty) {
            this.lanes = new ObstacleType[LANES];
            this.difficulty = difficulty;
            generateContent();
        }

        /**
         * 根据难度生成障碍物分布
         */
        void generateContent() {
            // 基础:每条跑道有概率出现障碍
            for (int i = 0; i < LANES; i++) {
                lanes[i] = ObstacleType.NONE;
            }

            // 难度越高,障碍越多
            double obstacleProb = 0.2 + difficulty * 0.05;

            for (int i = 0; i < LANES; i++) {
                if (random.nextDouble() < obstacleProb) {
                    lanes[i] = generateRandomObstacle();
                }
            }

            // 确保至少有一条安全跑道
            boolean hasSafe = false;
            for (ObstacleType o : lanes) {
                if (o == ObstacleType.NONE || o == ObstacleType.COIN) hasSafe = true;
            }
            if (!hasSafe) {
                lanes[random.nextInt(LANES)] = ObstacleType.NONE;
            }

            // 随机添加金币(在空白跑道上)
            for (int i = 0; i < LANES; i++) {
                if (lanes[i] == ObstacleType.NONE && random.nextDouble() < 0.4) {
                    lanes[i] = ObstacleType.COIN;
                }
            }

            // 道路类型
            roadType = random.nextInt(3);
        }

        ObstacleType generateRandomObstacle() {
            int r = random.nextInt(10);
            if (r < 3) return ObstacleType.LOW;       // 矮障碍,需滑动
            if (r < 6) return ObstacleType.HIGH;      // 高障碍,需跳跃
            if (r < 8) return ObstacleType.LEFT_WALL; // 左墙
            return ObstacleType.RIGHT_WALL;           // 右墙
        }
    }

    /**
     * 玩家状态
     */
    static class Player {
        int lane;           // 当前跑道 0-2
        int segment;        // 当前段
        int inSegmentPos;   // 在当前段中的位置
        boolean isJumping;  // 是否处于跳跃状态
        boolean isSliding;  // 是否处于滑动状态
        int actionTimer;    // 动作剩余持续时间
        int score;          // 得分
        int coins;          // 金币数
        boolean alive;      // 是否存活

        Player() {
            this.lane = 1;  // 中间跑道起步
            this.segment = 0;
            this.inSegmentPos = 0;
            this.isJumping = false;
            this.isSliding = false;
            this.actionTimer = 0;
            this.score = 0;
            this.coins = 0;
            this.alive = true;
        }
    }

    /**
     * 初始化地形
     */
    void generateInitialTerrain() {
        for (int i = 0; i < LOOKAHEAD * 2; i++) {
            terrain.add(new Segment(i / 10)); // 难度随距离增加
        }
    }

    /**
     * 动态扩展地形
     */
    void extendTerrain() {
        while (terrain.size() < currentSegment + LOOKAHEAD * 2) {
            terrain.add(new Segment(terrain.size() / 10));
        }
    }

    /**
     * 检查玩家在指定段和跑道上是否会碰撞
     */
    boolean wouldCollide(Player p, int segment, int lane) {
        if (segment >= terrain.size()) return false;
        Segment seg = terrain.get(segment);
        ObstacleType obs = seg.lanes[lane];

        switch (obs) {
            case NONE:
            case COIN:
                return false;
            case LOW:
                // 矮障碍:跳跃可过,滑动不可过(撞到)
                return !p.isJumping;
            case HIGH:
                // 高障碍:滑动可过,跳跃不可过(撞到)
                return !p.isSliding;
            case LEFT_WALL:
                // 左墙:只能在最右跑道通过
                return lane != LANES - 1;
            case RIGHT_WALL:
                // 右墙:只能在最左跑道通过
                return lane != 0;
            default:
                return false;
        }
    }

    /**
     * 贪心评估:评估从现在起走某条跑道的安全性和收益
     */
    double evaluatePath(Player p, int startLane, int lookAheadSegments) {
        double safetyScore = 0;
        double coinScore = 0;
        int currentLane = startLane;

        for (int i = 0; i < lookAheadSegments; i++) {
            int segIdx = p.segment + i;
            if (segIdx >= terrain.size()) break;

            Segment seg = terrain.get(segIdx);

            // 检查当前跑道是否有障碍
            if (wouldCollide(p, segIdx, currentLane)) {
                safetyScore -= 100 * (lookAheadSegments - i); // 越近的障碍扣分越多
                break; // 此路不通
            }

            // 金币奖励
            if (seg.lanes[currentLane] == ObstacleType.COIN) {
                coinScore += 10;
            }

            // 基础安全分
            safetyScore += 5;

            // 检查左右跑道是否有障碍(影响未来变道灵活性)
            int safeNeighbors = 0;
            if (currentLane > 0 && !wouldCollide(p, segIdx, currentLane - 1)) safeNeighbors++;
            if (currentLane < LANES - 1 && !wouldCollide(p, segIdx, currentLane + 1)) safeNeighbors++;
            safetyScore += safeNeighbors * 2; // 邻居安全加分
        }

        return safetyScore + coinScore;
    }

    /**
     * 选择最佳动作
     */
    Action selectBestAction(Player p) {
        extendTerrain();

        // 如果当前跑道有即将碰撞的危险,优先处理
        if (wouldCollide(p, p.segment + 1, p.lane) || wouldCollide(p, p.segment + 2, p.lane)) {
            ObstacleType upcoming = terrain.get(p.segment + 1).lanes[p.lane];

            // 尝试跳跃或滑动通过
            if (upcoming == ObstacleType.LOW && !p.isJumping) {
                return Action.JUMP;
            }
            if (upcoming == ObstacleType.HIGH && !p.isSliding) {
                return Action.SLIDE;
            }

            // 尝试变道
            double leftScore = (p.lane > 0) ? evaluatePath(p, p.lane - 1, LOOKAHEAD) : -9999;
            double rightScore = (p.lane < LANES - 1) ? evaluatePath(p, p.lane + 1, LOOKAHEAD) : -9999;
            double stayScore = evaluatePath(p, p.lane, LOOKAHEAD);

            if (leftScore > stayScore && leftScore > rightScore) return Action.LEFT;
            if (rightScore > stayScore && rightScore > leftScore) return Action.RIGHT;
        }

        // 没有紧急危险,评估变道是否更优(更多金币/更好路线)
        double currentScore = evaluatePath(p, p.lane, LOOKAHEAD);

        if (p.lane > 0) {
            double leftScore = evaluatePath(p, p.lane - 1, LOOKAHEAD);
            if (leftScore > currentScore + 20) return Action.LEFT;
        }
        if (p.lane < LANES - 1) {
            double rightScore = evaluatePath(p, p.lane + 1, LOOKAHEAD);
            if (rightScore > currentScore + 20) return Action.RIGHT;
        }

        return Action.NONE;
    }

    /**
     * 执行动作并更新玩家状态
     */
    void executeAction(Player p, Action action) {
        switch (action) {
            case LEFT:
                if (p.lane > 0) p.lane--;
                break;
            case RIGHT:
                if (p.lane < LANES - 1) p.lane++;
                break;
            case JUMP:
                p.isJumping = true;
                p.actionTimer = JUMP_DURATION;
                break;
            case SLIDE:
                p.isSliding = true;
                p.actionTimer = SLIDE_DURATION;
                break;
            case NONE:
                break;
        }
    }

    /**
     * 更新游戏状态(每帧调用)
     */
    void update(Player p) {
        if (!p.alive) return;

        // 更新动作计时器
        if (p.actionTimer > 0) {
            p.actionTimer--;
            if (p.actionTimer == 0) {
                p.isJumping = false;
                p.isSliding = false;
            }
        }

        // 前进
        p.inSegmentPos++;
        if (p.inSegmentPos >= SEGMENT_LENGTH) {
            p.inSegmentPos = 0;
            p.segment++;
            extendTerrain();
        }

        // 检查碰撞
        if (wouldCollide(p, p.segment, p.lane)) {
            p.alive = false;
            System.out.println("Game Over! Segment: " + p.segment + ", Score: " + p.score);
        }

        // 收集金币
        if (terrain.get(p.segment).lanes[p.lane] == ObstacleType.COIN) {
            p.coins++;
            p.score += 10;
            terrain.get(p.segment).lanes[p.lane] = ObstacleType.NONE; // 金币只能收集一次
        }

        p.score++;
    }

    /**
     * 运行AI模拟
     */
    void runSimulation(int maxSteps) {
        Player p = new Player();

        System.out.println("=== 神庙逃亡AI模拟开始 ===");

        for (int step = 0; step < maxSteps && p.alive; step++) {
            // AI决策
            Action action = selectBestAction(p);
            if (action != Action.NONE) {
                executeAction(p, action);
            }

            // 更新状态
            update(p);

            // 每100步输出状态
            if (step % 100 == 0) {
                System.out.printf("Step %d: Segment=%d, Lane=%d, Score=%d, Coins=%d%n",
                    step, p.segment, p.lane, p.score, p.coins);
            }
        }

        System.out.println("=== 模拟结束 ===");
        System.out.println("最终得分: " + p.score);
        System.out.println("收集金币: " + p.coins);
        System.out.println("到达段数: " + p.segment);
    }

    public static void main(String[] args) {
        TempleRunAI game = new TempleRunAI();
        game.runSimulation(5000);
    }
}

复杂度分析

操作 时间复杂度 空间复杂度 说明
地形生成 O(L) O(L) L为已生成段数,通常保持常数
碰撞检测 O(1) O(1) 查表操作
路径评估 O(K × L) O(1) K为前瞻段数,L为跑道数
动作选择 O(L² × K) O(1) 评估所有可能跑道

算法优化方向

  1. 动态规划路径规划:对于更复杂的障碍物组合,可使用DP计算最优动作序列
  2. 学习型障碍物预测:通过历史数据预测障碍物出现模式
  3. 物理模拟:加入重力、惯性等物理效果,使跳跃和滑动更真实
  4. 多线程地形生成:在后台线程预生成地形,避免主线程卡顿

总结

神庙逃亡的算法核心在于无限地形生成与实时避障决策。通过分块生成策略,我们在有限内存中创造了无尽的游戏世界;通过贪心路径评估,AI能够在高速移动中做出安全的避障决策。这套算法框架不仅适用于跑酷游戏,也可扩展到自动驾驶路径规划、机器人避障等实际应用领域。