每日算法 — 使用java实现是男人就下一百层:随机平台生成与安全着陆检测算法

《是男人就下一百层》是经典的街机下落类游戏,玩家操控角色在不断上升的钉板之间向下跳跃,通过左右移动寻找安全的平台间隙。本文用Java实现该游戏的核心引擎,重点讲解随机平台间隙生成算法、安全着陆区域检测以及角色运动状态机管理。

一、游戏机制与算法目标

游戏的核心规则:
– 屏幕顶部有不断下降的钉板,触碰即游戏结束
– 中间分布着带间隙的平台,玩家可从中穿过下落
– 玩家按左右键水平移动,角色自动受重力向下加速
– 穿过平台间隙继续下落得分,触碰平台则安全站立
– 随着层数增加,平台间隙变小、下降速度加快

需要解决的三个核心算法问题:
1. 如何随机生成带安全间隙的平台,确保游戏始终可解
2. 如何精确检测角色与平台的碰撞,区分”站在平台上”和”穿过间隙”
3. 如何设计平滑的难度曲线,使每10层为一个难度里程碑

二、核心算法原理

2.1 平台间隙生成算法

平台由多个”砖块”组成,中间预留一个或多个间隙。生成策略:
– 平台总宽度等于屏幕宽度
– 间隙数量和宽度随层数增加而动态调整
– 保证相邻两层平台的间隙不完全对齐,避免”直落通道”
– 使用种子随机数确保相同关卡的可复现性

2.2 碰撞检测与状态转换

角色有三种运动状态:
– 下落中:受重力加速,可穿过平台间隙
– 站立中:位于平台砖块上,可左右移动
– 起跳中:获得向上初速度,短时间内可向上穿越平台

碰撞检测逻辑:
– 若角色底部与平台砖块顶部相交,且角色正在下落 → 转为站立
– 若角色中心位于平台间隙水平范围内 → 可穿过平台继续下落

2.3 难度递增模型

采用分段线性难度曲线:
– 下降速度:$v_{fall} = v_{base} \cdot (1 + \lfloor level / 10 \rfloor \cdot 0.15)$
– 间隙宽度:$w_{gap} = w_{base} \cdot (1 – \lfloor level / 10 \rfloor \cdot 0.08)$
– 平台生成间隔:$h_{platform} = h_{base} – \lfloor level / 10 \rfloor \cdot 5$

每10层为一个难度段,玩家会明显感受到节奏变化。

三、完整Java实现

import javax.swing.*;
import java.awt.*;
import java.awt.event.*;
import java.util.ArrayList;
import java.util.List;
import java.util.Random;

/**
 * 是男人就下一百层:实现随机平台生成、安全间隙检测与运动状态机
 */
public class HundredFloors extends JPanel implements ActionListener, KeyListener {

    private static final int WIDTH = 500;
    private static final int HEIGHT = 700;
    private static final int PLAYER_WIDTH = 30;
    private static final int PLAYER_HEIGHT = 40;
    private static final double GRAVITY = 0.4;
    private static final double MOVE_SPEED = 5.0;
    private static final double BASE_FALL_SPEED = 1.5;
    private static final int PLATFORM_HEIGHT = 15;
    private static final int BRICK_WIDTH = 40;

    // 玩家状态
    private double playerX = WIDTH / 2.0 - PLAYER_WIDTH / 2.0;
    private double playerY = 100;
    private double playerVx = 0;
    private double playerVy = 0;
    private boolean moveLeft = false;
    private boolean moveRight = false;

    // 运动状态枚举
    enum PlayerState { FALLING, STANDING, JUMPING }
    private PlayerState state = PlayerState.FALLING;

    // 平台管理
    private List<Platform> platforms = new ArrayList<>();
    private double platformSpawnY = 200;
    private Random random = new Random(123);

    // 钉板(顶部死亡区域)
    private double spikeY = -50;
    private double spikeSpeed = 1.0;

    // 游戏状态
    private int floorCount = 0;
    private int score = 0;
    private boolean gameOver = false;
    private boolean gameWon = false;
    private long startTime;

    private Timer timer;

    /**
     * 平台类:由多个砖块组成,中间有间隙
     */
    static class Platform {
        double y;
        List<Brick> bricks = new ArrayList<>();
        boolean passed = false; // 玩家是否已穿过该平台

        Platform(double y, int gapStart, int gapWidth, int difficulty) {
            this.y = y;
            // 生成砖块,跳过间隙区域
            int x = 0;
            while (x < WIDTH) {
                if (x >= gapStart && x < gapStart + gapWidth) {
                    x = gapStart + gapWidth;
                    continue;
                }
                int bw = Math.min(BRICK_WIDTH, WIDTH - x);
                bricks.add(new Brick(x, y, bw, PLATFORM_HEIGHT));
                x += bw;
            }
        }

