5个C++控制台小游戏源码免费分享:从语法入门到实践进阶
2026/9/21 0:19:34 网站建设 项目流程

很多学C++的朋友都有过这种体验:语法书啃了半个月,for循环、数组、指针都认识你了,但合上书想写点东西,还是不知道从哪下手。我自己的经验是,学编程最快的路径就是做小项目,而小游戏是性价比最高的切入点——它不依赖复杂框架,输出反馈快,还能把各个语法点串起来。

这篇文章整理了5个可以直接运行的C++控制台小游戏源码,免费分享出来:猜数字、井字棋、扫雷、贪吃蛇、俄罗斯方块。全部用C++配合Windows控制台API写成,Visual Studio、Dev-C++、VS Code + MinGW都能编译。每一个游戏我都会拆开讲设计思路、核心难点和完整代码,还会分享我实际调试时踩过的坑。适合刚开始学C++的学生、想复习基础语法的在职开发,以及正在为课程设计找素材的朋友。

1. 5个C++小游戏的设计思路:难度阶梯与知识点覆盖

1.1 为什么选这5个游戏:难度递增与知识点的刻意安排

写C++小游戏,核心目的不是做出多炫的画面,而是用最小的复杂度把语言基础吃透。这5个游戏我刻意做了一个难度阶梯,你按顺序练习,基本上能把C++的入门语法完整过一遍。

猜数字是门槛最低的:只需要随机数、while循环、if分支和基本的cin/cout,这相当于你写的第一个“完整程序”。井字棋引入了二维数组和函数拆分,还带一个简单的人机AI,让你第一次体会到“程序做决策”。扫雷的核心是二维数组和递归展开,这里面有随机布雷、数字生成和边界处理,逻辑密度比前两个高不少。贪吃蛇开始涉及实时输入和状态刷新,要用到STL的vector来管理蛇身,还要处理方向控制和碰撞检测。俄罗斯方块则是控制台小游戏里面综合难度最高的一档,7种方块的形状表示、旋转矩阵、墙踢、消行、游戏结束判定,每一个点都能单独拿出来问“为什么”。

有不少人一上来就想写飞机大战、RPG,我劝你先别。控制台程序最大的优势是没有图形库的干扰,每一行代码都在为“游戏逻辑”服务。等你能把这5个游戏从零写一遍,再去碰EasyX、Qt、Unity这些带界面的东西,会轻松非常多。

1.2 开发环境准备:Visual Studio、Dev-C++还是VS Code + MinGW

代码是.cpp源文件,不是文本文档。有人把代码复制到记事本里然后改后缀名,那肯定跑不起来,必须用编译器。

环境优点缺点适合人群
Visual Studio Community免费,开箱即用,调试功能强安装包大,占用空间多新手、学生上课用
Dev-C++体积小,启动快,自带编译器界面老旧,调试一般临时用、老电脑
VS Code + MinGW-w64轻量,可自定义,体验现代需要手动配置编译器有命令行基础的开发者

我自己的建议是:如果你在Windows上学习,直接装Visual Studio Community,新建项目时选“控制台应用”,然后把代码粘贴进.cpp文件,按Ctrl+F5就能跑。如果你用的是Dev-C++,新建源文件、保存成.cpp、编译运行就行。VS Code的话需要先装MinGW-w64并配置好环境变量,难度高一点,但也不是什么大问题。

还有一个很多人忽略的点:以下代码里,贪吃蛇和俄罗斯方块用到了<windows.h><conio.h>这两个Windows专属头文件,所以只能在Windows下编译。macOS和Linux用户不是不能跑,但需要自己改造键盘输入和屏幕刷新部分的代码,这对初学者来说工作量不小,建议先在Windows上跑通。

2. 猜数字:用最短代码学会随机数与循环

2.1 游戏规则与代码结构拆解

猜数字的规则很简单:程序随机想一个1~100之间的整数,你有10次机会去猜,每次输入之后程序会告诉你“太大了”还是“太小了”,猜中就赢。

