c_tetris

第 5 课:碰撞检测 🚧

5.1 本课目标

理解碰撞检测的原理,学会如何判断方块能否移动到某个位置。


5.2 什么是碰撞检测?

碰撞检测 = 判断物体是否重叠

在俄罗斯方块中,碰撞检测用于回答这些问题:

┌────────┐
│   ██   │  ← 方块
│   ██   │
└────────┘
    ↓ 能否向左移动?
    
┌────────┐
│██      │  ← 可以!左边没东西
│██      │
└────────┘
┌────────┐
│   ██   │  ← 方块
│   ██   │
└────────┘
    ↓ 能否向下移动?
    
  ┌──┐
  │██│  ← 不能!下面有方块
  │██│
  └──┘

5.3 为什么需要碰撞检测?

没有碰撞检测会怎样?

// ❌ 错误做法:不检查碰撞
game->current.x--;  // 直接移动
// 结果:方块穿墙而出!

后果:

正确的做法

// ✅ 正确做法:先检查,再移动
game->current.x--;
if (board_check_collision(&game->board, &game->current)) {
    // 碰撞了,退回
    game->current.x++;
}

5.4 碰撞检测的四种情况

情况 1:超出左边界

if (x < 0) {
    return true;  // 碰撞
}

图示:

正常:        碰撞:
┌────┐       ┌────┐
│ ██ │       │█   │  ← x=-1,超出左边界
│ ██ │       │█   │
└────┘       └────┘
 x=0         x=-1 ❌

情况 2:超出右边界

if (x >= BOARD_WIDTH) {
    return true;  // 碰撞
}

图示:

正常:          碰撞:
┌────────┐     ┌────────┐
│     ██ │     │      ██│  ← x=10,超出右边界
│     ██ │     │      ██│  (BOARD_WIDTH=10)
└────────┘     └────────┘
  x=8            x=10 ❌

情况 3:超出底边界

if (y >= BOARD_HEIGHT) {
    return true;  // 碰撞
}

图示:

正常:          碰撞:
┌────────┐     ┌────────┐
│        │     │        │
│  ██    │     │        │
│  ██    │     │  ██    │  ← y=20,超出底边界
└────────┘     │  ██    │  (BOARD_HEIGHT=20)
  y=18         └────────┘
                 y=20 ❌

情况 4:碰到已固定的方块

if (y >= 0 && board[y][x] != 0) {
    return true;  // 碰撞
}

图示:

正常:          碰撞:
┌────────┐     ┌────────┐
│        │     │        │
│  ██    │     │  ██    │  ← 当前方块
│        │     │  ██    │
│        │     │███████│  ← 已固定方块 (board[y][x]!=0)
└────────┘     └────────┘

5.5 代码实现详解

完整碰撞检测函数

// tetromino.c
bool tetromino_check_collision(Tetromino* t, 
                               int board[BOARD_HEIGHT][BOARD_WIDTH]) {
    // 遍历方块的 4x4 形状矩阵
    for (int i = 0; i < 4; i++) {
        for (int j = 0; j < 4; j++) {
            // 只检查有方块的部分(shape[i][j] == 1)
            if (t->shape[i][j]) {
                // 计算实际坐标
                int x = t->x + j;
                int y = t->y + i;
                
                // 1. 检查左右边界
                if (x < 0 || x >= BOARD_WIDTH) {
                    return true;  // 碰撞
                }
                
                // 2. 检查底边界
                if (y >= BOARD_HEIGHT) {
                    return true;  // 碰撞
                }
                
                // 3. 检查已固定的方块
                //    注意:y < 0 时不检查(方块还在顶部以上)
                if (y >= 0 && board[y][x]) {
                    return true;  // 碰撞
                }
            }
        }
    }
    
    return false;  // 没有碰撞
}

为什么遍历 4x4 矩阵?

问题: 方块形状不规则,如何检查所有部分?

方案对比:

方案 1:记录每个方块的位置

// I 方块有 4 个方块

int blocks[4][2] = {{0,1}, {1,1}, {2,1}, {3,1}};


// 问题:
// - 每种方块都要单独处理
// - 旋转后要重新计算位置
// - 代码复杂

方案 2:遍历 4x4 矩阵(我们采用的方案)

// 统一处理所有方块
for (int i = 0; i < 4; i++) {
    for (int j = 0; j < 4; j++) {
        if (t->shape[i][j]) {
            // 检查这个方块
        }
    }
}

// 优点:
// - 所有方块用同一套逻辑
// - 旋转后自动适应
// - 代码简洁

为什么检查 t->shape[i][j]

