理解消行检测的原理,学会如何消除满行并计算分数。
当游戏板的某一行被方块完全填满时:
消除前: 消除后:
┌────────┐ ┌────────┐
│ │ │ │ ← 空行
│ ████ │ ← 满行 │ ████ │ ← 上面的行下落
│███████ │ │███████ │
│████████│ │████████│
└────────┘ └────────┘
↑ ↑
第 19 行(满)消除 第 19 行变空
消行后发生什么:
消行是俄罗斯方块的核心玩法:
玩家目标:
1. 放置方块
2. 形成满行
3. 消除得分
4. 避免堆满
如果没有消行:
- 方块会一直堆积
- 游戏很快结束
- 没有分数,没有乐趣
单行 vs 多行:
策略 1:单行消除(简单)
┌────────┐
│ │
│ ████ │ ← 消除 1 行 = 40 分
│███████ │
└────────┘
策略 2:四行消除(俄罗斯方块!)
┌────────┐
│ ████ │ ← 等待 I 方块
│ ████ │ ← 一次消除 4 行
│ ████ │ ← 1200 分!
│ ████ │
└────────┘
高手会:
步骤 1:从底部向上检查每一行
步骤 2:判断该行是否全满
步骤 3:如果满行,消除并下落上面的行
步骤 4:统计消除行数,计算分数
步骤 5:返回消除行数
// board.c
int board_clear_lines(Board* board) {
int cleared = 0; // 统计消除行数
// 步骤 1:从底部向上检查
for (int y = BOARD_HEIGHT - 1; y >= 0; y--) {
bool full = true;
// 步骤 2:检查该行是否全满
for (int x = 0; x < BOARD_WIDTH; x++) {
if (!board->cells[y][x]) {
full = false; // 有空格,不是满行
break;
}
}
if (full) {
cleared++;
// 步骤 3:消除该行,上面的行下落
for (int yy = y; yy > 0; yy--) {
memcpy(board->cells[yy],
board->cells[yy-1],
sizeof(board->cells[0]));
}
// 清空顶行
memset(board->cells[0], 0, sizeof(board->cells[0]));
// 重要:重新检查当前行(因为上面的行下落了)
y++;
}
}
return cleared;
}
for (int y = BOARD_HEIGHT - 1; y >= 0; y--)
// ↑ 从最底部开始
原因:
对比:
❌ 从顶部向下检查:
行 0: 满行 → 消除,行 1-19 下落
↓
行 1: 现在是原来的行 2(跳过了原来的行 1!)
↓
结果:漏检!
✅ 从底部向上检查:
行 19: 满行 → 消除,行 0-18 下落
↓
行 18: 现在是原来的行 17(正确!)
↓
结果:全部检查到!
y++?if (full) {
// ... 消除逻辑
y++; // 重要!重新检查当前行
}
原因图解:
初始状态:
行 19: ████ ████ ← 满行,消除
行 18: ████ ████ ← 满行
行 17: ████ ████ ← 满行
消除行 19 后,行 18 下落到行 19:
行 19: ████ ████ ← 原来的行 18(现在是满行!)
行 18: ████ ████ ← 原来的行 17
行 17: 空行 ← 新下落的空行
如果不 y++:
- 下一次循环 y=18
- 漏掉了新的行 19(可能也是满行)
如果 y++:
- y++ 后,循环 y--,还是检查 y=19
- 正确检查新的行 19
memcpy(board->cells[yy],
board->cells[yy-1],
sizeof(board->cells[0]));
这行代码做什么?
把上一行(yy-1)的整行数据复制到当前行(yy)
内存布局:
cells[yy-1]: [0][1][2][3][4][5][6][7][8][9] ← 复制这 10 个 int
↓
cells[yy]: [0][1][2][3][4][5][6][7][8][9] ← 粘贴到这里
效果:整行下落
为什么用 memcpy?
// 方案 1:逐个元素复制(慢)
for (int x = 0; x < BOARD_WIDTH; x++) {
board->cells[yy][x] = board->cells[yy-1][x];
}
// 方案 2:memcpy(快)
memcpy(board->cells[yy],
board->cells[yy-1],
sizeof(board->cells[0]));
// memcpy 是底层内存复制,速度更快
// score.c
#define SCORE_SINGLE 40 // 1 行
#define SCORE_DOUBLE 100 // 2 行
#define SCORE_TRIPLE 300 // 3 行
#define SCORE_TETRIS 1200 // 4 行(俄罗斯方块!)
int score_calculate_lines(int lines, int level) {
int base_scores[] = {0, 40, 100, 300, 1200};
if (lines >= 1 && lines <= 4) {
return base_scores[lines] * (level + 1);
}
return 0;
}
鼓励多行消除:
等级 0 时:
4 次单行消除:40 × 4 = 160 分
1 次四行消除:1200 分
1200 ÷ 160 = 7.5 倍!
所以高手会追求一次消除 4 行,而不是慢慢消单行
等级加成:
同样消除 4 行:
等级 0: 1200 × (0+1) = 1200 分
等级 5: 1200 × (5+1) = 7200 分
等级 10: 1200 × (10+1) = 13200 分
等级越高,分数越多,鼓励挑战高难度
// game.c
void game_update(Game* game) {
game->current.y++;
if (board_check_collision(&game->board, &game->current)) {
game->current.y--;
// 1. 锁定方块
board_lock_tetromino(&game->board, &game->current);
// 2. 检查并消除行
int lines = board_clear_lines(&game->board);
// 3. 如果有消除,更新分数
if (lines > 0) {
score_add(&game->score,
score_calculate_lines(lines, game->level));
game->lines_cleared += lines;
// 4. 检查升级
if (game->lines_to_level <= 0) {
game->level++;
game->lines_to_level = 10;
game->drop_interval = game->drop_interval * 90 / 100;
}
}
// 5. 生成新方块
game->current = game->next;
tetromino_init(&game->next, random_tetromino());
}
}
执行顺序很重要:
// render.c
void render_line_clear_animation(int lines[]) {
// 闪烁 3 次
for (int i = 0; i < 3; i++) {
// 显示消除的行(白色)
for (int l = 0; l < lines_count; l++) {
int y = lines[l];
for (int x = 0; x < BOARD_WIDTH; x++) {
attron(COLOR_PAIR(8));
mvaddch(y, x * 2, "██");
attroff(COLOR_PAIR(8));
}
}
refresh();
napms(50);
// 隐藏(黑色)
for (int l = 0; l < lines_count; l++) {
int y = lines[l];
for (int x = 0; x < BOARD_WIDTH; x++) {
mvaddstr(y, x * 2, " ");
}
}
refresh();
napms(50);
}
}
int lines = board_clear_lines(&game->board);
if (lines > 0) {
printf("消除了 %d 行!得分:%d\n",
lines,
score_calculate_lines(lines, game->level));
}
void print_board(Board* board) {
for (int y = 0; y < BOARD_HEIGHT; y++) {
printf("|");
for (int x = 0; x < BOARD_WIDTH; x++) {
if (board->cells[y][x]) {
printf("█");
} else {
printf(" ");
}
}
printf("|\n");
}
}
y++修改消行逻辑,从顶部向下检查,观察会出现什么问题
如果不使用 memcpy,改用 for 循环逐个元素复制,代码怎么写?
实现一个简单的消行动画(闪烁效果)
下一课:UI 界面