c_tetris

第 10 课:影子方块 👻

10.1 本课目标

理解影子方块的作用和实现原理,学会如何预测方块落点。


10.2 什么是影子方块?

影子方块 = 预测落点

显示当前方块如果直接下落,最终会停在什么位置。

┌────────┐
│        │
│  ██    │  ← 当前方块(实心)
│  ██    │
│        │
│        │
│  ░░    │  ← 影子方块(虚线/灰色)
│  ░░    │
└────────┘

好处:


10.3 实现原理

算法步骤

步骤 1:复制当前方块到影子方块
步骤 2:影子方块一直下落
步骤 3:直到碰撞前一刻停止
步骤 4:记录影子位置
步骤 5:渲染时用不同颜色/样式

流程图

game_update()
    ↓
方块下落 (current.y++)
    ↓
检查碰撞
    ↓
┌─────────────┐
│ 有碰撞?     │
└──────┬──────┘
   Yes │ No
    ↓  │  ↓
锁定方块  game_calculate_ghost()
           ↓
       ghost = current
           ↓
       while (!collision)
           ghost.y++
           ↓
       ghost.y-- (退回一格)
           ↓
       渲染时用灰色显示

10.4 代码实现

数据结构

// 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;
        }
    }
}

为什么用 while(1)?

// 方案 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 更清晰:
// - 逻辑简单:一直下落直到碰撞
// - 不需要考虑循环条件
// - 碰撞后退回一格的逻辑明确

在 game_update 中调用

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);
}

10.5 渲染影子

使用不同样式

// 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 ██
虚线效果 ".." ..
浅色字符 "░░" ░░
空心方块 "[]" []

我们选择 “..” 的原因:


10.6 渲染顺序

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,再画方块 → 方块覆盖文字 ❌

10.7 性能优化

什么时候需要重新计算?

// 不需要每次都计算的情况:
// 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
// 但俄罗斯方块计算量很小,可以忽略

10.8 特殊情况处理

影子在屏幕外

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);
    }
    // ...
}

✅ 本课检查清单


📝 小作业

  1. 尝试不同的影子样式(░░、[]、空心等)

  2. 实现性能优化:只在需要时重新计算影子

  3. 为影子方块添加动画效果(如闪烁)


下一课:最高分系统