// 4x4 形状矩阵示例(T 方块)
int shape[4][4] = {
    {0, 0, 0, 0},  // ← 都是 0,不需要检查
    {1, 1, 1, 0},  // ← 有 1,需要检查
    {0, 1, 0, 0},  // ← 有 1,需要检查
    {0, 0, 0, 0}   // ← 都是 0,不需要检查
};

// if (t->shape[i][j]) 只检查值为 1 的位置
// 跳过值为 0 的空位置

5.6 坐标转换详解

局部坐标 vs 全局坐标

方块的 4x4 矩阵(局部坐标):
    j=0  j=1  j=2  j=3
i=0  0    0    0    0
i=1  1    1    1    0    ← 这些是方块的"局部"位置
i=2  0    1    0    0
i=3  0    0    0    0

游戏板(全局坐标):
如果方块位置 t->x = 5, t->y = 10

那么 shape[1][0] 的实际位置是:
x = t->x + j = 5 + 0 = 5
y = t->y + i = 10 + 1 = 11

所以检查 board[11][5] 是否有碰撞

坐标转换公式

int x = t->x + j;  // 全局 X = 方块 X + 局部 j
int y = t->y + i;  // 全局 Y = 方块 Y + 局部 i

为什么需要转换?


5.7 碰撞检测的应用场景

场景 1:左右移动

// input.c
case KEY_LEFT:
    game->current.x--;  // 尝试向左移动
    if (board_check_collision(&game->board, &game->current)) {
        // 碰撞了,退回
        game->current.x++;
    }
    break;

case KEY_RIGHT:
    game->current.x++;  // 尝试向右移动
    if (board_check_collision(&game->board, &game->current)) {
        // 碰撞了,退回
        game->current.x--;
    }
    break;

流程图:

玩家按左键
    ↓
current.x--  (尝试移动)
    ↓
检查碰撞
    ↓
┌──────────────┐
│ 有碰撞?      │
└──────┬───────┘
   Yes │ No
    ↓  │  ↓
current.x++  保持新位置
(退回)

场景 2:方块下落

// game.c
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);
        
        // 生成新方块...
    }
}

为什么下落碰撞后要锁定?

场景 3:方块旋转

// input.c
case KEY_UP:
    {
        // 保存原状态
        Tetromino temp = game->current;
        
        // 尝试旋转
        tetromino_rotate(&temp);
        
        // 检查旋转后是否碰撞
        if (!board_check_collision(&game->board, &temp)) {
            // 可以旋转,应用
            game->current = temp;
        }
        // 否则不旋转(保持原样)
    }
    break;

为什么旋转前要先检查?


5.8 墙踢(Wall Kick)简介

什么是墙踢?

当方块在墙边旋转时,通过微调位置来”踢”开墙壁:

旋转前:          旋转后(墙踢):
┌──────┐         ┌──────┐
│█     │         │  █   │  ← 向右踢 1 格
│███   │   →     │ ███  │
└──────┘         └──────┘
贴左墙           离开墙

简单墙踢实现

void tetromino_rotate_with_wall_kick(Tetromino* t, Board* board) {
    Tetromino temp = *t;
    tetromino_rotate(&temp);  // 旋转
    
    // 如果碰撞,尝试左右踢
    if (board_check_collision(board, &temp)) {
        temp.x -= 1;  // 尝试向左踢
        if (board_check_collision(board, &temp)) {
            temp.x += 2;  // 尝试向右踢
            if (board_check_collision(board, &temp)) {
                // 都失败了,不旋转
                return;
            }
        }
    }
    
    // 应用旋转
    *t = temp;
}

5.9 调试技巧

可视化碰撞检测

// 在碰撞检测函数中添加调试输出
bool tetromino_check_collision(Tetromino* t, Board* board) {
    for (int i = 0; i < 4; i++) {
        for (int j = 0; j < 4; j++) {
            if (t->shape[i][j]) {
                int x = t->x + j;
                int y = t->y + i;
                
                printf("检查 (%d,%d) 形状 [%d][%d]=%d\n", 
                       x, y, i, j, t->shape[i][j]);
                
                if (x < 0 || x >= BOARD_WIDTH) {
                    printf("碰撞:超出边界 x=%d\n", x);
                    return true;
                }
                // ...
            }
        }
    }
    return false;
}

输出示例:

检查 (5,0) 形状 [0][0]=0
检查 (6,0) 形状 [0][1]=0
检查 (5,1) 形状 [1][0]=1
检查 (6,1) 形状 [1][1]=1
碰撞:超出边界 x=10

✅ 本课检查清单


📝 小作业

  1. 在碰撞检测函数中添加调试输出,观察每次检查的坐标

  2. 修改碰撞检测,添加”顶边界”检查(y < 0 时也检测碰撞),会发生什么?

  3. 实现简单的墙踢功能:当旋转碰撞时,尝试向左或向右移动 1 格


下一课:方块旋转