这个游戏虽然简单,但麻雀虽小五脏俱全。我用了外层while循环来实现“再玩一次”的选项,内层while来限制猜测次数,srand(time(nullptr))配合rand() % 100 + 1生成随机数,输入校验保证了玩家不会输入1~100之外的数字。代码里还用了一个win标志来记录是否猜中,这样如果10次用完还没猜对,就能给出不同的结束提示。

2.2 完整源代码:可直接复制运行的猜数字

#include <iostream> #include <cstdlib> #include <ctime> using namespace std; int main() { srand(time(nullptr)); char again = 'y'; while (again == 'y' || again == 'Y') { int target = rand() % 100 + 1; int guess, count = 0; bool win = false; cout << "===== 猜数字游戏 =====" << endl; cout << "我已经想好了一个 1~100 之间的数字。" << endl; while (count < 10) { cout << "还剩 " << 10 - count << " 次机会,请输入你的猜测:"; cin >> guess; if (guess < 1 || guess > 100) { cout << "输入范围是 1~100!" << endl; continue; } count++; if (guess > target) { cout << "太大了!" << endl; } else if (guess < target) { cout << "太小了!" << endl; } else { cout << "恭喜你猜中了!你用了 " << count << " 次猜对。" << endl; win = true; break; } } if (!win) { cout << "很遗憾,10次机会用完了。正确答案是 " << target << "。" << endl; } cout << "再玩一次吗?(y/n):"; cin >> again; } return 0; }

2.3 源码解读:随机数、循环与边界处理

rand() % 100 + 1这段,很多新手会问为什么不是rand() % 101。因为rand() % 100的结果是0~99,加1之后才是1~100。如果你想要一个[a, b]区间的随机整数,通用公式是rand() % (b - a + 1) + a,这个规律后面几个游戏也一直在用。

time(nullptr)作为随机数种子也很关键。如果你不写srand,每次运行程序rand()生成的序列都是固定的,玩起来就像背答案一样没意思。用当前时间当种子,每次运行才不一样。

代码里我用continue跳过非法输入,但这里有个小瑕疵:如果玩家输错,这次不消耗猜测次数,但提示信息会在下一次重绘时被清掉。控制台游戏经常有这种体验问题,不影响功能,但你在做自己的项目时可以把提示信息显示在固定区域。

扩展方向也很明确:加难度选择(简单1~50、普通1~100、困难1~200),或者把猜测次数改成和数字范围挂钩。理论上1~100的数字用二分法最多7次一定能猜中,你可以把机会设置成7次,让游戏更有挑战性。

3. 井字棋:人机对战与AI决策思路拆解

3.1 游戏规则与AI优先级设计

井字棋是3x3的棋盘,玩家用X,电脑用O,谁先把任意一行、一列或一条对角线连成三个,谁就赢。这个游戏代码量不大,但它第一次引入了“AI”的概念——电脑怎么决定下一步落子。

我给电脑设计了一个五级优先级的决策方案:第一,能赢就赢;第二,玩家马上要赢就堵;第三,占中心位置;第四,占角落;第五,随便找一个空位。你从这个规则能看出,这其实是一种“贪心策略”,每一手都选当下最优解。对于井字棋这种局面有限的游戏,这个策略已经能让电脑变得很难被击败,但还不是完美AI,后手玩家仍然有机会平局或获胜。

3.2 完整源代码:可直接复制运行的井字棋

