理解影子方块的作用和实现原理,学会如何预测方块落点。
影子方块 = 预测落点
显示当前方块如果直接下落,最终会停在什么位置。
┌────────┐
│ │
│ ██ │ ← 当前方块(实心)
│ ██ │
│ │
│ │
│ ░░ │ ← 影子方块(虚线/灰色)
│ ░░ │
└────────┘
好处:
步骤 1:复制当前方块到影子方块
步骤 2:影子方块一直下落
步骤 3:直到碰撞前一刻停止
步骤 4:记录影子位置
步骤 5:渲染时用不同颜色/样式
game_update()
↓
方块下落 (current.y++)
↓
检查碰撞
↓
┌─────────────┐
│ 有碰撞? │
└──────┬──────┘
Yes │ No
↓ │ ↓
锁定方块 game_calculate_ghost()
↓
ghost = current
↓
while (!collision)
ghost.y++
↓
ghost.y-- (退回一格)
↓
渲染时用灰色显示
// game.h
typedef struct {
Board board;
Tetromino current;
Tetromino next;
Tetromino hold;
Tetromino ghost; // 影子方块
// ...
} Game;
// game.c
void game_calculate_ghost(Game* game) {
if (!game || game->game_over) return;
// 1. 复制当前方块
game->ghost = game->current;
// 2. 一直下落直到碰撞
while (1) {
game->ghost.y++;
// 3. 检查碰撞
if (board_check_collision(&game->board, &game->ghost)) {
// 4. 碰撞了,退回一格
game->ghost.y--;
break;
}
}
}
// 方案 1:while 循环(我们采用的)
while (1) {
ghost.y++;
if (collision) {
ghost.y--;
break;
}
}
// 方案 2:for 循环
for (ghost.y = current.y; ghost.y < BOARD_HEIGHT; ghost.y++) {
if (collision) {
ghost.y--;
break;
}
}
// 方案 1 更清晰:
// - 逻辑简单:一直下落直到碰撞
// - 不需要考虑循环条件
// - 碰撞后退回一格的逻辑明确
void game_update(Game* game) {
game->current.y++;
if (board_check_collision(&game->board, &game->current)) {
game->current.y--;
board_lock_tetromino(&game->board, &game->current);
int lines = board_clear_lines(&game->board);
// ... 计分等逻辑
game->current = game->next;
tetromino_init(&game->next, random_tetromino());
}
// 每次更新后,重新计算影子位置
game_calculate_ghost(game);
}
// render.c
void render_ghost(Tetromino* t, int offset_x, int offset_y) {
attron(COLOR_PAIR(8)); // 灰色/白色
attron(A_DIM); // 半透明/变暗
for (int i = 0; i < 4; i++) {
for (int j = 0; j < 4; j++) {
if (t->shape[i][j]) {
int x = t->x + j + offset_x;
int y = t->y + i + offset_y;
if (y >= 0 && y < BOARD_HEIGHT) {
// 使用 ".." 表示虚线效果
mvaddstr(y, x * 2, "..");
}
}
}
}
attroff(A_DIM);
attroff(COLOR_PAIR(8));
}
| 样式 | 代码 | 效果 |
|---|---|---|
| 实心方块 | ACS_CKBOARD |
██ |
| 虚线效果 | ".." |
.. |
| 浅色字符 | "░░" |
░░ |
| 空心方块 | "[]" |
[] |
我们选择 “..” 的原因:
void render_game(Game* game) {
clear();
// 1. 先渲染影子(最底层)
render_ghost(&game->ghost, 0, 1);
// 2. 渲染固定方块(中间层)
render_board(&game->board);
// 3. 渲染当前方块(最上层)
render_tetromino(&game->current, 0, 1);
// 4. UI 元素
render_next_piece();
render_score();
refresh();
}
为什么这个顺序?
图层:
影子 (底层,灰色)
↓
固定方块 (中层,彩色)
↓
当前方块 (上层,彩色,最显眼)
↓
UI 文字 (最上层)
如果顺序错了:
- 先画当前方块,再画影子 → 影子覆盖当前方块 ❌
- 先画 UI,再画方块 → 方块覆盖文字 ❌
// 不需要每次都计算的情况:
// 1. 方块只是左右移动 → 影子 x 也移动,y 不变
// 2. 方块旋转 → 影子也旋转,y 可能变
// 需要重新计算的情况:
// 1. 方块下落 → 影子 y 可能变
// 2. 硬降 → 影子与当前方块重合
// 3. 方块锁定,生成新方块 → 完全重新计算
// 优化方案:
void game_update(Game* game) {
bool needs_ghost_update = false;
// 左右移动
if (key == KEY_LEFT || key == KEY_RIGHT) {
game->ghost.x = game->current.x;
// y 不变,不需要重新计算
}
// 下落
if (game->current.y != old_y) {
needs_ghost_update = true;
}
if (needs_ghost_update) {
game_calculate_ghost(game);
}
}
但我们的实现:
// 简单方案:每次都重新计算
// 优点:代码简单,不易出错
// 缺点:稍微浪费一点 CPU
// 但俄罗斯方块计算量很小,可以忽略
void render_ghost(Tetromino* t) {
for (int i = 0; i < 4; i++) {
for (int j = 0; j < 4; j++) {
if (t->shape[i][j]) {
int y = t->y + i;
// 检查是否在屏幕内
if (y < 0) continue; // 在顶部以上,不画
if (y >= BOARD_HEIGHT) continue; // 在底部以下,不画
// 绘制...
}
}
}
}
void render_game(Game* game) {
// 游戏结束时,不显示影子
if (!game->game_over) {
render_ghost(&game->ghost);
}
// ...
}
尝试不同的影子样式(░░、[]、空心等)
实现性能优化:只在需要时重新计算影子
为影子方块添加动画效果(如闪烁)
下一课:最高分系统