每日算法 — 使用java实现恐龙跳跳:平台跳跃物理引擎与地形随机生成

引言

恐龙跳跳(类似Chrome离线小恐龙游戏)是平台跳跃类游戏的极简代表。其核心算法涉及:物理模拟(重力、跳跃、碰撞)、无限地形生成、障碍物管理。本文用Java从零实现一个完整的平台跳跃物理引擎与程序化地形生成系统。

核心算法设计

1. 物理引擎(Verlet积分)

相比欧拉法,Verlet积分在能量守恒方面更稳定,适合平台跳跃游戏。

import java.util.*;

/**
 * 二维向量
 */
class Vec2 {
    double x, y;
    public Vec2(double x, double y) { this.x = x; this.y = y; }
    public Vec2 add(Vec2 v) { return new Vec2(x + v.x, y + v.y); }
    public Vec2 sub(Vec2 v) { return new Vec2(x - v.x, y - v.y); }
    public Vec2 mul(double s) { return new Vec2(x * s, y * s); }
}

/**
 * 物理对象:恐龙或障碍物
 */
class PhysicsBody {
    Vec2 position;      // 当前位置
    Vec2 prevPosition;  // 上一帧位置(Verlet积分需要)
    Vec2 acceleration;  // 加速度
    double width, height; // 包围盒尺寸
    boolean isStatic;   // 是否静止(如地面、平台)

    public PhysicsBody(Vec2 pos, double w, double h, boolean isStatic) {
        this.position = pos;
        this.prevPosition = pos;
        this.acceleration = new Vec2(0, 0);
        this.width = w;
        this.height = h;
        this.isStatic = isStatic;
    }

    /**
     * Verlet积分更新位置
     * 公式:x(t+Δt) = 2x(t) - x(t-Δt) + a(t)Δt²
     */
    public void updateVerlet(double dt) {
        if (isStatic) return;
        Vec2 velocity = position.sub(prevPosition);
        // 加入阻尼,模拟空气阻力
        velocity = velocity.mul(0.99);
        Vec2 nextPos = position.add(velocity).add(acceleration.mul(dt * dt));
        prevPosition = position;
        position = nextPos;
        acceleration = new Vec2(0, 0); // 重置加速度
    }

    /**
     * AABB轴对齐包围盒碰撞检测
     */
    public boolean intersects(PhysicsBody other) {
        return position.x < other.position.x + other.width &&
               position.x + width > other.position.x &&
               position.y < other.position.y + other.height &&
               position.y + height > other.position.y;
    }

    /**
     * 获取包围盒四条边
     */
    public double left()   { return position.x; }
    public double right()  { return position.x + width; }
    public double top()    { return position.y + height; }
    public double bottom() { return position.y; }
}

/**
 * 平台跳跃物理世界
 */
class PlatformerWorld {
    double gravity = -0.5;     // 重力加速度
    double groundY = 0;        // 地面高度
    List<PhysicsBody> bodies;  // 所有物理对象
    PhysicsBody player;        // 玩家(恐龙)
    double gameSpeed = 3.0;    // 游戏滚动速度

    public PlatformerWorld() {
        this.bodies = new ArrayList<>();
        // 创建玩家:宽20高20,位于(50, 100)
        this.player = new PhysicsBody(new Vec2(50, 100), 20, 20, false);
        this.bodies.add(player);
    }

    /**
     * 添加平台或障碍物
     */
    public void addPlatform(double x, double y, double w, double h) {
        PhysicsBody platform = new PhysicsBody(new Vec2(x, y), w, h, true);
        bodies.add(platform);
    }

    /**
     * 玩家跳跃:施加向上的瞬时速度
     */
    public void jump() {
        // 只有在地面或平台上才能跳跃
        if (isOnGroundOrPlatform()) {
            // Verlet中通过修改prevPosition来模拟瞬时速度
            player.prevPosition = new Vec2(player.position.x, player.position.y + 12);
        }
    }

    /**
     * 检测玩家是否站在地面或平台上
     */
    private boolean isOnGroundOrPlatform() {
        // 地面检测
        if (Math.abs(player.bottom() - groundY) < 1.0) return true;
        // 平台检测:下方是否有平台
        for (PhysicsBody b : bodies) {
            if (b != player && b.isStatic) {
                if (Math.abs(player.bottom() - b.top()) < 2.0 &&
                    player.right() > b.left() && player.left() < b.right()) {
                    return true;
                }
            }
        }
        return false;
    }