#include <iostream> using namespace std; char board[3][3] = {{'1','2','3'},{'4','5','6'},{'7','8','9'}}; void drawBoard() { system("cls"); cout << "玩家: X 电脑: O\n\n"; for (int i = 0; i < 3; i++) { cout << " " << board[i][0] << " | " << board[i][1] << " | " << board[i][2] << endl; if (i < 2) cout << "---|---|---" << endl; } cout << endl; } bool checkWin(char player) { for (int i = 0; i < 3; i++) { if (board[i][0] == player && board[i][1] == player && board[i][2] == player) return true; if (board[0][i] == player && board[1][i] == player && board[2][i] == player) return true; } if (board[0][0] == player && board[1][1] == player && board[2][2] == player) return true; if (board[0][2] == player && board[1][1] == player && board[2][0] == player) return true; return false; } bool boardFull() { for (int i = 0; i < 3; i++) for (int j = 0; j < 3; j++) if (board[i][j] != 'X' && board[i][j] != 'O') return false; return true; } void computerMove() { // 1. 直接赢 for (int i = 0; i < 3; i++) { for (int j = 0; j < 3; j++) { if (board[i][j] != 'X' && board[i][j] != 'O') { char tmp = board[i][j]; board[i][j] = 'O'; if (checkWin('O')) return; board[i][j] = tmp; } } } // 2. 堵玩家 for (int i = 0; i < 3; i++) { for (int j = 0; j < 3; j++) { if (board[i][j] != 'X' && board[i][j] != 'O') { char tmp = board[i][j]; board[i][j] = 'X'; if (checkWin('X')) { board[i][j] = 'O'; return; } board[i][j] = tmp; } } } // 3. 占中心 if (board[1][1] != 'X' && board[1][1] != 'O') { board[1][1] = 'O'; return; } // 4. 占角落 int corners[4][2] = {{0,0},{0,2},{2,0},{2,2}}; for (int k = 0; k < 4; k++) { int r = corners[k][0], c = corners[k][1]; if (board[r][c] != 'X' && board[r][c] != 'O') { board[r][c] = 'O'; return; } } // 5. 任意空位 for (int i = 0; i < 3; i++) for (int j = 0; j < 3; j++) if (board[i][j] != 'X' && board[i][j] != 'O') { board[i][j] = 'O'; return; } } int main() { drawBoard(); while (true) { int pos; cout << "请输入位置(1-9):"; cin >> pos; if (pos < 1 || pos > 9 || board[(pos-1)/3][(pos-1)%3] == 'X' || board[(pos-1)/3][(pos-1)%3] == 'O') { cout << "无效位置,重新输入!" << endl; continue; } board[(pos-1)/3][(pos-1)%3] = 'X'; if (checkWin('X')) { drawBoard(); cout << "恭喜你赢了!" << endl; break; } if (boardFull()) { drawBoard(); cout << "平局!" << endl; break; } computerMove(); if (checkWin('O')) { drawBoard(); cout << "电脑赢了!" << endl; break; } if (boardFull()) { drawBoard(); cout << "平局!" << endl; break; } drawBoard(); } return 0; }

3.3 AI决策的逻辑核心与后续升级方向

仔细看computerMove的前两步,它们的实现思路是“模拟落子再还原”。比如判断“电脑能不能赢”,就把当前空位临时填上O,然后调用checkWin('O'),赢了就保留落子并返回;不赢就把这个位置恢复成原来的字符。这种“试走一步,看看结果,不行就撤销”的思路,很多围棋程序、象棋程序的底层也是同一个套路,只不过它们试的深度更深。

为什么中心优先于角落?因为井字棋里中心位置能同时参与4条胜利线(横、竖、两条对角线),而角落只参与3条,边格只参与2条。占住中心,既方便自己连线,也能限制对方的发展空间。

如果想升级这个AI,最简单的方向是引入minimax算法。井字棋的状态空间很小,9个格子,每个格子只有三种状态,就算全量枚举也才不到2万种局面,用递归做搜索毫无压力。如果做到那一步,电脑就永远不会输了。再往后可以加“玩家先手还是后手”的选项、难度分级(新手模式让电脑随机走)、以及记录胜负次数。

4. 扫雷:二维数组与递归展开的经典练手项目

4.1 游戏规则与核心数据结构

扫雷的规则不用多说:棋盘里随机埋着雷,翻开一个格子,如果是雷就炸,如果是数字就显示周围8格有几颗雷,如果是0就会自动展开连片的空白区域。

