理解碰撞检测的原理,学会如何判断方块能否移动到某个位置。
碰撞检测 = 判断物体是否重叠
在俄罗斯方块中,碰撞检测用于回答这些问题:
┌────────┐
│ ██ │ ← 方块
│ ██ │
└────────┘
↓ 能否向左移动?
┌────────┐
│██ │ ← 可以!左边没东西
│██ │
└────────┘
┌────────┐
│ ██ │ ← 方块
│ ██ │
└────────┘
↓ 能否向下移动?
┌──┐
│██│ ← 不能!下面有方块
│██│
└──┘
// ❌ 错误做法:不检查碰撞
game->current.x--; // 直接移动
// 结果:方块穿墙而出!
后果:
// ✅ 正确做法:先检查,再移动
game->current.x--;
if (board_check_collision(&game->board, &game->current)) {
// 碰撞了,退回
game->current.x++;
}
if (x < 0) {
return true; // 碰撞
}
图示:
正常: 碰撞:
┌────┐ ┌────┐
│ ██ │ │█ │ ← x=-1,超出左边界
│ ██ │ │█ │
└────┘ └────┘
x=0 x=-1 ❌
if (x >= BOARD_WIDTH) {
return true; // 碰撞
}
图示:
正常: 碰撞:
┌────────┐ ┌────────┐
│ ██ │ │ ██│ ← x=10,超出右边界
│ ██ │ │ ██│ (BOARD_WIDTH=10)
└────────┘ └────────┘
x=8 x=10 ❌
if (y >= BOARD_HEIGHT) {
return true; // 碰撞
}
图示:
正常: 碰撞:
┌────────┐ ┌────────┐
│ │ │ │
│ ██ │ │ │
│ ██ │ │ ██ │ ← y=20,超出底边界
└────────┘ │ ██ │ (BOARD_HEIGHT=20)
y=18 └────────┘
y=20 ❌
if (y >= 0 && board[y][x] != 0) {
return true; // 碰撞
}
图示:
正常: 碰撞:
┌────────┐ ┌────────┐
│ │ │ │
│ ██ │ │ ██ │ ← 当前方块
│ │ │ ██ │
│ │ │███████│ ← 已固定方块 (board[y][x]!=0)
└────────┘ └────────┘
// 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; // 没有碰撞
}
问题: 方块形状不规则,如何检查所有部分?
方案对比:
方案 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 的空位置
方块的 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
为什么需要转换?
// 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++ 保持新位置
(退回)
// 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);
// 生成新方块...
}
}
为什么下落碰撞后要锁定?
// input.c
case KEY_UP:
{
// 保存原状态
Tetromino temp = game->current;
// 尝试旋转
tetromino_rotate(&temp);
// 检查旋转后是否碰撞
if (!board_check_collision(&game->board, &temp)) {
// 可以旋转,应用
game->current = temp;
}
// 否则不旋转(保持原样)
}
break;
为什么旋转前要先检查?
当方块在墙边旋转时,通过微调位置来”踢”开墙壁:
旋转前: 旋转后(墙踢):
┌──────┐ ┌──────┐
│█ │ │ █ │ ← 向右踢 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;
}
// 在碰撞检测函数中添加调试输出
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
在碰撞检测函数中添加调试输出,观察每次检查的坐标
修改碰撞检测,添加”顶边界”检查(y < 0 时也检测碰撞),会发生什么?
实现简单的墙踢功能:当旋转碰撞时,尝试向左或向右移动 1 格
下一课:方块旋转