        boolean containsGapAt(double px) {
            for (Brick b : bricks) {
                if (px >= b.x && px <= b.x + b.width) {
                    return false; // 在砖块上,不是间隙
                }
            }
            return true; // 不在任何砖块上,是间隙
        }
    }

    static class Brick {
        int x, y, width, height;
        Brick(int x, int y, int w, int h) {
            this.x = x; this.y = y; this.width = w; this.height = h;
        }

        boolean intersects(double px, double py, int pw, int ph) {
            return px < x + width && px + pw > x &&
                   py < y + height && py + ph > y;
        }
    }

    public HundredFloors() {
        setPreferredSize(new Dimension(WIDTH, HEIGHT));
        setBackground(new Color(40, 30, 50));
        setFocusable(true);
        addKeyListener(this);
        startTime = System.currentTimeMillis();

        // 生成初始平台
        for (int i = 0; i < 6; i++) {
            generatePlatform(i * 120 + 200);
        }

        timer = new Timer(16, this);
        timer.start();
    }

    /**
     * 根据当前难度生成带间隙的平台
     */
    private void generatePlatform(double y) {
        int difficulty = floorCount / 10; // 每10层提升一次难度

        // 间隙宽度随难度递减,最小保证玩家能穿过
        int minGap = PLAYER_WIDTH + 10;
        int maxGap = Math.max(minGap, 120 - difficulty * 8);
        int gapWidth = minGap + random.nextInt(Math.max(1, maxGap - minGap));

        // 间隙起始位置,确保不与上一层完全对齐(增加挑战性)
        int maxStart = WIDTH - gapWidth;
        int gapStart = random.nextInt(Math.max(1, maxStart));

        // 若上一层存在,确保间隙位置有明显偏移
        if (!platforms.isEmpty()) {
            Platform last = platforms.get(platforms.size() - 1);
            int lastGapCenter = findGapCenter(last);
            if (Math.abs(gapStart + gapWidth / 2 - lastGapCenter) < 80) {
                gapStart = (gapStart + WIDTH / 2) % Math.max(1, maxStart);
            }
        }

        platforms.add(new Platform(y, gapStart, gapWidth, difficulty));
    }

    private int findGapCenter(Platform p) {
        // 简化为取第一个间隙中点
        for (int x = 0; x < WIDTH; x++) {
            if (p.containsGapAt(x)) {
                int end = x;
                while (end < WIDTH && p.containsGapAt(end)) end++;
                return (x + end) / 2;
            }
        }
        return WIDTH / 2;
    }

    /**
     * 核心游戏循环
     */
    @Override
    public void actionPerformed(ActionEvent e) {
        if (gameOver || gameWon) return;

        int difficulty = floorCount / 10;

        // 1. 钉板持续下降
        spikeSpeed = BASE_FALL_SPEED * (1 + difficulty * 0.15);
        spikeY += spikeSpeed;

        // 2. 玩家水平移动
        if (moveLeft) playerVx = -MOVE_SPEED;
        else if (moveRight) playerVx = MOVE_SPEED;
        else playerVx = 0;
        playerX += playerVx;
        playerX = Math.max(0, Math.min(WIDTH - PLAYER_WIDTH, playerX));

        // 3. 玩家垂直运动(重力)
        if (state == PlayerState.FALLING || state == PlayerState.JUMPING) {
            playerVy += GRAVITY;
        }
        playerY += playerVy;

        // 4. 平台碰撞检测与状态机转换
        boolean onPlatform = false;
        for (Platform p : platforms) {
            // 检测玩家是否站在平台上(底部碰撞)
            if (state != PlayerState.JUMPING && playerVy >= 0) {
                double playerBottom = playerY + PLAYER_HEIGHT;
                double playerCenterX = playerX + PLAYER_WIDTH / 2.0;

                // 玩家底部接近平台顶部,且水平方向有重叠
                if (Math.abs(playerBottom - p.y) < 10 && 
                    playerBottom <= p.y + PLATFORM_HEIGHT &&
                    !p.containsGapAt(playerCenterX)) {

                    playerY = p.y - PLAYER_HEIGHT;
                    playerVy = 0;
                    state = PlayerState.STANDING;
                    onPlatform = true;
                }
            }

            // 计分:穿过平台
            if (!p.passed && p.y > playerY + PLAYER_HEIGHT) {
                p.passed = true;
                floorCount++;
                score += 100;
                if (floorCount >= 100) gameWon = true;
            }
        }

        if (!onPlatform && state == PlayerState.STANDING) {
            state = PlayerState.FALLING;
        }

        // 5. 起跳后状态恢复
        if (state == PlayerState.JUMPING && playerVy > 0) {
            state = PlayerState.FALLING;
        }

        // 6. 钉板碰撞检测(触碰顶部钉板即死亡)
        if (playerY < spikeY + 30) {
            gameOver = true;
        }

        // 7. 平台滚动与动态生成
        for (Platform p : platforms) {
            p.y += spikeSpeed; // 平台随钉板一起上升(相对玩家下降)
        }

        // 移除屏幕外的平台,在底部生成新平台
        platforms.removeIf(p -> p.y > HEIGHT + 50);

        // 计算最低平台位置
        double lowestY = platforms.isEmpty() ? 0 : platforms.get(platforms.size() - 1).y;
        while (lowestY < HEIGHT + 100) {
            double spacing = Math.max(80, 140 - difficulty * 5);
            lowestY += spacing;
            generatePlatform(lowestY);
        }

        repaint();
    }

