俄罗斯方块终端游戏实现 ------ 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 强制终止)
- 视觉效果:
- 红色边界框
- 白色活动与固定方块 "[]"
- 实时分数显示
- 方块自动下落,支持操作
- 消行后上方方块下移,分数增加
✅ 最终理想结果:游戏流畅运行,玩家可通过方向键控制方块,消除行得分,无崩溃或逻辑错误。