别踩白块(Don’t Tap the White Tile)是一款考验反应速度的经典音乐节奏游戏。玩家需要在黑色方块滑落到屏幕底部之前准确点击它们,同时避免触碰到白色方块。本文用Java实现别踩白块游戏的核心算法,重点讲解贪心最优点击序列与动态难度调整策略。
游戏规则与算法建模
游戏界面由4列组成,黑色方块从顶部向底部匀速滑落。核心算法挑战包括:
- 点击判定:在方块进入底部判定区域时,判断玩家点击是否准确
- 连击判定:连续准确点击的计数与奖励机制
- 动态难度:根据玩家表现调整方块下落速度
- 最优序列生成:确保生成的方块序列既有挑战性又可完成
贪心最优点击序列生成
为了保证游戏体验,方块生成不能纯随机——可能出现无法完成的序列(如两列同时要求点击)。我们使用贪心策略确保生成的序列始终可完成,同时保持挑战性。
import java.util.*;
/**
* 别踩白块 - 方块序列生成器
* 使用贪心策略确保生成的序列可完成且具有节奏感
*/
public class TileSequenceGenerator {
private static final int COLS = 4; // 4列
private static final double MIN_INTERVAL = 0.15; // 最小间隔(秒)
private static final double MAX_INTERVAL = 0.5; // 最大间隔(秒)
static class Tile {
int col; // 所在列 0-3
double spawnTime; // 生成时间
double speed; // 下落速度 (像素/秒)
int type; // 0=普通黑块, 1=长条, 2=奖励块
Tile(int col, double spawnTime, double speed, int type) {
this.col = col; this.spawnTime = spawnTime;
this.speed = speed; this.type = type;
}
}
private Random rand = new Random();
private double currentTime = 0;
/**
* 贪心生成下一批方块
* 约束1:同一时刻最多1个黑块(避免无法同时点击)
* 约束2:相邻时间间隔不能太短(给玩家反应时间)
* 约束3:列分布尽量均匀(避免总是同一列)
*/
public List<Tile> generateSequence(int count, double baseSpeed) {
List<Tile> sequence = new ArrayList<>();
int[] colCount = new int[COLS]; // 记录每列出现次数
for (int i = 0; i < count; i++) {
// 贪心选择:选择出现次数最少的列,增加均匀性
int minCount = Integer.MAX_VALUE;
List<Integer> candidates = new ArrayList<>();
for (int c = 0; c < COLS; c++) {
if (colCount[c] < minCount) {
minCount = colCount[c];
candidates.clear();
candidates.add(c);
} else if (colCount[c] == minCount) {
candidates.add(c);
}
}
int col = candidates.get(rand.nextInt(candidates.size()));
colCount[col]++;
// 动态调整间隔:连击多时加快,失误时减慢
double interval = MIN_INTERVAL + (MAX_INTERVAL - MIN_INTERVAL)
* (1 - (double)i / count);
currentTime += interval;
// 随进度逐渐加速
double speed = baseSpeed * (1 + (double)i / count * 0.5);
int type = (rand.nextDouble() < 0.1) ? 2 : 0; // 10%概率奖励块
sequence.add(new Tile(col, currentTime, speed, type));
}
return sequence;
}
/**
* 验证序列是否可完成
* 确保任意两个相邻方块的点击时间差大于人类反应时间(约0.1秒)
*/
public boolean isSequenceValid(List<Tile> sequence) {
final double REACTION_TIME = 0.1;
for (int i = 1; i < sequence.size(); i++) {
double diff = sequence.get(i).spawnTime - sequence.get(i-1).spawnTime;
if (diff < REACTION_TIME) return false;
}
return true;
}
}
动态难度调整算法
游戏需要根据玩家表现实时调整难度。我们使用一个基于最近表现评分的滑动窗口机制:
/**
* 动态难度调整器
* 根据玩家最近N次点击的准确率调整下落速度
*/
public class DynamicDifficulty {
private static final int WINDOW_SIZE = 10; // 评估窗口大小
private static final double MIN_SPEED = 200; // 最小速度(像素/秒)
private static final double MAX_SPEED = 800; // 最大速度(像素/秒)
private static final double SPEED_STEP = 30; // 速度调整步长
private Queue<Boolean> recentHits = new LinkedList<>(); // 最近点击结果
private double currentSpeed = 300; // 当前速度
/**
* 记录一次点击结果并调整难度
* @param hit true=准确点击, false=失误
*/
public void recordHit(boolean hit) {
recentHits.offer(hit);
if (recentHits.size() > WINDOW_SIZE) {
recentHits.poll();
}
// 计算最近准确率
long hits = recentHits.stream().filter(b -> b).count();
double accuracy = (double) hits / recentHits.size();
// 贪心调整策略:准确率高则加速,低则减速
if (accuracy >= 0.9 && recentHits.size() == WINDOW_SIZE) {
currentSpeed = Math.min(MAX_SPEED, currentSpeed + SPEED_STEP);
} else if (accuracy < 0.6) {
currentSpeed = Math.max(MIN_SPEED, currentSpeed - SPEED_STEP * 2);
}
}
public double getCurrentSpeed() { return currentSpeed; }
/**
* 计算当前难度等级 (1-10)
*/
public int getDifficultyLevel() {
return (int) Math.round((currentSpeed - MIN_SPEED) / (MAX_SPEED - MIN_SPEED) * 9) + 1;
}
}
点击判定与评分系统
import java.awt.*;
import java.awt.event.*;
import javax.swing.*;
import java.util.*;
import java.util.List;
/**
* 别踩白块完整游戏实现
*/
public class PianoTilesGame extends JPanel implements ActionListener, MouseListener {
private static final int WIDTH = 400;
private static final int HEIGHT = 600;
private static final int COL_WIDTH = WIDTH / 4;
private static final int TILE_HEIGHT = 120;
private static final int HIT_ZONE_Y = HEIGHT - 150; // 判定线位置
private List<Tile> tiles = new ArrayList<>();
private Timer timer;
private double gameTime = 0;
private int score = 0;
private int combo = 0;
private int maxCombo = 0;
private int missCount = 0;
private double baseSpeed = 300; // 像素/秒
private DynamicDifficulty difficulty = new DynamicDifficulty();
private Random rand = new Random();
private double nextSpawnTime = 0;
static class Tile {
int col;
double y; // 当前Y坐标
double speed;
boolean clicked;
boolean missed;
Color color;
Tile(int col, double y, double speed) {
this.col = col; this.y = y; this.speed = speed;
this.clicked = false; this.missed = false;
this.color = Color.BLACK;
}
}
public PianoTilesGame() {
setPreferredSize(new Dimension(WIDTH, HEIGHT));
setBackground(Color.WHITE);
addMouseListener(this);
timer = new Timer(16, this); // ~60 FPS
}
/**
* 贪心策略生成下一个方块
* 优先选择当前列数最少的列,保证分布均匀
*/
private void spawnTile() {
// 统计当前每列的方块数
int[] colTiles = new int[4];
for (Tile t : tiles) {
if (!t.clicked && !t.missed) colTiles[t.col]++;
}
// 贪心选择:选择当前方块最少的列
int minTiles = Integer.MAX_VALUE;
List<Integer> candidates = new ArrayList<>();
for (int i = 0; i < 4; i++) {
if (colTiles[i] < minTiles) {
minTiles = colTiles[i];
candidates.clear();
candidates.add(i);
} else if (colTiles[i] == minTiles) {
candidates.add(i);
}
}
int col = candidates.get(rand.nextInt(candidates.size()));
double speed = difficulty.getCurrentSpeed();
tiles.add(new Tile(col, -TILE_HEIGHT, speed));
}
@Override
public void actionPerformed(ActionEvent e) {
double dt = 0.016; // 16ms
gameTime += dt;
// 生成新方块
if (gameTime >= nextSpawnTime) {
spawnTile();
// 动态调整生成间隔:速度越快,间隔越短
double interval = Math.max(0.2, 0.5 - (difficulty.getCurrentSpeed() - 300) / 1000);
nextSpawnTime = gameTime + interval;
}
// 更新方块位置
Iterator<Tile> it = tiles.iterator();
while (it.hasNext()) {
Tile t = it.next();
t.y += t.speed * dt;
// 判定失误:方块滑过判定线且未被点击
if (t.y > HEIGHT && !t.clicked && !t.missed) {
t.missed = true;
combo = 0;
missCount++;
difficulty.recordHit(false);
}
// 移除已滑出屏幕的方块
if (t.y > HEIGHT + TILE_HEIGHT) {
it.remove();
}
}
repaint();
}
@Override
public void mousePressed(MouseEvent e) {
int col = e.getX() / COL_WIDTH;
// 贪心查找该列最接近判定线的未点击方块
Tile target = null;
double minDist = Double.MAX_VALUE;
for (Tile t : tiles) {
if (t.col == col && !t.clicked && !t.missed) {
double dist = Math.abs(t.y - HIT_ZONE_Y);
if (dist < minDist && dist < TILE_HEIGHT) {
minDist = dist;
target = t;
}
}
}
if (target != null) {
// 判定成功
target.clicked = true;
target.color = Color.GRAY;
// 计算判定精度
double accuracy = 1 - minDist / TILE_HEIGHT;
int points = (int) (10 * accuracy);
if (accuracy > 0.8) {
combo++;
points += combo;
if (combo > maxCombo) maxCombo = combo;
} else {
combo = 0;
}
score += Math.max(1, points);
difficulty.recordHit(true);
} else {
// 点击空白区域或白块 = 失误
combo = 0;
missCount++;
difficulty.recordHit(false);
}
repaint();
}
@Override
protected void paintComponent(Graphics g) {
super.paintComponent(g);
// 绘制列分割线
g.setColor(Color.LIGHT_GRAY);
for (int i = 1; i < 4; i++) {
g.drawLine(i * COL_WIDTH, 0, i * COL_WIDTH, HEIGHT);
}
// 绘制判定线
g.setColor(Color.RED);
g.drawLine(0, HIT_ZONE_Y, WIDTH, HIT_ZONE_Y);
// 绘制方块
for (Tile t : tiles) {
g.setColor(t.color);
g.fillRect(t.col * COL_WIDTH + 2, (int)t.y, COL_WIDTH - 4, TILE_HEIGHT);
if (t.clicked) {
g.setColor(Color.GREEN);
g.drawRect(t.col * COL_WIDTH + 2, (int)t.y, COL_WIDTH - 4, TILE_HEIGHT);
}
}
// 绘制UI
g.setColor(Color.BLACK);
g.setFont(new Font("Arial", Font.BOLD, 18));
g.drawString("Score: " + score, 10, 30);
g.drawString("Combo: " + combo, 10, 55);
g.drawString("Max Combo: " + maxCombo, 10, 80);
g.drawString("Speed: " + String.format("%.0f", difficulty.getCurrentSpeed()), 10, 105);
g.drawString("Miss: " + missCount, 10, 130);
}
public void start() { timer.start(); }
@Override public void mouseClicked(MouseEvent e) {}
@Override public void mouseReleased(MouseEvent e) {}
@Override public void mouseEntered(MouseEvent e) {}
@Override public void mouseExited(MouseEvent e) {}
public static void main(String[] args) {
SwingUtilities.invokeLater(() -> {
JFrame frame = new JFrame("别踩白块 - 贪心策略");
PianoTilesGame game = new PianoTilesGame();
frame.add(game);
frame.pack();
frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
frame.setVisible(true);
game.start();
});
}
}
算法核心要点总结
贪心策略在别踩白块中的应用:
- 序列生成贪心:优先选择出现频率最低的列,保证4列分布均匀,避免玩家疲劳
- 点击判定贪心:在点击列中,选择距离判定线最近的方块作为目标,最大化判定精度
- 难度调整贪心:根据滑动窗口内的准确率,快速响应调整速度,保持心流体验
时间复杂度分析:
– 方块生成:O(4) = O(1),只需遍历4列统计
– 点击判定:O(N),N为当前方块数,通常 N < 20
– 动态难度更新:O(1),滑动窗口大小固定
扩展方向
- 多指同时点击:支持同时点击多个黑块,序列生成需要保证相邻方块间隔大于反应时间
- 音乐同步:将方块生成与BPM(每分钟节拍数)绑定,实现音乐节奏游戏
- 机器学习难度预测:用玩家历史数据训练模型,预测最佳难度曲线