理解俄罗斯方块的核心数据结构设计,学会如何用 C 语言表示游戏元素。
数据结构 = 数据的组织方式
好的数据结构设计:
差的数据结构设计:
// 虽然本项目没单独定义,但理解这个概念很重要
typedef struct {
int x;
int y;
} Point;
为什么需要 Point?
// 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) { ... } // 代码自文档化
枚举的好处:
// 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;
方案对比:
方案 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
// board.h
#define BOARD_WIDTH 10
#define BOARD_HEIGHT 20
typedef struct {
int cells[BOARD_HEIGHT][BOARD_WIDTH];
} Board;
这是俄罗斯方块的标准尺寸,由游戏发明者阿列克谢·帕基特诺夫确定。
历史原因:
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 表示空格?
if (cells[y][x] == 0)// 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?
if (game_over) 比 if (game_over == 1) 更清晰┌─────────────────────────────────────────────┐
│ 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 │
└─────────────────────────────────────────────┘
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];
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 字节
为什么关心内存大小?
计算一个 Tetromino 结构体占用多少字节内存
如果要添加”影子方块”功能,需要在 Game 结构体中添加什么成员?
写出访问 game.current.shape[1][2] 的 C 代码
下一课:游戏循环