这个游戏最典型的编程考点有三个:布雷算法、数字统计、递归展开。我额外做了一个经典的处理——把二维数组上下左右各扩大一圈,声明成mine[ROWS+2][COLS+2]。这样在遍历周围8个格子的时候,就不需要每次都判断是不是数组边界,因为地图外圈永远是0。这个思路很多二维数组相关的项目都能用,比如生命游戏、俄罗斯方块的边界地图,非常实用。

4.2 完整源代码:可直接复制运行的扫雷

#include <iostream> #include <cstdlib> #include <cstring> #include <ctime> using namespace std; const int ROWS = 9, COLS = 9, MINES = 10; int mine[ROWS + 2][COLS + 2]; bool opened[ROWS + 2][COLS + 2]; void init() { memset(mine, 0, sizeof(mine)); memset(opened, 0, sizeof(opened)); int placed = 0; while (placed < MINES) { int r = rand() % ROWS + 1; int c = rand() % COLS + 1; if (mine[r][c] != -1) { mine[r][c] = -1; placed++; } } for (int i = 1; i <= ROWS; i++) { for (int j = 1; j <= COLS; j++) { if (mine[i][j] == -1) continue; int cnt = 0; for (int di = -1; di <= 1; di++) for (int dj = -1; dj <= 1; dj++) if (mine[i + di][j + dj] == -1) cnt++; mine[i][j] = cnt; } } } void openCell(int r, int c) { if (r < 1 || r > ROWS || c < 1 || c > COLS) return; if (opened[r][c]) return; opened[r][c] = true; if (mine[r][c] == 0) { for (int di = -1; di <= 1; di++) for (int dj = -1; dj <= 1; dj++) openCell(r + di, c + dj); } } void display() { system("cls"); cout << " "; for (int j = 1; j <= COLS; j++) cout << j << " "; cout << endl; for (int i = 1; i <= ROWS; i++) { cout << i << " "; if (i < 10) cout << " "; for (int j = 1; j <= COLS; j++) { if (!opened[i][j]) { cout << "* "; } else if (mine[i][j] == -1) { cout << "M "; } else { cout << mine[i][j] << " "; } } cout << endl; } } int main() { srand(time(nullptr)); init(); bool gameOver = false, win = false; int openedCount = 0; while (!gameOver) { display(); int r, c; cout << "输入要翻开的格子(行 列,空格分隔):"; cin >> r >> c; if (r < 1 || r > ROWS || c < 1 || c > COLS) { cout << "输入错误!" << endl; continue; } if (opened[r][c]) { cout << "这个格子已经打开过!" << endl; continue; } if (mine[r][c] == -1) { opened[r][c] = true; gameOver = true; cout << "踩雷了!游戏结束。" << endl; continue; } openCell(r, c); openedCount = 0; for (int i = 1; i <= ROWS; i++) for (int j = 1; j <= COLS; j++) if (opened[i][j]) openedCount++; if (openedCount == ROWS * COLS - MINES) { gameOver = true; win = true; display(); cout << "恭喜你赢了!" << endl; } } if (!win) { for (int i = 1; i <= ROWS; i++) for (int j = 1; j <= COLS; j++) if (mine[i][j] == -1) opened[i][j] = true; display(); } return 0; }

4.3 最容易踩的坑:数组越界与递归边界

写扫雷最容易出的问题就是数组越界。如果你不把地图扩大一圈,统计某个格子周围雷数的时候,就要写一堆if (i > 0 && j > 0)之类的判断,非常容易漏,而且越界访问带来的错误往往不报错,只会在运行到某个奇怪的时候崩溃,排查起来相当折磨。

递归函数openCell是另一个容易出问题的地方。它的逻辑是:翻开当前格子,如果当前格子数字是0,就递归翻开周围8个格子。这里必须加上opened[r][c]的判断,否则会出现两个格子互相递归调用停不下来的情况。另外,r > ROWS || c > COLS的边界判断也是必须的,否则边缘的0区域会把程序带出数组范围。

