俄罗斯方块终端游戏实现 —— C语言系统编程与终端控制
俄罗斯方块终端游戏实现 —— C语言系统编程与终端控制
本项目是一个使用 C语言 实现的简易 俄罗斯方块(Tetris)终端游戏,完整涵盖了 系统调用、终端控制、随机数、二维数组地图、碰撞检测、图形绘制、非阻塞输入、光标控制、清屏与重绘、方块旋转与锁定、行消除与计分机制 等多个核心知识点。
📌 项目依赖头文件与宏定义
#include <fcntl.h> // 用于 fcntl() 函数,实现非阻塞输入检测
#include <stdio.h> // 标准输入输出,printf, putchar 等
#include <stdlib.h> // rand(), srand() 生成随机方块
#include <termios.h> // 终端属性控制,实现 getch() 与 kbhit()
#include <time.h> // time() 为随机数播种
#include <unistd.h> // usleep() 实现游戏帧率控制#define H 22 // 游戏地图高度(行数),含边界
#define W 12 // 游戏地图宽度(列数),含边界
✅ 理想结果:程序编译无警告,头文件正确包含,宏定义生效,地图尺寸为 22 行 × 12 列。
🧱 全局变量与方块结构体定义
int map[H][W]; // 二维地图数组,1=边界,2=固定方块,0=空白
int score = 0; // 玩家得分,每消除一行 +100 分// 当前方块结构体:位置(x,y)、类型(0-6)、旋转状态(0-3)
struct Block
{int x;int y;int type; // 0-6 对应 7 种方块类型int rotation; // 0-3 对应 4 种旋转状态
};
struct Block currentBlock; // 当前活动方块实例
✅ 理想结果:全局变量初始化成功,currentBlock 可在函数中被正确赋值和读取。
🔲 七种方块的 4×4×4 三维旋转矩阵定义
// 7种方块,每种4种旋转,每种旋转为4×4矩阵(实际使用中心2×4或3×3区域)
int BLOCKS[7][4][4][4] = {// I 方块 (长条形) —— 4种旋转状态相同或镜像{{{0, 0, 0, 0}, {1, 1, 1, 1}, {0, 0, 0, 0}, {0, 0, 0, 0}},{{0, 1, 0, 0}, {0, 1, 0, 0}, {0, 1, 0, 0}, {0, 1, 0, 0}},{{0, 0, 0, 0}, {1, 1, 1, 1}, {0, 0, 0, 0}, {0, 0, 0, 0}},{{0, 1, 0, 0}, {0, 1, 0, 0}, {0, 1, 0, 0}, {0, 1, 0, 0}}},// O 方块 (正方形) —— 4种旋转完全相同{{{0, 0, 0, 0}, {0, 1, 1, 0}, {0, 1, 1, 0}, {0, 0, 0, 0}},{{0, 0, 0, 0}, {0, 1, 1, 0}, {0, 1, 1, 0}, {0, 0, 0, 0}},{{0, 0, 0, 0}, {0, 1, 1, 0}, {0, 1, 1, 0}, {0, 0, 0, 0}},{{0, 0, 0, 0}, {0, 1, 1, 0}, {0, 1, 1, 0}, {0, 0, 0, 0}}},// T 方块 (T 字形) —— 4种旋转状态{{{0, 0, 0, 0}, {0, 1, 0, 0}, {1, 1, 1, 0}, {0, 0, 0, 0}},{{0, 1, 0, 0}, {0, 1, 1, 0}, {0, 1, 0, 0}, {0, 0, 0, 0}},{{0, 0, 0, 0}, {1, 1, 1, 0}, {0, 1, 0, 0}, {0, 0, 0, 0}},{{0, 1, 0, 0}, {0, 1, 1, 0}, {0, 1, 0, 0}, {0, 0, 0, 0}}},// S 方块 (S 形) —— 2种独特旋转,重复两次{{{0, 0, 0, 0}, {0, 1, 1, 0}, {1, 1, 0, 0}, {0, 0, 0, 0}},{{0, 1, 0, 0}, {0, 1, 1, 0}, {0, 0, 1, 0}, {0, 0, 0, 0}},{{0, 0, 0, 0}, {0, 1, 1, 0}, {1, 1, 0, 0}, {0, 0, 0, 0}},{{0, 1, 0, 0}, {0, 1, 1, 0}, {0, 0, 1, 0}, {0, 0, 0, 0}}},// Z 方块 (Z 形) —— 2种独特旋转,重复两次{{{0, 0, 0, 0}, {1, 1, 0, 0}, {0, 1, 1, 0}, {0, 0, 0, 0}},{{0, 0, 1, 0}, {0, 1, 1, 0}, {0, 1, 0, 0}, {0, 0, 0, 0}},{{0, 0, 0, 0}, {1, 1, 0, 0}, {0, 1, 1, 0}, {0, 0, 0, 0}},{{0, 0, 1, 0}, {0, 1, 1, 0}, {0, 1, 0, 0}, {0, 0, 0, 0}}},// L 方块 (L 形) —— 4种旋转状态{{{0, 0, 0, 0}, {1, 1, 1, 0}, {1, 0, 0, 0}, {0, 0, 0, 0}},{{0, 1, 0, 0}, {0, 1, 0, 0}, {0, 1, 1, 0}, {0, 0, 0, 0}},{{0, 0, 0, 0}, {1, 0, 0, 0}, {1, 1, 1, 0}, {0, 0, 0, 0}},{{0, 1, 1, 0}, {0, 1, 0, 0}, {0, 1, 0, 0}, {0, 0, 0, 0}}},// J 方块 (J 形) —— 4种旋转状态{{{0, 0, 0, 0}, {1, 1, 1, 0}, {0, 0, 1, 0}, {0, 0, 0, 0}},{{0, 1, 0, 0}, {0, 1, 0, 0}, {1, 1, 0, 0}, {0, 0, 0, 0}},{{0, 0, 0, 0}, {0, 0, 1, 0}, {1, 1, 1, 0}, {0, 0, 0, 0}},{{0, 1, 1, 0}, {0, 1, 0, 0}, {0, 1, 0, 0}, {0, 0, 0, 0}}}};
✅ 理想结果:所有方块形状与旋转状态定义正确,游戏中可随机生成并正确旋转。
🗺️ 地图初始化函数 —— initMap()
void initMap() // 初始化游戏地图,设置边界
{int x, y;for (y = 0; y < H; ++y){for (x = 0; x < W; ++x){if (x == 0 || x == W - 1 || y == H - 1) // 左右边界和底边界{map[y][x] = 1; // 边界用 1 标记,绘制为红色}else{map[y][x] = 0; // 内部区域初始为空白}}}
}
✅ 理想结果:地图四周和底部显示红色边界,内部为空白,为游戏提供碰撞检测基础。
🎲 创建新方块 —— createNewBlock()
void createNewBlock()
{currentBlock.type = rand() % 7; // 随机选择 0~6 类型currentBlock.rotation = rand() % 4; // 随机选择 0~3 旋转状态// 新方块出生在顶部中央(x 偏移 -2 以居中 4×4 方块)currentBlock.x = W / 2 - 2;currentBlock.y = 0;
}
✅ 理想结果:每次调用生成一个随机类型和旋转的新方块,出现在地图顶部中央。
🚧 碰撞检测 —— checkCollision()
int checkCollision(int x, int y, int type, int rotation)
// 检测指定位置、类型、旋转的方块是否与边界或固定块碰撞
{int i, j; // 遍历 4×4 方块矩阵for (i = 0; i < 4; ++i){for (j = 0; j < 4; ++j){if (BLOCKS[type][rotation][i][j] != 0) // 如果该位置是方块实体{int nx = x + j; // 计算在地图中的实际 x 坐标int ny = y + i; // 计算在地图中的实际 y 坐标// 检测是否越界:左、右、下、上边界(上边界允许负值,但地图不绘制)if (nx < 0 || nx >= W || ny >= H || ny < 0){return 1; // 发生碰撞}// 检测是否与已固定的方块(map[][] == 2)碰撞if (ny >= 0 && map[ny][nx] != 0) // 注意:只检测地图内区域{return 1;}}}}return 0; // 无碰撞
}
✅ 理想结果:方块移动或旋转前调用,若返回 1 则禁止操作,防止穿墙或重叠。
🔒 锁定方块 —— lockBlock()
void lockBlock()
// 将当前活动方块写入地图,变为固定块(值为 2)
{int i, j;for (i = 0; i < 4; ++i){for (j = 0; j < 4; ++j){if (BLOCKS[currentBlock.type][currentBlock.rotation][i][j] != 0){int nx = currentBlock.x + j;int ny = currentBlock.y + i;// 确保坐标在地图范围内再写入if (ny >= 0 && ny < H && nx >= 0 && nx < W){map[ny][nx] = 2; // 固定方块标记为 2}}}}
}
✅ 理想结果:方块落地后变为地图的一部分,不可再移动,后续方块与其碰撞。
🧹 行消除与下移 —— checkLineClear()
void checkLineClear()
{int y = H - 2; // 从倒数第二行开始检查(最后一行是边界)int lines_cleared = 0;while (y >= 0) // 从底向上逐行检查{ int is_full = 1; // 假设当前行是满的// 检查游戏区域(x=1 到 W-2)是否全为固定块(值为2)for (int x = 1; x < W - 1; x++){if (map[y][x] == 0) // 存在空白{is_full = 0;break;}}if (is_full){++lines_cleared;// 将当前行及以上所有行向下移动一行for (int y_tmp = y; y_tmp > 0; y_tmp--){for (int x = 0; x < W; x++){map[y_tmp][x] = map[y_tmp - 1][x]; // 下移}}// 清空最顶行for (int x = 0; x < W; x++){map[0][x] = 0;}// 注意:y 不自减,因为下移后原 y 行是新内容,需重新检查}else{--y; // 当前行不满,检查上一行}}// 更新分数:每消除一行 +100 分score += lines_cleared * 100;
}
✅ 理想结果:满行被消除,上方行下移,顶部清空,分数按行数增加。
🖱️ 终端光标控制函数
void gotoxy(int x, int y)
// 使用 VT100 转义序列定位光标到指定位置(y行, x列)
{printf("\033[%d;%dH", y + 1, x * 2 + 1); // 行号+1,列号*2+1(因每个方块占2字符宽)fflush(stdout); // 强制刷新输出缓冲区
}void hideCursor()
// 隐藏终端光标,提升游戏沉浸感
{printf("\033[?25l"); // VT100 隐藏光标指令fflush(stdout);
}void showCursor()
// 游戏结束后显示光标
{printf("\033[?25h"); // VT100 显示光标指令fflush(stdout);
}
✅ 理想结果:光标可精确定位到指定位置绘制方块,游戏运行时光标隐藏,退出后恢复。
⌨️ 终端非阻塞输入函数 —— getch() 与 kbhit()
int getch(void)
// 从终端读取一个字符,不回显(非缓冲、非回显模式)
{struct termios oldt, newt;int ch;tcgetattr(STDIN_FILENO, &oldt); // 获取当前终端属性newt = oldt;newt.c_lflag &= ~(ICANON | ECHO); // 关闭缓冲与回显tcsetattr(STDIN_FILENO, TCSANOW, &newt); // 应用新设置ch = getchar(); // 读取字符tcsetattr(STDIN_FILENO, TCSANOW, &oldt); // 恢复原设置return ch;
}int kbhit(void)
// 检测是否有按键按下,非阻塞(立即返回)
{struct termios oldt, newt;int ch;int oldf;tcgetattr(STDIN_FILENO, &oldt);newt = oldt;newt.c_lflag &= ~(ICANON | ECHO);tcsetattr(STDIN_FILENO, TCSANOW, &newt);oldf = fcntl(STDIN_FILENO, F_GETFL, 0);fcntl(STDIN_FILENO, F_SETFL, oldf | O_NONBLOCK); // 设置非阻塞读取ch = getchar(); // 尝试读取tcsetattr(STDIN_FILENO, TCSANOW, &oldt);fcntl(STDIN_FILENO, F_SETFL, oldf); // 恢复原设置if (ch != EOF){ungetc(ch, stdin); // 将字符放回输入流,供后续 getch() 使用return 1; // 有按键}return 0; // 无按键
}
✅ 理想结果:kbhit() 立即返回是否有按键,getch() 读取按键值,支持方向键(ESC [ D/C/A)。
🖼️ 地图绘制函数 —— drawMap()
void drawMap()
{printf("\033[2J"); // VT100 清屏指令gotoxy(0, 0); // 光标归位到左上角int y = 0, x = 0;for (y = 0; y < H; ++y){for (x = 0; x < W; ++x){// 如果当前格子属于活动方块范围if (x >= currentBlock.x && x < currentBlock.x + 4 && y >= currentBlock.y && y < currentBlock.y + 4){int rel_x = x - currentBlock.x;int rel_y = y - currentBlock.y;// 如果方块矩阵中该位置为实体,则绘制活动方块if (BLOCKS[currentBlock.type][currentBlock.rotation][rel_y][rel_x]){printf("[]");continue;}}// 绘制地图内容:边界(红色)、固定块(白色)、空白(空格)if (map[y][x] == 1){printf("\033[1;31m[]\033[0m"); // 红色边界}else if (map[y][x] == 2){printf("[]"); // 固定方块}else{printf(" "); // 空白}}putchar('\n'); // 换行}// 在右侧显示当前分数gotoxy(W + 1, 5);printf("Score: %d", score);fflush(stdout); // 强制输出
}
✅ 理想结果:每帧清屏重绘,活动方块叠加在地图上,边界红色,分数实时显示。
🎮 主游戏循环 —— main()
int main(void)
{srand(time(NULL)); // 用当前时间初始化随机数种子initMap(); // 初始化带边界的地图createNewBlock(); // 创建第一个活动方块hideCursor(); // 隐藏终端光标while (1) // 无限游戏循环{if (kbhit()) // 检测是否有按键{char key = getch(); // 读取按键if (key == 27) // 如果是 ESC 键(方向键序列开头){getch(); // 读取 '['key = getch(); // 读取方向字符if (key == 'D') // 左方向键{// 检测左移是否碰撞,无碰撞则移动if (checkCollision(currentBlock.x - 1, currentBlock.y, currentBlock.type, currentBlock.rotation) == 0){--currentBlock.x;}}else if (key == 'C') // 右方向键{if (checkCollision(currentBlock.x + 1, currentBlock.y, currentBlock.type, currentBlock.rotation) == 0){++currentBlock.x;}}else if (key == 'A') // 上方向键(旋转){int new_rotation = (currentBlock.rotation + 1) % 4; // 循环旋转if (checkCollision(currentBlock.x, currentBlock.y, currentBlock.type, new_rotation) == 0){currentBlock.rotation = new_rotation;}}}}// 尝试下落一格if (checkCollision(currentBlock.x, currentBlock.y + 1, currentBlock.type, currentBlock.rotation) == 0){++currentBlock.y; // 无碰撞,下落}else // 下落发生碰撞{lockBlock(); // 锁定当前方块到地图checkLineClear(); // 检查并消除满行createNewBlock(); // 生成新方块}drawMap(); // 绘制当前帧usleep(500000); // 延迟 0.5 秒(500毫秒),控制下落速度}showCursor(); // 理论上不会执行(因 while(1)),但保留用于规范return 0;
}
✅ 理想结果:游戏持续运行,方块自动下落,支持左右移动和旋转,落地后锁定,消除行并计分,新方块生成,帧率稳定。
🧩 知识点归纳
类别 知识点 对应函数/代码段
系统编程 终端属性控制、非阻塞输入 getch(), kbhit(), termios.h, fcntl.h
图形界面 VT100 转义序列、光标定位、清屏、颜色 gotoxy(), hideCursor(), \033[2J, \033[1;31m
游戏逻辑 碰撞检测、方块锁定、行消除、计分 checkCollision(), lockBlock(), checkLineClear()
数据结构 三维数组存储方块形态、二维地图 BLOCKS[7][4][4][4], map[H][W]
控制流程 主循环、条件移动、状态更新 main() 中 while 循环与条件分支
随机机制 随机生成方块类型与旋转 srand(), rand() % 7, rand() % 4
时间控制 控制帧率与下落速度 usleep(500000)
🏁 运行环境与理想结果
- 操作系统:Linux / macOS(支持 VT100 终端)
- 编译命令:gcc -o tetris main.c
- 运行命令:./tetris
- 控制键:
- ←:左移(ESC [ D)
- →:右移(ESC [ C)
- ↑:旋转(ESC [ A)
- ESC:退出(未实现,需 Ctrl+C 强制终止)
- 视觉效果:
- 红色边界框
- 白色活动与固定方块 “[]”
- 实时分数显示
- 方块自动下落,支持操作
- 消行后上方方块下移,分数增加
✅ 最终理想结果:游戏流畅运行,玩家可通过方向键控制方块,消除行得分,无崩溃或逻辑错误。