1. 项目概述为什么C小游戏是绝佳的练手项目最近在整理硬盘翻出来一堆大学时期写的C小游戏代码像贪吃蛇、俄罗斯方块、五子棋这些。看着这些略显稚嫩但结构清晰的源码突然觉得对于正在学习C的朋友来说没有什么比亲手实现一个经典小游戏更能巩固基础、提升兴趣的了。很多人学C啃完了语法书面对“黑框框”的控制台和一堆抽象概念依然不知道如何下手写一个完整的程序。这些小游戏项目恰恰是连接语法知识与实际应用的绝佳桥梁。一个完整的C小游戏麻雀虽小五脏俱全。它几乎能覆盖C核心语法的所有方面从基本的数据类型、流程控制循环、分支到函数封装、数组与字符串处理再到面向对象思想类与对象、标准模板库STL的使用甚至可能涉及简单的算法如碰撞检测、AI逻辑和Windows API/图形库的基础调用。通过实现它你能真切地感受到变量如何存储游戏状态循环如何驱动游戏帧函数如何模块化代码类如何抽象游戏中的实体比如蛇、方块、棋子。这比做一百道孤立的练习题要有效得多。今天我就以几个最经典的案例——控制台版本的贪吃蛇、俄罗斯方块和猜数字游戏为例把完整的源码和实现思路分享出来。你可以直接复制代码去运行、去修改、去折腾。我的目的不是让你“抄作业”而是希望通过剖析这些源码让你明白一个可运行的C程序是如何从零搭建的其中有哪些关键的编程思维和调试技巧。无论你是刚学完C语法的新手还是想重温基础的老鸟相信都能从中获得启发。2. 核心设计思路控制台游戏的骨架与心跳在开始看具体代码之前我们必须先理解这类控制台小游戏的通用架构。它们不像现代游戏引擎那样复杂但其核心逻辑是相通的可以提炼出一个清晰的模型。2.1 游戏循环驱动一切的引擎所有实时游戏的核心都是一个“游戏循环”Game Loop。这是一个无限循环在每一轮循环中程序按顺序完成几件关键事情处理输入检测玩家是否按下了按键如方向键、空格键。更新游戏状态根据输入和内部逻辑计算游戏中所有对象的新状态如蛇头的新位置、方块的新下落位置。渲染输出将最新的游戏状态绘制到屏幕上在控制台中通常是通过清屏后重新打印字符来实现。控制帧率让每次循环之间有一个短暂的延迟以免游戏运行过快同时也能降低CPU占用。在C控制台环境中我们通常用一个while(true)或while(!gameOver)循环来构建这个引擎。处理输入依赖于conio.h中的_kbhit()和_getch()函数Windows特有Linux/macOS需用其他库如ncurses。渲染则通过system(“cls”)清屏后用cout或printf打印二维字符数组来实现游戏画面。注意频繁使用system(“cls”)会导致屏幕闪烁。对于更流畅的体验可以考虑使用Windows API直接操作控制台缓冲区但这对于入门项目来说复杂度较高。我们目前的实现以清晰易懂为首要目标。2.2 状态管理与数据表示游戏中的所有信息都需要用变量和数据结构来存储。这是将游戏规则“翻译”成代码的过程。地图/场景通常用一个二维数组如char map[HEIGHT][WIDTH]表示。数组中的每个元素代表屏幕上的一个“格子”用不同的字符如‘#’表示墙’ ‘表示空地‘*’表示食物来区分。游戏对象如贪吃蛇的蛇身可以用一个存储坐标struct Point {int x; int y;}的数组或std::vector来表示。俄罗斯方块的当前下落形态可以用一个小的二维数组如int shape[4][4]来定义。游戏状态变量如分数int score、游戏是否结束bool gameOver、当前难度/速度int speed等。清晰的数据设计是逻辑清晰的前提。在动手写逻辑之前先花点时间规划好你的数据结构会事半功倍。2.3 模块化设计函数与类的运用不要把所有的代码都堆在main函数里。合理的模块化能让代码易于阅读、调试和扩展。功能函数将特定的功能封装成函数。例如void drawMap(const char map[][WIDTH])负责绘制地图。void processInput(char direction)负责处理键盘输入更新方向。bool checkCollision(const Snake snake, const Point food)负责检测碰撞。类Class当游戏对象有较复杂的状态和行为时使用类来封装是更好的选择。例如可以定义一个Snake类其私有数据成员包括蛇身坐标向量、移动方向等公有成员函数包括move(),grow(),checkSelfCollision()等。这更符合面向对象的思想也使得在游戏中管理多个对象比如未来想加入两条蛇变得更加容易。在接下来的具体游戏中我会演示这两种组织方式你可以对比感受。3. 经典案例一贪吃蛇控制台版完整实现与解析贪吃蛇是理解游戏循环和状态更新的绝佳入门项目。它的规则简单但包含了移动、增长、碰撞检测等核心游戏机制。3.1 数据结构与全局定义我们首先定义游戏所需的基本元素。这里我采用结构体和全局变量结合的方式便于初学者理解整体数据流。#include iostream #include conio.h // 用于键盘输入 #include windows.h // 用于Sleep函数和控制台操作 #include vector #include cstdlib // 用于rand() #include ctime // 用于srand() using namespace std; // 游戏区域常量 const int WIDTH 20; const int HEIGHT 20; // 方向枚举 enum Direction { STOP 0, LEFT, RIGHT, UP, DOWN }; // 坐标点结构体 struct Point { int x; int y; }; // 游戏全局状态 Point food; // 食物位置 vectorPoint snake; // 蛇身vector[0]是蛇头 Direction dir; // 当前移动方向 bool gameOver; int score; int speed; // 控制游戏速度值越大延迟越长速度越慢这里使用std::vectorPoint来存储蛇身因为它可以动态增长蛇吃了食物会变长。Direction枚举让方向判断更直观。speed变量用于控制游戏循环的延迟实现难度调节。3.2 核心函数实现接下来是游戏的各个功能模块。// 初始化游戏 void setup() { gameOver false; dir STOP; // 游戏开始时蛇静止 score 0; speed 100; // 初始延迟100毫秒 // 初始化蛇身长度为3水平放置在中间 snake.clear(); snake.push_back({WIDTH / 2, HEIGHT / 2}); // 蛇头 snake.push_back({WIDTH / 2 - 1, HEIGHT / 2}); snake.push_back({WIDTH / 2 - 2, HEIGHT / 2}); // 随机生成第一个食物 srand(time(0)); food.x rand() % (WIDTH - 2) 1; // 避免生成在边界上 food.y rand() % (HEIGHT - 2) 1; } // 绘制游戏画面 void draw() { system(cls); // 清屏Windows系统命令 // 绘制上边界 for (int i 0; i WIDTH 2; i) cout #; cout endl; for (int y 0; y HEIGHT; y) { for (int x 0; x WIDTH; x) { // 绘制左边界 if (x 0) cout #; // 绘制蛇头 if (x snake[0].x y snake[0].y) cout O; // 绘制蛇身 else if (isSnakeBody(x, y)) // 需要实现isSnakeBody函数 cout o; // 绘制食物 else if (x food.x y food.y) cout F; else cout ; // 空地 // 绘制右边界 if (x WIDTH - 1) cout #; } cout endl; } // 绘制下边界 for (int i 0; i WIDTH 2; i) cout #; cout endl; // 显示分数和提示 cout Score: score endl; cout Press x to quit. endl; } // 辅助函数判断坐标是否为蛇身除蛇头外 bool isSnakeBody(int x, int y) { for (size_t i 1; i snake.size(); i) { // 从1开始跳过蛇头 if (snake[i].x x snake[i].y y) return true; } return false; } // 处理键盘输入 void input() { if (_kbhit()) { // 检查是否有按键按下 switch (_getch()) { // 获取按键字符不显示在屏幕上 case a: if (dir ! RIGHT) dir LEFT; // 防止直接反向 break; case d: if (dir ! LEFT) dir RIGHT; break; case w: if (dir ! DOWN) dir UP; break; case s: if (dir ! UP) dir DOWN; break; case x: gameOver true; break; } } } // 更新游戏逻辑 void logic() { if (dir STOP) return; // 未开始移动 // 1. 移动蛇身从尾部开始每一节移动到前一节的位置 Point prev snake[0]; Point temp; for (size_t i 1; i snake.size(); i) { temp snake[i]; snake[i] prev; prev temp; } // 2. 根据方向移动蛇头 switch (dir) { case LEFT: snake[0].x--; break; case RIGHT: snake[0].x; break; case UP: snake[0].y--; break; case DOWN: snake[0].y; break; } // 3. 检测是否吃到食物 if (snake[0].x food.x snake[0].y food.y) { score 10; // 在蛇尾添加新的一节位置暂时与旧蛇尾重合下次移动时会展开 snake.push_back({snake.back().x, snake.back().y}); // 生成新的食物确保不在蛇身上 do { food.x rand() % (WIDTH - 2) 1; food.y rand() % (HEIGHT - 2) 1; } while (isSnakeBody(food.x, food.y) || (food.x snake[0].x food.y snake[0].y)); // 随着分数增加速度变快延迟减少 if (speed 50) speed - 2; } // 4. 碰撞检测撞墙或撞到自己 // 撞墙 if (snake[0].x 0 || snake[0].x WIDTH || snake[0].y 0 || snake[0].y HEIGHT) { gameOver true; } // 撞自己从第二节开始检查 for (size_t i 1; i snake.size(); i) { if (snake[0].x snake[i].x snake[0].y snake[i].y) { gameOver true; break; } } }3.3 主函数与游戏循环最后用主函数将以上模块串联起来。int main() { setup(); // 初始化 while (!gameOver) { draw(); // 绘制 input(); // 输入 logic(); // 逻辑更新 Sleep(speed); // 控制游戏速度单位毫秒 } // 游戏结束画面 system(cls); cout Game Over! endl; cout Your final score is: score endl; cout Press any key to exit...; _getch(); // 等待一个按键 return 0; }实操心得在logic()函数的移动部分初学者常犯的错误是“从头部开始移动”这会导致整条蛇瞬间变成一节。正确的做法是“从尾部开始每一节移动到前一节的位置”最后再单独移动蛇头。这是贪吃蛇算法的关键点之一。4. 经典案例二俄罗斯方块核心算法剖析俄罗斯方块比贪吃蛇更复杂一些核心在于“方块形态的表示与旋转”以及“消行判断”。理解了这两点就掌握了俄罗斯方块的灵魂。4.1 方块的数据表示与旋转算法俄罗斯方块有7种基本形态I, J, L, O, S, T, Z每种形态有1到4种旋转状态。如何高效地表示和操作它们方法一预定义法推荐给初学者将所有形态的所有旋转状态硬编码在四维数组里。这是最直观的方法。// 定义每个方块是4x4的网格用1表示有格子0表示无格子。 // shapes[方块类型][旋转状态][y][x] int shapes[7][4][4][4] { // I 型 { {{0,0,0,0}, {1,1,1,1}, {0,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,0,0,0}, {1,1,1,1}, {0,0,0,0}}, {{0,1,0,0}, {0,1,0,0}, {0,1,0,0}, {0,1,0,0}} }, // ... 其他J, L, O, S, T, Z形态的定义 }; // 当前下落的方块 int curType; // 当前方块类型索引 (0-6) int curRotation; // 当前旋转状态索引 (0-3) int curX, curY; // 方块左上角在游戏区域中的坐标旋转操作就变成了简单地改变curRotation索引。碰撞检测时只需遍历当前形态的4x4数组检查其中为1的格子映射到游戏地图(gameGrid)上是否越界或重叠。方法二旋转公式法更优雅对于一个以某个点为中心旋转的方块其格子坐标可以通过矩阵旋转公式计算。对于基于原点(0,0)的90度顺时针旋转公式为新x -旧y新y 旧x。你需要先找到方块的“旋转中心”对于大多数方块是其4x4网格的大致中心点然后将每个格子的坐标相对于中心点进行旋转变换。这种方法代码更通用但理解起来稍难。4.2 游戏区域、碰撞与消行游戏区域通常用一个高比如202宽比如10的二维数组int grid[HEIGHT][WIDTH]表示其中0为空1为已固定的方块。碰撞检测在方块尝试移动或旋转前需要预计算其新位置/形态然后遍历其4x4网格中为1的格子检查(curY y, curX x)是否超出地图边界或者该位置在grid中是否为1已占用。消行逻辑当方块固定后从底部开始遍历每一行。如果某一行所有格子都为1则该行可消除。消除后将该行以上的所有行整体向下移动一行。这是一个典型的数组操作注意要从下往上处理以免数据覆盖出错。void clearLines() { for (int y HEIGHT - 1; y 0; y--) { bool lineIsFull true; for (int x 0; x WIDTH; x) { if (grid[y][x] 0) { lineIsFull false; break; } } if (lineIsFull) { // 将该行以上的所有行下移 for (int yy y; yy 0; yy--) { for (int x 0; x WIDTH; x) { grid[yy][x] grid[yy - 1][x]; } } // 最顶行清空 for (int x 0; x WIDTH; x) { grid[0][x] 0; } // 因为当前行(y)已经变成了上一行(y-1)的内容需要再次检查同一行y此时已是新的内容 y; score 100; // 加分 } } }4.3 一个简化的俄罗斯方块主循环框架// 伪代码框架展示核心流程 int main() { initGame(); // 初始化网格、分数、生成第一个方块等 while (!gameOver) { drawGridAndPiece(); // 绘制 handleInput(); // 处理左右移动、旋转、加速下落 if (isTimeToFall()) { // 定时下落 if (canMoveDown(curPiece)) { curY; // 下落一格 } else { lockPiece(); // 固定方块到网格 clearLines(); // 消行 spawnNewPiece(); // 生成新方块 if (collisionAtSpawn()) { // 新方块出生即碰撞游戏结束 gameOver true; } } } delay(gameSpeed); } return 0; }注意事项俄罗斯方块的“幽灵方块”预览下落位置和“暂存方块”Hold功能是很好的扩展练习。实现它们需要额外维护一个预览的方块状态和一个暂存的方块状态逻辑上并不复杂但能极大提升游戏体验和代码组织能力。5. 经典案例三猜数字游戏与随机数生成猜数字游戏规则极简程序随机生成一个数字玩家来猜程序反馈“大了”、“小了”直到猜中。它虽然简单但涉及C编程中几个非常重要的基础概念。5.1 随机数的正确生成方法这是很多新手会踩坑的地方。直接使用rand()而不进行初始化每次程序运行生成的随机数序列都是一样的。错误示范int secretNumber rand() % 100 1; // 生成1-100的随机数 // 如果不播种每次运行程序secretNumber都会是同一个序列的第一个数。正确做法#include cstdlib #include ctime int main() { // 用当前时间作为随机数种子确保每次运行序列不同 srand(static_castunsigned int(time(0))); int secretNumber rand() % 100 1; // 现在每次运行都不同了 // ... 游戏逻辑 return 0; }time(0)返回自1970年1月1日以来的秒数每秒都在变化因此用它做种子rand()生成的序列在每次程序启动时都会不同。进阶提示C11引入了更强大、更现代的random库可以生成质量更好、分布更可控的随机数。对于猜数字这种简单游戏rand()足够用但了解新标准是好事。#include random std::random_device rd; // 非确定性随机数生成器 std::mt19937 gen(rd()); // 以rd()播种Mersenne Twister引擎 std::uniform_int_distribution distrib(1, 100); // 定义1到100的均匀分布 int secretNumber distrib(gen); // 生成随机数5.2 输入验证与循环控制玩家输入可能不是数字或者超出了合理范围。健壮的程序必须处理这些异常输入。int guess; while (true) { cout 请输入你的猜测 (1-100): ; if (cin guess) { // 输入成功读取为一个整数 if (guess 1 || guess 100) { cout 数字必须在1到100之间 endl; } else { break; // 输入有效跳出循环 } } else { // 输入失败例如输入了字母 cout 输入无效请输入一个整数 endl; cin.clear(); // 清除错误状态标志 cin.ignore(numeric_limitsstreamsize::max(), \n); // 忽略掉错误输入行的剩余内容 } }这里的关键是cin.clear()和cin.ignore()。当cin期望一个整数却收到字符时它会进入错误状态后续所有输入操作都会失败。clear()用于重置错误状态ignore()用于清空输入缓冲区中的错误数据。5.3 完整的猜数字游戏示例#include iostream #include cstdlib #include ctime #include limits // 用于numeric_limits using namespace std; int main() { srand(static_castunsigned int(time(0))); int secretNumber rand() % 100 1; int guess 0; int attempts 0; cout 欢迎来到猜数字游戏我已经想好了一个1到100之间的数字。 endl; while (guess ! secretNumber) { attempts; cout 第 attempts 次尝试请输入你的猜测: ; // 安全的输入循环 while (!(cin guess)) { cin.clear(); cin.ignore(numeric_limitsstreamsize::max(), \n); cout 输入无效请输入一个整数: ; } if (guess secretNumber) { cout 太大了 endl; } else if (guess secretNumber) { cout 太小了 endl; } else { cout 恭喜你猜对了数字正是 secretNumber 。 endl; cout 你总共用了 attempts 次尝试。 endl; } } // 简单评价 if (attempts 5) { cout 太厉害了你是高手 endl; } else if (attempts 10) { cout 还不错 endl; } else { cout 加油下次可以更快的 endl; } return 0; }这个简单的游戏包含了输入、输出、循环、条件判断、随机数和基本的错误处理是检验C语法掌握程度的完美试金石。6. 从控制台到图形界面Qt入门与项目迁移思考当你熟练掌握了控制台游戏的核心逻辑后很自然地会想“如何给它加上漂亮的图形界面” 这时像Qt这样的C图形用户界面库就派上用场了。将控制台游戏迁移到图形界面不仅仅是换一种绘制方式更是编程思维的一次升级。6.1 为什么选择Qt对于C开发者Qt是一个极其优秀的选择跨平台一套代码可以编译运行在Windows、macOS、Linux上。信号与槽机制这是Qt的核心特性用于对象间的通信。它取代了传统的回调函数更加安全、灵活。例如按钮的点击事件信号可以自动连接到处理函数槽。丰富的控件按钮、标签、输入框、绘图面板等一应俱全。强大的绘图系统QPainter类可以让你轻松绘制2D图形这正是我们重写游戏渲染部分所需要的。6.2 迁移贪吃蛇到Qt的基本思路项目结构重构将游戏逻辑与界面渲染分离。这是最重要的原则。你的Snake类、Food类、游戏状态Game类应该完全独立于Qt。它们只负责数据和处理逻辑如移动、碰撞检测不应该包含任何QWidget或绘图代码。创建游戏主窗口继承自QWidget或QMainWindow。这个窗口将作为游戏的容器。重写绘制事件在Qt中所有绘图都在paintEvent(QPaintEvent *event)函数中进行。你需要在这里使用QPainter来绘制网格、蛇身、食物而不是用cout打印字符。// 在自定义Widget类中 void GameWidget::paintEvent(QPaintEvent *) { QPainter painter(this); painter.setRenderHint(QPainter::Antialiasing); // 1. 绘制背景网格 // 2. 遍历snake向量绘制矩形或圆角矩形代表蛇身 // 3. 绘制食物 // 4. 绘制分数等文字信息 }处理用户输入重写keyPressEvent(QKeyEvent *event)函数。在这里捕获方向键Qt::Key_Left,Qt::Key_Right等的按下事件并调用游戏逻辑层的接口来改变蛇的方向。用定时器代替Sleep循环在控制台程序中我们用Sleep阻塞主线程来控制帧率。在GUI程序中阻塞主线程会导致界面卡死。Qt提供了QTimer类。QTimer *gameTimer new QTimer(this); connect(gameTimer, QTimer::timeout, this, GameWidget::updateGame); // 连接定时器信号到游戏更新槽函数 gameTimer-start(100); // 每100毫秒触发一次在updateGame()槽函数中调用游戏逻辑层的update()函数然后调用this-update()这会触发paintEvent重绘界面。线程安全考虑如果你的游戏逻辑计算量很大为了避免界面卡顿可能需要将游戏逻辑放到一个单独的线程QThread中运行并通过信号槽与主界面线程通信。但对于贪吃蛇这种简单游戏在主线程中用定时器更新完全足够。6.3 一个简单的Qt绘图示例绘制蛇身// 假设snake是存储QPoint的QVector QVectorQPoint snake; // ... 初始化snake void GameWidget::paintEvent(QPaintEvent *) { QPainter painter(this); painter.fillRect(rect(), Qt::black); // 黑色背景 // 设置画笔和画刷 QPen pen(Qt::green, 2); QBrush brush(Qt::darkGreen); painter.setPen(pen); painter.setBrush(brush); int cellSize 20; // 每个格子的像素大小 // 绘制蛇身 for (const QPoint point : snake) { QRect rect(point.x() * cellSize, point.y() * cellSize, cellSize, cellSize); painter.drawRect(rect); painter.fillRect(rect, brush); // 填充颜色 } // 绘制蛇头用不同颜色 if (!snake.isEmpty()) { painter.setBrush(Qt::red); QRect headRect(snake.first().x() * cellSize, snake.first().y() * cellSize, cellSize, cellSize); painter.drawRect(headRect); painter.fillRect(headRect, Qt::red); } }迁移到图形界面是一个系统工程但也是将你的C技能从“玩具”提升到“应用”的关键一步。它迫使你思考模块化、事件驱动和面向对象设计这些经验在未来的任何C项目中都无比宝贵。7. 调试、优化与项目扩展指南写完代码能运行只是第一步。写出健壮、高效、可扩展的代码才是我们追求的目标。这部分分享一些在开发这些小游戏过程中积累的调试技巧和优化思路。7.1 常见调试技巧与问题排查“蛇”或“方块”移动异常画面闪烁或卡顿检查游戏循环确保输入、逻辑更新、渲染的顺序正确并且有合适的延迟Sleep或定时器间隔。延迟太短CPU占用高且可能来不及处理输入延迟太长游戏不跟手。检查碰撞检测逻辑这是Bug高发区。仔细核对边界条件x WIDTH还是x WIDTH以及碰撞检测的遍历范围是否错误地包含了蛇头自己。建议单独写一个debugDraw()函数在控制台打印出游戏地图的数值状态而不是图形符号这能帮你精准定位物体坐标。控制台闪烁system(“cls”)是罪魁祸首。可以尝试“双缓冲”技术先在内存中构建好一整屏要输出的字符串最后一次性cout出去能极大缓解闪烁。或者直接转向使用图形库。随机数不随机或者食物生成在了墙上/蛇身上确保srand(time(0))只调用一次通常在main函数开头。如果在循环里多次调用由于time(0)秒级变化可能连续几帧的种子一样导致“随机”数相同。食物生成算法在贪吃蛇例程中我使用了do…while循环来确保食物生成在空位。务必检查循环条件是否覆盖了所有非法位置蛇身和蛇头。内存泄漏与指针错误如果使用了动态内存在简单的控制台游戏中尽量使用std::vector,std::string等RAII容器避免手动new/delete。如果必须用指针记住“谁申请谁释放”的原则。使用valgrind(Linux) 或 Visual Studio 的内存诊断工具来检测内存泄漏。7.2 性能优化与代码重构建议减少不必要的计算在贪吃蛇的draw()函数中我们遍历了整个地图20×20来绘制。其实可以只绘制发生变化的部分蛇头旧位置、蛇头新位置、蛇尾旧位置、食物位置但这会大大增加逻辑复杂度。对于这么小的规模全屏重绘完全可以接受。这是一个典型的“用性能换代码清晰度”的权衡。在碰撞检测中对于贪吃蛇的自我碰撞不需要遍历整个蛇身向量来检查蛇头是否与每一节相撞。可以只检查蛇头是否与紧接着的头后面几节相撞因为蛇不会瞬间折返很长的距离但这需要更精细的逻辑。初期实现完整检测更稳妥。代码重构走向面向对象当你觉得全局变量越来越多函数参数越来越复杂时就是重构为类的好时机。Game类封装游戏主循环、状态score,gameOver、地图(grid)、以及Snake和Food对象的实例。它提供Run(),HandleInput(),Update(),Render()等公共接口。Snake类封装蛇身坐标向量、移动方向、生长等方法。提供Move(),ChangeDirection(),CheckCollision()等方法。Food类封装食物坐标和生成方法。 这样main函数会变得非常简洁int main() { Game game; game.Initialize(); game.Run(); return 0; }这种结构清晰职责分明也方便你未来添加更多功能比如多种食物、障碍物等。7.3 功能扩展与创意发挥掌握了基础版本后你可以尝试添加新功能这比从头写一个新游戏更有挑战性也更有成就感。贪吃蛇扩展多种食物普通食物1分1节加速食物短时间内速度提升减速食物毒食物减少长度。障碍物随机生成不可穿越的墙。关卡系统分数达到一定值后进入下一关速度加快地图变大或出现障碍物。AI模式实现一个自动寻路算法如BFS最短路径算法让蛇自动寻找食物并避开自己和墙壁。俄罗斯方块扩展下一个方块预览在侧边显示下一个即将出现的方块。暂存功能按某个键可以将当前方块暂存并取出之前暂存的方块。得分系统单消、双消、三消、四消Tetris给予指数级增长的分数。粒子效果消行时添加简单的视觉特效如清空行的闪烁。通用优化添加音效使用简单的库如PlaySoundon Windows,SFML音频模块在吃食物、消行、游戏结束时播放音效。本地化高分记录将最高分保存到本地文件如highscore.txt中下次游戏启动时读取。通过这些扩展练习你会遇到文件I/O、更复杂的算法、状态管理、甚至简单的音频处理等新问题。每一个问题的解决都是你编程能力实实在在的一次提升。记住编程最好的学习方法就是不断地“做”和“改”。把这些源码当成你的起点尽情地去拆解、修改、破坏和重建吧。