这个版本没有实现右键标记地雷的功能,你可以自己加一个“标记”模式。解析输入的时候让玩家选择是“翻开”还是“标记”,标记过的格子用?显示,再选一次取消标记。这个功能对赢下游戏其实不是必须的,但加上之后更接近真实扫雷。

5. 贪吃蛇:实时输入与碰撞检测的控制台实践

5.1 游戏规则与几个关键设计点

贪吃蛇是很多人写的第一个“动态”控制台游戏。蛇会自己往前移动,你用WASD控制方向,吃到食物就变长,撞到墙壁或者撞到自己就结束。

这里最关键的设计是用一个vector<Point>来保存蛇身,头部在snake[0],尾部在snake.back()。每次移动,在头部插入新位置,如果吃到食物就不删尾部,蛇就变长一格;没吃到食物就删掉尾部,蛇整体向前平移。这个“插入头部 + 删除尾部”的思路很简洁,比用数组存每个格子的状态好写很多。

还有一个新手容易忽视的点:蛇不能180度掉头。如果不加限制,蛇向右走的时候你按了左键,蛇头会直接撞到自己的脖子然后游戏结束,体验很糟糕。我在代码里加了一个方向判断,只有新方向和当前方向不冲突时才会更新。

5.2 完整源代码:可直接复制运行的贪吃蛇

#include <iostream> #include <cstdlib> #include <ctime> #include <conio.h> #include <windows.h> #include <vector> using namespace std; const int W = 20, H = 20; int dx[4] = {0, 1, 0, -1}; int dy[4] = {-1, 0, 1, 0}; struct Point { int x, y; }; vector<Point> snake; int dir = 1; Point food; int score = 0; void draw() { system("cls"); for (int y = 0; y < H; y++) { for (int x = 0; x < W; x++) { if (x == 0 || y == 0 || x == W - 1 || y == H - 1) { cout << "#"; } else { bool isHead = (x == snake[0].x && y == snake[0].y); bool isBody = false; for (size_t i = 1; i < snake.size(); i++) { if (x == snake[i].x && y == snake[i].y) { isBody = true; break; } } if (isHead) cout << "O"; else if (isBody) cout << "o"; else if (x == food.x && y == food.y) cout << "*"; else cout << " "; } } cout << endl; } cout << "得分:" << score << " 方向键: W/A/S/D" << endl; } void generateFood() { while (true) { food.x = rand() % (W - 2) + 1; food.y = rand() % (H - 2) + 1; bool onSnake = false; for (size_t i = 0; i < snake.size(); i++) { if (food.x == snake[i].x && food.y == snake[i].y) { onSnake = true; break; } } if (!onSnake) return; } } int main() { srand(time(nullptr)); snake.clear(); snake.push_back({W / 2, H / 2}); snake.push_back({W / 2 - 1, H / 2}); snake.push_back({W / 2 - 2, H / 2}); generateFood(); while (true) { if (_kbhit()) { char ch = _getch(); int newDir = dir; if (ch == 'w' || ch == 'W') newDir = 0; else if (ch == 'd' || ch == 'D') newDir = 1; else if (ch == 's' || ch == 'S') newDir = 2; else if (ch == 'a' || ch == 'A') newDir = 3; if ((newDir == 0 && dir != 2) || (newDir == 1 && dir != 3) || (newDir == 2 && dir != 0) || (newDir == 3 && dir != 1)) { dir = newDir; } } Point newHead = {snake[0].x + dx[dir], snake[0].y + dy[dir]}; if (newHead.x <= 0 || newHead.y <= 0 || newHead.x >= W - 1 || newHead.y >= H - 1) break; bool hitSelf = false; for (size_t i = 0; i < snake.size(); i++) { if (newHead.x == snake[i].x && newHead.y == snake[i].y) { hitSelf = true; break; } } if (hitSelf) break; snake.insert(snake.begin(), newHead); if (newHead.x == food.x && newHead.y == food.y) { score += 10; generateFood(); } else { snake.pop_back(); } draw(); Sleep(150); } cout << "游戏结束!得分:" << score << endl; return 0; }