    private void jump() {
        if (state == PlayerState.STANDING) {
            playerVy = -10;
            state = PlayerState.JUMPING;
        }
    }

    @Override
    protected void paintComponent(Graphics g) {
        super.paintComponent(g);
        Graphics2D g2d = (Graphics2D) g;
        g2d.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON);

        // 绘制钉板(顶部死亡区域)
        g2d.setColor(new Color(180, 50, 50));
        int spikeHeight = (int)(spikeY + 50);
        if (spikeHeight > 0) {
            g2d.fillRect(0, 0, WIDTH, spikeHeight);
            // 绘制尖刺
            g2d.setColor(new Color(220, 80, 80));
            for (int x = 0; x < WIDTH; x += 20) {
                int[] sx = {x, x + 10, x + 20};
                int[] sy = {spikeHeight, spikeHeight - 15, spikeHeight};
                g2d.fillPolygon(sx, sy, 3);
            }
        }

        // 绘制平台
        for (Platform p : platforms) {
            for (Brick b : p.bricks) {
                // 砖块颜色根据层数变化
                int colorShift = (floorCount / 10) % 3;
                if (colorShift == 0) g2d.setColor(new Color(100, 180, 100));
                else if (colorShift == 1) g2d.setColor(new Color(100, 150, 200));
                else g2d.setColor(new Color(200, 150, 100));

                g2d.fillRect(b.x, (int)p.y, b.width, b.height);
                g2d.setColor(Color.WHITE);
                g2d.drawRect(b.x, (int)p.y, b.width, b.height);
            }
        }

        // 绘制玩家
        g2d.setColor(new Color(255, 220, 100));
        g2d.fillOval((int)playerX, (int)playerY, PLAYER_WIDTH, PLAYER_HEIGHT);
        // 玩家眼睛(方向指示)
        g2d.setColor(Color.BLACK);
        int eyeOffset = (moveRight ? 4 : moveLeft ? -4 : 0);
        g2d.fillOval((int)playerX + 8 + eyeOffset, (int)playerY + 10, 6, 6);
        g2d.fillOval((int)playerX + 18 + eyeOffset, (int)playerY + 10, 6, 6);

        // 绘制UI
        g2d.setColor(Color.WHITE);
        g2d.setFont(new Font("Microsoft YaHei", Font.BOLD, 18));
        g2d.drawString("Floor: " + floorCount + " / 100", 20, 30);
        g2d.drawString("Score: " + score, 180, 30);

        long elapsed = (System.currentTimeMillis() - startTime) / 1000;
        g2d.drawString(String.format("Time: %d:%02d", elapsed / 60, elapsed % 60), 320, 30);

        // 难度指示
        int diff = floorCount / 10;
        g2d.setColor(new Color(255, 200, 100));
        g2d.drawString("Difficulty: " + "★".repeat(diff + 1), 20, 55);

        // 状态指示
        g2d.setColor(new Color(200, 200, 200));
        g2d.setFont(new Font("Microsoft YaHei", Font.PLAIN, 14));
        g2d.drawString("State: " + state, 20, HEIGHT - 20);

        if (gameOver) {
            g2d.setColor(new Color(150, 0, 0, 200));
            g2d.fillRect(0, 0, WIDTH, HEIGHT);
            g2d.setColor(Color.WHITE);
            g2d.setFont(new Font("Microsoft YaHei", Font.BOLD, 36));
            g2d.drawString("GAME OVER", WIDTH / 2 - 100, HEIGHT / 2);
            g2d.setFont(new Font("Microsoft YaHei", Font.PLAIN, 18));
            g2d.drawString("Reached Floor: " + floorCount, WIDTH / 2 - 80, HEIGHT / 2 + 35);
            g2d.drawString("Press R to Restart", WIDTH / 2 - 80, HEIGHT / 2 + 65);
        }

