《是男人就下一百层》是经典的街机下落类游戏,玩家操控角色在不断上升的钉板之间向下跳跃,通过左右移动寻找安全的平台间隙。本文用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)$。
六、扩展思路
- AI自动游玩:使用射线投射预判下一层间隙位置,自动规划水平移动路径
- 多种平台类型:移动平台(水平往复)、易碎平台(站立后消失)、弹力平台(起跳加速)
- 道具系统:减速时钟(钉板暂时停止)、透视镜(显示下方3层平台布局)
- 关卡编辑器:将平台生成参数序列化为JSON,支持玩家自制关卡分享
“是男人就下一百层”将随机生成、碰撞检测和状态机管理融为一体,是理解2D平台游戏物理引擎的极佳入门项目。