5.3 控制台刷新与节奏控制的实战细节

_kbhit()是控制台游戏里很重要的一个函数,它“非阻塞”地检测有没有键被按下。如果用cin等待输入,蛇就不会自动移动;用_kbhit(),你才能做到“不按键时蛇一直往前走,按键时改变方向”。每次循环读一次按键,蛇就在那个方向上前进一格,节奏由Sleep(150)控制,这个值越小游戏越快。

很多人第一次运行贪吃蛇会觉得画面闪烁严重,原因是system("cls")每次把整个屏幕清掉再重绘,速度一快就会有闪烁感。一个比较简单的优化办法是用SetConsoleCursorPosition把光标定位到左上角,再逐行覆盖重绘,而不是清屏。你可以自己封装一个gotoxy函数,把draw()里的system("cls")改成光标定位,闪烁会明显改善。

关于游戏的节奏,我建议把速度等级做成分段式:每吃到5个食物,Sleep的时间减少20毫秒,最低降到30毫秒。这样游戏有成长曲线,而不是从开头到结尾一个速度。

6. 俄罗斯方块:旋转、墙踢与消行的进阶挑战

6.1 游戏规则与整体架构

俄罗斯方块是这5个游戏里最复杂的。玩法是7种方块不断从顶部落下,你可以左右移动、旋转、加速下落,落到底部就会固定住。固定的方块堆满一整行就会消失,如果方块堆到顶部,游戏结束。

我从一开始就决定把方块用4x4矩阵来表示,每一种方块是一组01数字。比如I型方块在矩阵里是1111一行,T型方块是第二行一个1加上第三行111。为什么不用坐标数组?因为旋转的时候矩阵直接做转置特别方便,坐标数组反而要在代码里写死各种旋转后的样子。

游戏的主要数据结构是board[H][W],保存已经固定的方块;当前下落方块用Tetromino结构体保存,里面有形状矩阵和两个坐标。每次循环做三件事:读取输入、尝试下落、绘制画面。落不下去就固定当前方块、消行、生成新方块。

6.2 完整源代码:可直接复制运行的俄罗斯方块