        if (gameWon) {
            g2d.setColor(new Color(0, 150, 0, 200));
            g2d.fillRect(0, 0, WIDTH, HEIGHT);
            g2d.setColor(Color.WHITE);
            g2d.setFont(new Font("Microsoft YaHei", Font.BOLD, 36));
            g2d.drawString("YOU WIN!", WIDTH / 2 - 80, HEIGHT / 2);
            g2d.setFont(new Font("Microsoft YaHei", Font.PLAIN, 18));
            g2d.drawString("Final Score: " + score, WIDTH / 2 - 70, HEIGHT / 2 + 35);
        }
    }

    @Override public void keyPressed(KeyEvent e) {
        if (e.getKeyCode() == KeyEvent.VK_LEFT) moveLeft = true;
        if (e.getKeyCode() == KeyEvent.VK_RIGHT) moveRight = true;
        if (e.getKeyCode() == KeyEvent.VK_SPACE) jump();
        if (e.getKeyCode() == KeyEvent.VK_R && (gameOver || gameWon)) {
            resetGame();
        }
    }

    @Override public void keyReleased(KeyEvent e) {
        if (e.getKeyCode() == KeyEvent.VK_LEFT) moveLeft = false;
        if (e.getKeyCode() == KeyEvent.VK_RIGHT) moveRight = false;
    }
    @Override public void keyTyped(KeyEvent e) {}

    private void resetGame() {
        playerX = WIDTH / 2.0 - PLAYER_WIDTH / 2.0;
        playerY = 100;
        playerVx = 0;
        playerVy = 0;
        state = PlayerState.FALLING;
        platforms.clear();
        spikeY = -50;
        floorCount = 0;
        score = 0;
        gameOver = false;
        gameWon = false;
        startTime = System.currentTimeMillis();
        random = new Random(123);
        for (int i = 0; i < 6; i++) {
            generatePlatform(i * 120 + 200);
        }
    }

    public static void main(String[] args) {
        SwingUtilities.invokeLater(() -> {
            JFrame frame = new JFrame("是男人就下一百层 - 随机平台与安全着陆检测");
            frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
            frame.add(new HundredFloors());
            frame.pack();
            frame.setLocationRelativeTo(null);
            frame.setVisible(true);
        });
    }
}

四、关键算法详解

4.1 间隙不对齐策略

若相邻两层平台的间隙完全对齐,玩家可以一路直落,失去游戏性。代码通过以下逻辑强制错位:

if (Math.abs(gapStart + gapWidth / 2 - lastGapCenter) < 80) {
    gapStart = (gapStart + WIDTH / 2) % maxStart;
}

当新旧间隙中心距离小于80像素时,将新间隙偏移半个屏幕宽度,确保玩家必须水平移动才能找到下一层的安全通道。

4.2 平台碰撞的精确判定

角色与平台的碰撞需要区分三种情况:
1. 站立:角色底部与砖块顶部接触,且角色中心不在间隙中
2. 穿过间隙:角色中心位于间隙水平范围内,继续下落
3. 侧面碰撞:角色侧面碰到砖块侧面(简化处理为允许穿透,降低挫败感)

代码中通过 containsGapAt(playerCenterX) 判断角色是否处于间隙上方,这是最核心的碰撞逻辑。

4.3 运动状态机

角色的三种状态通过显式状态机管理:

STANDING --(空格/跳跃)--> JUMPING --(速度转正)--> FALLING --(着陆)--> STANDING

状态机避免了复杂的条件嵌套,使碰撞检测逻辑清晰可维护。例如只有在 FALLING 或 STANDING 状态下才能触发着陆判定,JUMPING 状态中的上升阶段允许角色向上穿越平台。

五、复杂度分析

操作 时间复杂度 说明
平台碰撞检测 $O(N \cdot M)$ $N$ 为平台数,$M$ 为每平台砖块数
间隙生成 $O(W / BRICK_WIDTH)$ $W$ 为屏幕宽度
平台滚动更新 $O(N)$ 更新所有平台Y坐标
屏幕外清理 $O(N)$ 遍历移除越界平台

由于屏幕同时显示的平台数被物理高度限制(通常6~8个),所有操作实际为 $O(1)$。

六、扩展思路

  1. AI自动游玩:使用射线投射预判下一层间隙位置,自动规划水平移动路径
  2. 多种平台类型:移动平台(水平往复)、易碎平台(站立后消失)、弹力平台(起跳加速)
  3. 道具系统:减速时钟(钉板暂时停止)、透视镜(显示下方3层平台布局)
  4. 关卡编辑器:将平台生成参数序列化为JSON,支持玩家自制关卡分享

“是男人就下一百层”将随机生成、碰撞检测和状态机管理融为一体,是理解2D平台游戏物理引擎的极佳入门项目。