c_tetris

第 6 课:方块旋转 🔄

6.1 本课目标

理解矩阵旋转的原理,学会如何实现方块的 90 度旋转。


6.2 旋转的数学原理

顺时针旋转 90 度公式

核心公式:

新位置 [j][3-i] = 原位置 [i][j]

图解:

原矩阵 (2x2 示例):        旋转后:
[0][0]  [0][1]           [1][0]  [0][0]
[1][0]  [1][1]    →      [1][1]  [0][1]

规律:
- [0][0] → [0][1]  (第 0 行第 0 列 → 第 0 行第 1 列)
- [0][1] → [1][1]  (第 0 行第 1 列 → 第 1 行第 1 列)
- [1][0] → [0][0]  (第 1 行第 0 列 → 第 0 行第 0 列)
- [1][1] → [1][0]  (第 1 行第 1 列 → 第 1 行第 0 列)

4x4 矩阵旋转示例

T 方块旋转 90 度:

旋转前 (0 度):          旋转后 (90 度):
0 0 0 0                 0 0 1 0
1 1 1 0        →        0 1 1 0
0 1 0 0                 0 0 1 0
0 0 0 0                 0 0 0 0

坐标变换:
[1][0]=1 → [0][2]=1
[1][1]=1 → [1][2]=1
[1][2]=1 → [2][2]=1
[2][1]=1 → [1][1]=1

6.3 代码实现

旋转函数

// tetromino.c
void tetromino_rotate(Tetromino* t) {
    int temp[4][4];
    int old_shape[4][4];
    
    // 1. 保存原形状
    memcpy(old_shape, t->shape, sizeof(t->shape));
    
    // 2. 顺时针旋转 90 度
    for (int i = 0; i < 4; i++) {
        for (int j = 0; j < 4; j++) {
            temp[j][3-i] = t->shape[i][j];
        }
    }
    
    // 3. 应用旋转
    memcpy(t->shape, temp, sizeof(t->shape));
    t->rotation = (t->rotation + 1) % 4;
}

代码详解

步骤 1:为什么需要 temp 数组?

// ❌ 错误做法:直接修改原数组
for (int i = 0; i < 4; i++) {
    for (int j = 0; j < 4; j++) {
        t->shape[j][3-i] = t->shape[i][j];
        // 问题:后面的迭代会用到已修改的值!
    }
}

// ✅ 正确做法:用临时数组
int temp[4][4];
// 先写入 temp,最后再复制回 t->shape

步骤 2:旋转公式解析

temp[j][3-i] = t->shape[i][j];

// 示例:i=1, j=0
// t->shape[1][0] = 1 (T 方块的左边)
// temp[0][3-1] = temp[0][2] = 1
// 结果:[1][0] → [0][2]

步骤 3:更新旋转状态

t->rotation = (t->rotation + 1) % 4;

// 旋转状态:0 → 1 → 2 → 3 → 0 → ...
// % 4 确保在 0-3 之间循环

6.4 旋转的实际应用

在输入处理中调用

// input.c
case KEY_UP:
case 'k':
case 'w':
    {
        Tetromino temp = game->current;
        tetromino_rotate(&temp);
        
        // 检查旋转后是否碰撞
        if (!board_check_collision(&game->board, &temp)) {
            game->current = temp;
        }
    }
    break;

为什么旋转前要先检查碰撞?

场景:方块在墙边

旋转前:          直接旋转:
┌──────┐         ┌──────┐
│█     │         │█     │  ← 旋转后超出边界!
│███   │   →     │███   │
└──────┘         └──────┘

正确做法:
1. 创建临时副本
2. 旋转副本
3. 检查副本是否碰撞
4. 如果不碰撞,应用旋转
5. 如果碰撞,保持原样

6.5 四种旋转状态

旋转状态枚举

// 虽然代码中用 0-3 表示,但理解这四个状态很重要
typedef enum {
    ROTATION_0,    // 0 度(初始)
    ROTATION_90,   // 顺时针 90 度
    ROTATION_180,  // 180 度
    ROTATION_270   // 顺时针 270 度(或逆时针 90 度)
} RotationState;

连续旋转示例

T 方块连续旋转:

0 度:              90 度:
0 0 0 0             0 0 1 0
1 1 1 0      →      0 1 1 0
0 1 0 0             0 0 1 0
0 0 0 0             0 0 0 0

180 度:            270 度:
0 1 0 0             0 0 0 0
1 1 1 0      →      0 1 1 0
0 0 0 0             0 0 1 0
0 0 0 0             0 0 0 0

再转一次回到 0 度...

6.6 特殊情况:O 方块

O 方块不需要旋转

// O 方块的形状
int O_shape[4][4] = {
    {0, 0, 0, 0},
    {0, 1, 1, 0},
    {0, 1, 1, 0},
    {0, 0, 0, 0}
};

// 旋转 90 度后:
int O_rotated[4][4] = {
    {0, 0, 0, 0},
    {0, 1, 1, 0},
    {0, 1, 1, 0},
    {0, 0, 0, 0}
};

// 结果:完全一样!

优化: 可以为 O 方块跳过旋转检查

void tetromino_rotate(Tetromino* t) {
    // O 方块不需要旋转
    if (t->type == TETRO_O) {
        return;
    }
    
    // ... 正常旋转逻辑
}

6.7 逆时针旋转

如果需要逆时针旋转

逆时针 90 度公式:

新位置 [3-j][i] = 原位置 [i][j]

代码:

void tetromino_rotate_counter_clockwise(Tetromino* t) {
    int temp[4][4];
    
    for (int i = 0; i < 4; i++) {
        for (int j = 0; j < 4; j++) {
            temp[3-j][i] = t->shape[i][j];
        }
    }
    
    memcpy(t->shape, temp, sizeof(t->shape));
    t->rotation = (t->rotation + 3) % 4;  // 逆时针 = 顺时针 3 次
}

6.8 调试技巧

可视化旋转

// 旋转前后打印形状
void print_shape(int shape[4][4]) {
    for (int i = 0; i < 4; i++) {
        for (int j = 0; j < 4; j++) {
            printf("%d ", shape[i][j]);
        }
        printf("\n");
    }
    printf("\n");
}

// 使用
printf("旋转前:\n");
print_shape(t->shape);

tetromino_rotate(t);

printf("旋转后:\n");
print_shape(t->shape);

✅ 本课检查清单


📝 小作业

  1. 手动计算 T 方块从 0 度旋转到 90 度后,每个 1 的新位置

  2. 实现逆时针旋转函数,并测试

  3. 为 O 方块添加旋转优化(跳过旋转)


下一课:消行逻辑