#include <iostream> #include <cstring> #include <cstdlib> #include <ctime> #include <conio.h> #include <windows.h> using namespace std; const int W = 10, H = 20; int board[H][W] = {0}; int shapes[7][4][4] = { {{0,0,0,0},{1,1,1,1},{0,0,0,0},{0,0,0,0}}, // I {{0,0,0,0},{0,1,1,0},{0,1,1,0},{0,0,0,0}}, // O {{0,0,0,0},{0,1,0,0},{1,1,1,0},{0,0,0,0}}, // T {{0,0,0,0},{0,1,1,0},{1,1,0,0},{0,0,0,0}}, // S {{0,0,0,0},{1,1,0,0},{0,1,1,0},{0,0,0,0}}, // Z {{0,0,0,0},{1,0,0,0},{1,1,1,0},{0,0,0,0}}, // J {{0,0,0,0},{0,0,1,0},{1,1,1,0},{0,0,0,0}} // L }; struct Tetromino { int shape[4][4]; int x, y; Tetromino(int type) { for (int i = 0; i < 4; i++) for (int j = 0; j < 4; j++) shape[i][j] = shapes[type][i][j]; x = 3; y = 0; } }; bool collide(const Tetromino& t) { for (int i = 0; i < 4; i++) { for (int j = 0; j < 4; j++) { if (t.shape[i][j]) { int bx = t.x + j; int by = t.y + i; if (bx < 0 || bx >= W || by >= H) return true; if (by >= 0 && board[by][bx]) return true; } } } return false; } void place(const Tetromino& t) { for (int i = 0; i < 4; i++) { for (int j = 0; j < 4; j++) { if (t.shape[i][j]) { int by = t.y + i; int bx = t.x + j; if (by >= 0 && by < H && bx >= 0 && bx < W) board[by][bx] = 1; } } } } void clearLines() { for (int i = H - 1; i >= 0; i--) { bool full = true; for (int j = 0; j < W; j++) { if (board[i][j] == 0) { full = false; break; } } if (full) { for (int k = i; k > 0; k--) for (int j = 0; j < W; j++) board[k][j] = board[k - 1][j]; for (int j = 0; j < W; j++) board[0][j] = 0; i++; } } } void draw(const Tetromino& cur) { system("cls"); for (int i = 0; i < H; i++) { cout << "#"; for (int j = 0; j < W; j++) { bool hasBlock = false; if (board[i][j]) hasBlock = true; for (int r = 0; r < 4; r++) for (int c = 0; c < 4; c++) if (cur.shape[r][c] && cur.y + r == i && cur.x + c == j) hasBlock = true; cout << (hasBlock ? "[]" : " "); } cout << "#" << endl; } cout << "a/d:左右移动 w:旋转 s:加速下落 q:退出" << endl; } int main() { srand(time(nullptr)); Tetromino cur(rand() % 7); int fallSpeed = 500; while (true) { if (_kbhit()) { char ch = _getch(); if (ch == 'a') { cur.x--; if (collide(cur)) cur.x++; } else if (ch == 'd') { cur.x++; if (collide(cur)) cur.x--; } else if (ch == 'w') { int tmp[4][4]; for (int i = 0; i < 4; i++) for (int j = 0; j < 4; j++) tmp[j][3 - i] = cur.shape[i][j]; memcpy(cur.shape, tmp, sizeof(tmp)); if (collide(cur)) { cur.x++; if (collide(cur)) cur.x -= 2; if (collide(cur)) { cur.x++; for (int i = 0; i < 4; i++) for (int j = 0; j < 4; j++) tmp[j][3 - i] = cur.shape[i][j]; memcpy(cur.shape, tmp, sizeof(tmp)); } } } else if (ch == 's') { fallSpeed = 30; } else if (ch == 'q') { return 0; } } cur.y++; if (collide(cur)) { cur.y--; place(cur); clearLines(); cur = Tetromino(rand() % 7); if (collide(cur)) { system("cls"); cout << "游戏结束!" << endl; break; } fallSpeed = 500; } draw(cur); Sleep(fallSpeed); } return 0; }

6.3 旋转、墙踢与消行的高频坑位

旋转的公式是tmp[j][3 - i] = cur.shape[i][j],这是一个顺时针旋转90度的矩阵变换。你不需要死记这个公式,理解方式很简单:原来在第i行第j列的元素,旋转后跑到第j行第(3-i)列。如果方块不是正方形,这个公式也成立,因为这里的4x4矩阵本质上是正方形。

墙踢(wall kick)是指方块靠着墙旋转时,旋转后的形状可能超出边界。我在代码里用的策略是:旋转后如果碰撞,先尝试右移一格,不行就左移一格,还是不行就放弃这次旋转。这个策略实现简单,实战够用。真正的俄罗斯方块游戏墙踢规则要复杂得多,SRS(Super Rotation System)系统里对不同方块类型有不同的墙踢数据表,但那是进阶话题了。

消行代码里有个容易忽略的细节:消掉一行后,上面的所有行都要往下移,然后我写了i++来抵消for循环里的i--,相当于重新检查这一行。为什么要这样?因为如果连续两行都满了,上面的行下移之后,当前行可能又变成满的,必须再查一次。这个i++的写法非常经典,也是很多新手调试半天才想明白的难点。

7. 常见编译错误与运行环境问题排查

7.1 编译报错速查表

我根据平时帮别人看代码的经验,把这几个游戏最容易遇到的编译错误整理成了一张表。

