c_tetris

第 3 课:数据结构 🏗️

3.1 本课目标

理解俄罗斯方块的核心数据结构设计,学会如何用 C 语言表示游戏元素。


3.2 为什么数据结构很重要?

数据结构 = 数据的组织方式

好的数据结构设计:

差的数据结构设计:


3.3 核心数据结构

3.3.1 点(Point)- 基础坐标

// 虽然本项目没单独定义,但理解这个概念很重要
typedef struct {
    int x;
    int y;
} Point;

为什么需要 Point?


3.3.2 方块类型枚举

// tetromino.h
typedef enum {
    TETRO_I,    // 长条
    TETRO_O,    // 方块
    TETRO_T,    // T 字
    TETRO_S,    // S 字
    TETRO_Z,    // Z 字
    TETRO_J,    // J 字
    TETRO_L,    // L 字
    TETRO_COUNT // 总数(用于随机生成)
} TetrominoType;

为什么用枚举(enum)?

对比 1:用数字

int type = 0;  // 0 是什么?不知道
if (type == 0) { ... }  // 魔法数字,难理解

对比 2:用枚举

TetrominoType type = TETRO_I;  // 一目了然
if (type == TETRO_I) { ... }   // 代码自文档化

枚举的好处:


3.3.3 方块(Tetromino)结构

// tetromino.h
typedef struct {
    TetrominoType type;     // 方块类型(I/O/T/S/Z/J/L)
    int x, y;               // 位置(左上角坐标)
    int rotation;           // 旋转状态(0-3)
    int color;              // 颜色(1-7,对应 ncurses 颜色对)
    int shape[4][4];        // 4x4 形状矩阵
} Tetromino;

为什么用 4x4 矩阵表示形状?

方案对比:

方案 1:存储每个方块的位置

// I 方块

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


// 问题:每种方块都要单独定义,旋转复杂

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

// I 方块(0 度)
int shape[4][4] = {
    {0,0,0,0},
    {1,1,1,1},
    {0,0,0,0},
    {0,0,0,0}
};

// 优点:
// - 统一表示所有 7 种方块
// - 旋转简单(矩阵旋转)
// - 碰撞检测方便(遍历 16 个格子)

形状矩阵可视化

T 方块 - 4 个旋转状态:

旋转 0:          旋转 90:
0 0 0 0          0 1 0 0
1 1 1 0    →     1 1 0 0
0 1 0 0          0 1 0 0
0 0 0 0          0 0 0 0

旋转 180:        旋转 270:
0 1 0 0          0 0 1 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

3.3.4 游戏板(Board)结构

// board.h
#define BOARD_WIDTH   10
#define BOARD_HEIGHT  20

typedef struct {
    int cells[BOARD_HEIGHT][BOARD_WIDTH];
} Board;

为什么是 10x20?

这是俄罗斯方块的标准尺寸,由游戏发明者阿列克谢·帕基特诺夫确定。

历史原因:

数组内存布局

cells[20][10] 在内存中的样子:

 0:  [0][0] [0][1] [0][2] ... [0][9]
 1:  [1][0] [1][1] [1][2] ... [1][9]
...
 19: [19][0] [19][1] [19][2] ... [19][9]

内存地址:连续存储
┌─────────────────────────────────┐
  0 (10  int)   1 (10  int)  ... 
└─────────────────────────────────┘

单元格值的含义

cells[y][x] = 0;   // 空格
cells[y][x] = 1;   // I 方块(青色)
cells[y][x] = 2;   // O 方块(黄色)
cells[y][x] = 3;   // T 方块(紫色)
// ... 以此类推

为什么用 0 表示空格?


3.3.5 游戏(Game)结构