    /**
     * 物理帧更新
     */
    public void update(double dt) {
        // 1. 施加重力
        if (!isOnGroundOrPlatform()) {
            player.acceleration = new Vec2(0, gravity);
        }

        // 2. Verlet积分更新所有物体
        for (PhysicsBody b : bodies) {
            b.updateVerlet(dt);
        }

        // 3. 碰撞检测与响应
        resolveCollisions();

        // 4. 地面约束
        if (player.bottom() < groundY) {
            player.position = new Vec2(player.position.x, groundY + player.height);
            player.prevPosition = new Vec2(player.prevPosition.x, player.position.y);
        }

        // 5. 世界滚动:所有静态物体向左移动,模拟恐龙向右跑
        for (PhysicsBody b : bodies) {
            if (b.isStatic) {
                b.position = new Vec2(b.position.x - gameSpeed, b.position.y);
            }
        }

        // 6. 移除超出左边界的地形
        bodies.removeIf(b -> b.isStatic && b.right() < -100);
    }

    /**
     * 碰撞响应:将玩家推出碰撞体
     */
    private void resolveCollisions() {
        for (PhysicsBody b : bodies) {
            if (b == player || !b.isStatic) continue;
            if (player.intersects(b)) {
                // 判断碰撞方向
                double overlapTop = player.top() - b.bottom();    // 从下方撞入
                double overlapBottom = b.top() - player.bottom(); // 落到平台上
                double overlapLeft = player.right() - b.left();   // 从左侧撞入
                double overlapRight = b.right() - player.left();  // 从右侧撞入

                // 找到最小穿透深度,沿该方向推出
                double minOverlap = Math.min(
                    Math.min(overlapTop, overlapBottom),
                    Math.min(overlapLeft, overlapRight)
                );

                if (minOverlap == overlapBottom) {
                    // 站在平台上
                    player.position = new Vec2(player.position.x, b.top() + player.height);
                    player.prevPosition = new Vec2(player.prevPosition.x, player.position.y);
                } else if (minOverlap == overlapTop) {
                    player.position = new Vec2(player.position.x, b.bottom());
                    player.prevPosition = new Vec2(player.prevPosition.x, player.position.y);
                } else if (minOverlap == overlapLeft) {
                    // 撞到平台左侧,游戏结束或反弹
                    player.position = new Vec2(b.left() - player.width, player.position.y);
                } else {
                    player.position = new Vec2(b.right(), player.position.y);
                }
            }
        }
    }
}

2. 程序化地形生成

使用一维Perlin噪声生成连续起伏的平台高度,保证地形可玩性(不会出现无法跳过的间隙)。

/**
 * 一维Perlin噪声生成器
 * 用于生成平滑随机地形
 */
class PerlinNoise1D {
    private final int[] permutation;
    private final int size = 256;

    public PerlinNoise1D(long seed) {
        Random rand = new Random(seed);
        permutation = new int[size * 2];
        for (int i = 0; i < size; i++) permutation[i] = i;
        // Fisher-Yates shuffle
        for (int i = size - 1; i > 0; i--) {
            int j = rand.nextInt(i + 1);
            int tmp = permutation[i];
            permutation[i] = permutation[j];
            permutation[j] = tmp;
        }
        System.arraycopy(permutation, 0, permutation, size, size);
    }

    private double fade(double t) {
        return t * t * t * (t * (t * 6 - 15) + 10); // 6t⁵ - 15t⁴ + 10t³
    }

    private double lerp(double a, double b, double t) {
        return a + t * (b - a);
    }

    private double grad(int hash, double x) {
        return (hash & 1) == 0 ? x : -x;
    }

    public double noise(double x) {
        int X = (int) Math.floor(x) & (size - 1);
        double xf = x - Math.floor(x);
        double u = fade(xf);
        return lerp(grad(permutation[X], xf), grad(permutation[X + 1], xf - 1), u);
    }
}

/**
 * 无限地形生成器
 */
class InfiniteTerrainGenerator {
    PerlinNoise1D heightNoise;
    PerlinNoise1D gapNoise;
    double chunkWidth = 200; // 每块地形宽度
    double lastGeneratedX = 0;
    Random random;

    public InfiniteTerrainGenerator(long seed) {
        this.heightNoise = new PerlinNoise1D(seed);
        this.gapNoise = new PerlinNoise1D(seed + 1);
        this.random = new Random(seed);
    }