报错信息原因解决方法
'system' was not declared缺少<cstdlib><stdlib.h>在文件开头加#include <cstdlib>
'_kbhit' was not declared缺少<conio.h>,或者不是Windows环境#include <conio.h>
'Sleep' was not declared缺少<windows.h>#include <windows.h>
'memcpy' was not declared缺少<cstring>#include <cstring>
'SetConsoleCursorPosition' was not declared缺少<windows.h>#include <windows.h>
error C4996: 'strcpy': This function or variable may be unsafeVS的安全检查在文件开头加#define _CRT_SECURE_NO_WARNINGS
中文乱码源文件编码和控制台编码不一致用GBK或UTF-8 BOM保存源文件

这些报错基本都是“头文件没包含全”导致的。C++不像Java那样自动导入所有库,你不包含头文件,编译器就不认识这个函数,所以养成“用哪个函数就查哪个头文件”的习惯很重要。

7.2 运行环境问题:MSVCP140.dll与Visual C++ Redistributable

很多人把自己编译好的exe发给别人,别人双击时报错“找不到MSVCP140.dll”,这是因为目标电脑缺少Visual C++运行库。MSVCP140.dll对应的就是Visual C++ 2015-2022 Redistributable,它是运行Visual C++程序时必需的动态链接库。

解决方案很简单:去微软官网搜索“Visual C++ Redistributable”,下载x64版本安装就行。如果电脑是32位系统,还要同时装x86版本。这个安装包是免费的,装完基本能解决大多数“缺dll”的问题。

另外还有个常见情况:在编译或安装某个Python扩展、C++第三方库时,报错“Microsoft Visual C++ 14.0 or greater is required. Get it with 'Microsoft C++ Build Tools'”。这个不是缺运行库,而是缺完整编译工具链。解决方法是安装VS Build Tools,或者在Visual Studio Installer里勾选“使用C++的桌面开发”工作负载,把C++编译环境补上。这个问题我见得太多了,很多人卡了半天以为自己写的代码有问题,其实纯粹是环境缺组件。

7.3 我的调试与排查经验

控制台小游戏的bug其实很好查,因为你能看到每一步的输出。我自己的习惯是,先用小数据把逻辑跑通,再扩展到完整场景。

比如扫雷,我会先把雷数设置成1,棋盘设成3x3,手动输入坐标,确认布雷、数字、递归展开都正常,再改成9x9和10雷。贪吃蛇就先让蛇撞墙,确认游戏结束逻辑正确,再判断撞自己。俄罗斯方块的旋转算法我建议先在纸上把4x4矩阵写出来,手动转一次,再用代码验证,不然光对着屏幕看很容易头晕。

数组越界这种问题是最难查的,因为它不一定崩溃,有时候只是输出一些莫名其妙的值。遇到这种情况,最有效的办法是把所有数组下标打印出来,或者用调试器逐步走。不要靠猜,控制台程序不像图形程序那样信息多,但打印大法是永远的神。

如果你运行exe后窗口一闪而过,别急着怀疑代码,那是程序正常跑完退出了。在main函数最后加一行system("pause"),或者cin.get()让程序停在结束前,就能看到结果了。这个坑很多人踩过,但它其实不是程序错误。

写这几个小游戏,我最大的体会是:代码比想象中容易写,难的是把逻辑拆清楚。拿俄罗斯方块来说,如果一开始就想着把所有功能一次性写完,很容易写到一半就乱了。但当你把碰撞检测、旋转、消行拆成独立的函数,每个函数只做一件事,整个程序的思路就清晰多了。

最后分享一个我练手时的做法:不满足于代码跑通,而是给每个游戏加一个自己设计的功能。比如给猜数字加难度选择,给井字棋加胜负统计,给扫雷加计时器。当你发现自己加功能时需要改动原来的结构,说明你对程序的理解已经不止于语法层面了。这5个代码免费拿走,但收获多少,取决于你愿意动手改多少。C++这门语言,看十遍不如敲一遍,希望你能从第一个猜数字开始,把这条路走通。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询