// game.h
typedef struct {
    Board board;            // 游戏板(20x10 数组)
    Tetromino current;      // 当前下落的方块
    Tetromino next;         // 下一个方块(预览)
    Tetromino hold;         // 保留的方块
    Tetromino ghost;        // 影子方块(预测落点)
    
    bool has_held;          // 本回合是否已使用保留
    
    int score;              // 当前分数
    int level;              // 当前等级
    int lines_cleared;      // 累计消除行数
    int lines_to_level;     // 距离下一级还需消除的行数
    
    int drop_interval;      // 下落间隔(毫秒)
    
    bool running;           // 游戏是否运行
    bool paused;            // 是否暂停
    bool game_over;         // 游戏是否结束
} Game;

为什么把所有状态放在一个结构体?

方案对比:

方案 1:全局变量(不推荐)

// 全局变量 - 糟糕的做法
Board g_board;
Tetromino g_current;
int g_score;
int g_level;
// ... 十几个全局变量

// 问题:
// - 命名混乱(都要加 g_ 前缀)
// - 难以保存/加载游戏
// - 无法同时运行多个游戏

方案 2:Game 结构体(推荐)

Game game;
game.board
game.current
game.score
// ... 所有状态都在一个结构体里

// 优点:
// - 命名清晰(game.xxx)
// - 易于保存/加载(复制整个结构体)
// - 可以创建多个游戏实例

布尔类型的使用

#include <stdbool.h>

bool running = true;    // true/false 比 1/0 更清晰
bool paused = false;
bool game_over = false;

为什么用 bool?


3.4 数据结构关系图

┌─────────────────────────────────────────────┐
│                  Game                        │
│  ┌─────────────────────────────────────┐    │
│  │              Board                   │    │
│  │  ┌───────────────────────────┐      │    │
│  │  │  cells[20][10]            │      │    │
│  │  │  [0]=0 [1]=0 [2]=5 ...    │      │    │
│  │  └───────────────────────────┘      │    │
│  └─────────────────────────────────────┘    │
│                                              │
│  ┌─────────────┐  ┌─────────────┐           │
│  │  current    │  │    next     │           │
│  │  Tetromino  │  │  Tetromino  │           │
│  │  type=I     │  │  type=T     │           │
│  │  x=5, y=0   │  │  x=5, y=0   │           │
│  │  shape[4][4]│  │  shape[4][4]│           │
│  └─────────────┘  └─────────────┘           │
│                                              │
│  score=1250  level=5  running=true          │
└─────────────────────────────────────────────┘

3.5 实际应用示例

初始化方块

void tetromino_init(Tetromino* t, TetrominoType type) {
    t->type = type;
    t->x = BOARD_WIDTH / 2 - 2;  // 居中
    t->y = 0;
    t->rotation = 0;
    t->color = type + 1;  // 每种方块不同颜色
    
    // 根据类型设置形状
    memcpy(t->shape, SHAPES[type][0], sizeof(t->shape));
}

访问嵌套结构

Game* game = game_create();

// 访问 Board 中的 cells
game->board.cells[10][5] = 1;

// 访问当前方块的位置
int x = game->current.x;
int y = game->current.y;

// 访问当前方块的形状
int shape_value = game->current.shape[0][0];

3.6 内存大小计算

printf("Tetromino 大小:%zu 字节\n", sizeof(Tetromino));
// 输出:Tetromino 大小:80 字节
// 计算:4(type) + 8(x,y) + 4(rotation) + 4(color) + 64(shape[4][4]) = 84 字节

printf("Board 大小:%zu 字节\n", sizeof(Board));
// 输出:Board 大小:800 字节
// 计算:20 * 10 * 4(int) = 800 字节

printf("Game 大小:%zu 字节\n", sizeof(Game));
// 输出:Game 大小:约 1200 字节

为什么关心内存大小?


✅ 本课检查清单


📝 小作业

  1. 计算一个 Tetromino 结构体占用多少字节内存

  2. 如果要添加”影子方块”功能,需要在 Game 结构体中添加什么成员?

  3. 写出访问 game.current.shape[1][2] 的 C 代码


下一课:游戏循环