    /**
     * 根据游戏世界状态生成新地形块
     * @param world 物理世界
     * @param screenRight 屏幕右边界
     */
    public void generateAhead(PlatformerWorld world, double screenRight) {
        while (lastGeneratedX < screenRight + 500) {
            generateChunk(world, lastGeneratedX);
            lastGeneratedX += chunkWidth;
        }
    }

    private void generateChunk(PlatformerWorld world, double startX) {
        double baseHeight = 50; // 基础地面高度
        double noiseScale = 0.01;

        // 使用Perlin噪声生成地形高度
        double n = heightNoise.noise(startX * noiseScale);
        double platformHeight = baseHeight + n * 40; // 高度范围:10-90

        // 随机决定是否生成平台(连续平台不生成间隙)
        double gapValue = gapNoise.noise(startX * noiseScale * 2);
        if (gapValue > 0.3) {
            // 生成平台
            double platformWidth = 80 + random.nextDouble() * 80;
            world.addPlatform(startX, platformHeight, platformWidth, 10);
        } else {
            // 生成地面(避免过大间隙,保证可跳性)
            world.addPlatform(startX, baseHeight - 20, chunkWidth, 20);
        }

        // 随机生成低矮障碍物(仙人掌类)
        if (random.nextDouble() < 0.3) {
            double obsX = startX + 20 + random.nextDouble() * (chunkWidth - 40);
            double obsHeight = 15 + random.nextDouble() * 15;
            world.addPlatform(obsX, baseHeight - 20 + obsHeight, 10, obsHeight);
        }
    }
}

3. 跳跃AI决策

让AI自动判断何时跳跃。

/**
 * 简单的跳跃AI:基于前方障碍物检测决定跳跃时机
 */
class JumpAI {
    double detectionRange = 80; // 前方检测距离
    double minJumpDistance = 30; // 最小跳跃触发距离

    /**
     * 决策是否跳跃
     * @param player 玩家对象
     * @param bodies 世界中所有物体
     * @return true表示应该跳跃
     */
    public boolean shouldJump(PhysicsBody player, List<PhysicsBody> bodies) {
        double playerBottom = player.bottom();
        double playerRight = player.right();

        for (PhysicsBody b : bodies) {
            if (b.isStatic && b != player) {
                // 检测前方是否有高于地面的障碍物
                if (b.left() > playerRight && b.left() < playerRight + detectionRange) {
                    if (b.top() > playerBottom + 5) { // 障碍物高度超过脚底
                        // 判断距离是否进入跳跃区间
                        double dist = b.left() - playerRight;
                        if (dist <= minJumpDistance + 10 && dist >= minJumpDistance - 5) {
                            return true;
                        }
                    }
                }
            }
        }
        return false;
    }
}

演示程序

public class DinoJumpDemo {
    public static void main(String[] args) {
        PlatformerWorld world = new PlatformerWorld();
        InfiniteTerrainGenerator terrain = new InfiniteTerrainGenerator(12345);
        JumpAI ai = new JumpAI();

        // 初始化地形
        terrain.generateAhead(world, 800);

        System.out.println("=== 恐龙跳跳模拟 ===");
        double dt = 0.5; // 时间步长
        for (int frame = 0; frame < 50; frame++) {
            // AI决策
            if (ai.shouldJump(world.player, world.bodies)) {
                world.jump();
                System.out.printf("[帧%d] 跳跃! 高度:%.1f\n", frame, world.player.position.y);
            }

            // 更新物理
            world.update(dt);
            terrain.generateAhead(world, world.player.position.x + 800);

            // 输出状态
            System.out.printf("帧%d: 恐龙位置(%.1f, %.1f), 地面物体数:%d\n",
                frame, world.player.position.x, world.player.position.y, world.bodies.size());

            // 检测是否撞到障碍物(简化:高度过低表示掉落)
            if (world.player.position.y < -100) {
                System.out.println("恐龙掉落,游戏结束!");
                break;
            }
        }
    }
}

复杂度分析

  • Verlet积分:每帧O(n),n为物体数量
  • 碰撞检测:O(n²),使用空间划分可优化至O(n)
  • Perlin噪声:O(1)每采样点
  • 地形生成:O(m)每帧,m为新增平台数

总结

本文从物理模拟、碰撞检测、程序化生成三个维度构建平台跳跃游戏核心。Verlet积分提供稳定的物理模拟;AABB碰撞检测高效处理平台交互;Perlin噪声保证地形既随机又可玩。读者可在此基础上扩展双倍跳跃、斜坡滑动、敌人AI等高级特性。