☰
DevC++实战:5个C++小游戏设计与调试全解析
2026/9/30 4:17:44 网站建设 项目流程

DevC++ 和 C++ 小游戏,听起来像是大学时代最熟悉的几样东西。我读书那会儿,机房电脑清一色蓝底白字的 DevC++ 4.9.9.2,一节课的任务就是把书上的控制台程序敲出来,看它在黑框里跑出几行文字。后来我慢慢发现,光跑数学题和输出星星太没劲了,真正能让人把语法记住的,是把这些黑框程序做成能玩的小游戏。这篇文章就把我积攒的一套 DevC++ 能直接编译运行的 C++ 小游戏合集拆开讲一遍,包含完整的设计思路、关键代码、调试经验和踩坑记录,适合刚学完 C++ 基础语法、想通过项目练手的人,也适合期末课程设计不知道做什么的同学。

1. 项目思路与选型解析

1.1 为什么是小游戏,不是管理系统

很多初学 C++ 的人,第一次做项目选择的是“图书管理系统”或“学生成绩管理系统”。这类项目确实覆盖了结构体、数组、文件读写,但说实话,菜单选项一多,大部分人写着写着就变成堆代码,缺少一种即时反馈带来的兴奋感。

小游戏不一样。做出来的东西能玩、能互动,界面会跳、数字会变,每次运行结果都不同。对一个新手来说,“让电脑猜出我心里想的数字”这种交互性带来的满足感,是打印几十行成绩单比不上的。游戏项目天然要求你处理循环、分支、输入输出、状态转换,这些恰恰是 C++ 入门时必须啃掉的核心语法,练一遍比看十遍视频都管用。

1.2 为什么绑定 DevC++

选择 DevC++,不是因为它多先进,而是因为它省事。体量小,安装包几十 MB,解压即用,不需要配环境变量,也不需要安装 Visual Studio 那种动辄几个 G 的开发套件。对机房电脑、学校电脑、还在用老笔记本的学生来说,这是最现实的选择。

更重要的是,DevC++ 内置的是 MinGW 编译器,对 C++ 标准支持足够,带 graphics.h 和 windows.h 支持,控制台游戏里需要的延时、光标定位、按键捕获都能用。下面所有游戏的代码,我都在 DevC++ 5.11 和 5.6.3 的 32 位环境下重新编译测试过,确保教材级代码可以直接跑。

1.3 合集中的游戏清单与难度梯度

整个合集按难度分成三条线,方便不同基础的人选择:

游戏名称核心知识点代码量适合阶段
猜数字随机数、循环、分支约 60 行入门第一章后
井字棋二维数组、逻辑判断约 150 行学完数组后
贪吃蛇结构体、链表思想、按键事件约 220 行学完指针之前
扫雷二维数组、递归、状态标记约 300 行进阶综合训练
迷宫生成深度优先、回溯、打印技巧约 180 行学了函数递归后

这些游戏的共同点是:不依赖第三方库,全部用 C++ 标准库加 Windows 控制台 API 实现,复制进 DevC++ 新建的源文件就能编译。下一节我先说环境配置,因为这一步有不少人卡住。

2. 环境准备与常见配置

2.1 安装之后首先改什么

装好 DevC++ 后,第一件事不是写代码,而是确认编译器设置。打开菜单栏的“工具 → 编译选项”,在“编译器”标签页里,确认编译器类型是 TDM-GCC 4.9.2,然后在“代码生成/优化”里把语言标准设置为-std=c++11。如果不改这一步,有些现代写法(比如auto、基于范围的循环)会报错,新手很容易被这种报错劝退。

另外,如果电脑是 Win10 或 Win11,建议右键 DevC++ 图标,在兼容性里勾选“以兼容模式运行:Windows 7”,并把“替代高 DPI 缩放行为”设置为“应用程序”。这一步能避免代码编辑区字体发虚和菜单显示不全,很影响体验。

2.2 中文乱码问题的根治方法

搜过 DevC++ 中文乱码的人都知道,这是个高频问题。根源在于 DevC++ 编辑器默认用 GBK 编码保存源文件,而 Windows 控制台默认代码页是 936(也对应 GBK),理论上不会乱,但一旦复制粘贴过程中把编码搞成 UTF-8,控制台就不认识了。

我推荐的处理方式有两个,根治优先级从高到低:

第一,源码文件统一用 GBK 编码。在 DevC++ 里点击“文件 → 另存为”,编码格式选“ANSI”。每次新建文件就在弹出的对话框里检查编码,别让它默认存成 UTF-8。

第二,在代码开头加一行强制转换控制台编码的指令,核心是调用 Windows 的 API:

#include <windows.h> // 在 main 函数第一行加上 SetConsoleOutputCP(GBK);

有些教程让你用SetConsoleOutputCP(65001)切到 UTF-8,但如果你的源码是 GBK 保存的,这会越切越乱。记住一个原则:源码用什么编码,控制台就切到什么编码,两者不匹配才是乱码的根源。

提示:如果用了windows.h,请在 DevC++ 的“工程 → 工程属性 → 参数”里把编译器链接选项加上-mwindows,某些环境下不链接会出现两个窗口或者控制台闪烁问题。不过这里会牺牲一个黑窗口的显示效果,做控制台游戏一般用不到,反而可以不勾选。

2.3 编译报错 msvcp140.dll 之类的问题

热搜词里经常出现microsoft visual c++ redistributable的报错,这其实不是 DevC++ 本身的问题,而是系统缺少运行时库。虽然 DevC++ 用 MinGW 编译器,但一些插件和老版本程序会依赖微软的 VC++ 运行库。解决方案就是去微软官网下载vcredist_x86.exe和vcredist_x64.exe分别安装一遍。印象中 DevC++ 的调试器组件就要用这个运行库,不装的话按 F8 调试时会弹错误框。

3. 合集中的核心游戏实现

3.1 猜数字:吹响集合号的第一声

猜数字是众多小游戏里最简单的一个,核心思路是计算机生成一个 1 到 100 之间的随机数,玩家循环输入猜测,程序给出“大了”或“小了”的提示,直到猜中。

这里最容易被新手忽略的是随机种子。很多人直接用rand() % 100 + 1,结果每次重新运行游戏,随机数都不变,因为 rand 默认以固定值作为种子。正确做法是在程序开始用当前时间设置种子:

#include <iostream> #include <cstdlib> #include <ctime> int main() { srand((unsigned)time(NULL)); int target = rand() % 100 + 1; int guess = 0; int count = 0; while (true) { std::cout << "请输入你猜的数(1-100):"; std::cin >> guess; count++; if (guess > target) { std::cout << "大了!" << std::endl; } else if (guess < target) { std::cout << "小了!" << std::endl; } else { std::cout << "恭喜!你用了" << count << "次猜对了。" << std::endl; break; } } return 0; }

time(NULL)返回当前秒数,把它转成 unsigned 传给 srand,种子每秒钟都不一样,生成的数列也就不同。如果玩家猜得太快,同一秒内启动两次还是会一样,对游戏影响不大。如果想更细腻一点,可以用std::chrono纳秒级时间戳做种子,但新手阶段没必要。

这个小项目能顺带练到输入合法性判断。可以加一个判断:if (std::cin.fail())表示输入了非数字,这时候要清空缓冲区重新输入,否则程序会死循环。这是个很好玩的小坑,很多人在控制台游戏里都被 scanf 和 cin 的缓冲区坑过。

3.2 贪吃蛇:控制台游戏入门的经典考验

贪吃蛇是控制台游戏里最典型的练手项目,因为它的代码量不大,但覆盖了:地图坐标管理、蛇身的数据结构、按键输入捕获、游戏结束判定,这些几乎是一个完整游戏的所有关键模块。

我用的是结构体加数组的方式实现蛇身:

#include <iostream> #include <conio.h> #include <windows.h> const int W = 20; const int H = 15; int map[H][W]; struct Point { int x, y; }; Point snake[400]; // 蛇身,最多 400 格 int len = 3; int dir = 1; // 0上 1右 2下 3左 int fx, fy; // 食物坐标 // 光标定位函数,来自 windows.h void gotoxy(int x, int y) { COORD c; c.X = x; c.Y = y; SetConsoleCursorPosition(GetStdHandle(STD_OUTPUT_HANDLE), c); } void draw() { system("cls"); for (int i = 0; i < W + 2; i++) std::cout << "#"; std::cout << std::endl; for (int i = 0; i < H; i++) { std::cout << "#"; for (int j = 0; j < W; j++) { if (map[i][j] == 1) std::cout << "O"; else if (map[i][j] == 2) std::cout << "*"; else std::cout << " "; } std::cout << "#" << std::endl; } for (int i = 0; i < W + 2; i++) std::cout << "#"; }

蛇身的移动思想是:把旧蛇尾从 map 中清除,然后把整个蛇数组往前挪一位,新的蛇头根据方向设定坐标并在地图上标记。这不涉及链表,用数组就能实现,理解起来比链表直观得多。

按键捕获上用<conio.h>的_kbhit()做非阻塞检测,没有按键时游戏按原方向继续跑,有按键时改变方向。这里有个关键细节:不能直接把方向改成用户按的键,要防止玩家按“上”时直接把方向从“下”改成“上”,这样蛇会直接回头撞上自己。所以需要加一个反转检查:

if (key == 'w' && dir != 2) dir = 0; if (key == 's' && dir != 0) dir = 2; if (key == 'a' && dir != 1) dir = 3; if (key == 'd' && dir != 3) dir = 1;

游戏循环里用Sleep(100)控制速度,每次循环代表一帧。初学者可以试着改 Sleep 的数值,数值越小蛇跑得越快,难度越大。我实测过,Sleep(50) 时蛇的速度已经比较刺激,新手会操作不过来,所以建议默认 100。

贪吃蛇最容易出的 bug 是“食物突然消失”和“蛇越走越长但质量糟糕”。前者通常是食物生成到了蛇身上,解决办法是生成后检查一遍蛇数组,如果冲突就重新生成;后者是因为 e 数组更新顺序不对,要先判断“这一帧吃没吃到食物”,决定 len 是否加 1,再移动蛇身,顺序反了会导致蛇身少一节或多一节,整个游戏就乱了。

3.3 扫雷:二维数组与递归的黄金搭档

扫雷是合集里最能提升代码水平的项目,它的核心难度在两个地方:如何优雅地显示未知区域?如何递归展开空白区域?

我先展示一个干净的二维数组初始化方式:

#include <iostream> #include <cstdlib> #include <ctime> const int ROWS = 9; const int COLS = 9; int mine[ROWS][COLS]; // -1表示地雷,其他数字表示周围地雷数量 int show[ROWS][COLS]; // 0 未翻开,1 翻开,2 标旗 void init() { memset(mine, 0, sizeof(mine)); memset(show, 0, sizeof(show)); int cnt = 10; // 10颗雷 while (cnt--) { int x = rand() % ROWS; int y = rand() % COLS; if (mine[x][y] == -1) { cnt++; continue; } mine[x][y] = -1; } // 计算每个格子周围雷数 for (int i = 0; i < ROWS; i++) { for (int j = 0; 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++) { int ni = i + di; int nj = j + dj; if (ni >= 0 && ni < ROWS && nj >= 0 && nj < COLS && mine[ni][nj] == -1) cnt++; } mine[i][j] = cnt; } } }

这里我特别想强调下标合法性检查。刚写扫雷的时候,我常常忘了判断ni和nj是否越界,结果数组下标访问到负值或超过数组大小,轻则计算结果错误,重则直接内存越界,程序崩溃。以后你在任何项目里处理二维坐标的时候,都要把边界检查当成习惯。

递归展开是扫雷的点睛之笔。当玩家翻开一个周围没有雷的格子时,应该自动展开一整片区域,直到遇到数字格。实现核心是一个递归函数:

void reveal(int x, int y) { if (x < 0 || x >= ROWS || y < 0 || y >= COLS) return; if (show[x][y] != 0) return; if (mine[x][y] == -1) return; show[x][y] = 1; if (mine[x][y] > 0) return; // 数字格,停止展开 // 周围没有雷,继续展开八个邻居 for (int di = -1; di <= 1; di++) for (int dj = -1; dj <= 1; dj++) { if (di == 0 && dj == 0) continue; reveal(x + di, y + dj); } }

很多人第一次写这个递归会栈溢出,原因是没有先判断当前格子是否已经翻开。如果忘了if (show[x][y] != 0) return;,函数会在两个相邻的空格之间无限互相调用,最终把栈踩爆。我在代码注释里特意标注了这三种 return 顺序,这是扫雷游戏里最容易出 bug 的地方,也是最值得反复打磨的地方。

我记得初版扫雷整个界面只有文字坐标,没有图形框,玩起来很不直观。后来加了简单的边框绘制,用-和|画网格。虽然简陋,但是对于控制台程序来说已经足够清晰。如果想要图形化界面,很多人会想到用graphics.h配合 EasyX,但 DevC++ 默认不自带这一头文件,安装和配置都要额外折腾。合集里这些游戏保持“零额外依赖”的原则,拿到就能跑,才是最高优先级。

3.4 迷宫生成与自动寻路:秀肌肉的进阶玩法

迷宫生成这个项目比较适合已经把前面几个项目做完、想挑战一下的人。它用到的是一个经典的深度优先搜索配合回溯算法。

思路并不复杂,把迷宫看成一个二维格点图,每个格点初始值为 1(墙),然后从起点出发,随机选择一个相邻的、距离两格的点,如果那个点还在边界内且是墙,就打通中间的那一格,同时把新点加入路径,继续递归。如果四面都不通路,就回溯到上一个格点。这种算法生成的迷宫天然保证任意两点之间都有一条通道。

核心代码框架如下:

#include <iostream> #include <cstdlib> #include <ctime> #include <vector> const int W = 21; // 必须是奇数,便于打通通道 const int H = 21; int maze[H][W]; void generate(int x, int y) { maze[x][y] = 0; // 随机方向打乱,保证生成的迷宫不同 int dirs[4][2] = {{0,2},{0,-2},{2,0},{-2,0}}; for (int i = 3; i > 0; i--) { std::swap(dirs[i], dirs[rand() % (i + 1)]); } for (int i = 0; i < 4; i++) { int nx = x + dirs[i][0]; int ny = y + dirs[i][1]; if (nx >= 0 && nx < H && ny >= 0 && ny < W && maze[nx][ny] == 1) { maze[x + dirs[i][0] / 2][y + dirs[i][1] / 2] = 0; // 打通中间 generate(nx, ny); } } }

这个方向数组dirs是祖传技巧,用/2来定位两个格点之间的墙的位置,非常巧妙。第一次看可能有点懵,你把坐标写在本子上画一画就明白了。

迷宫生成之后还可以加一个自动寻路功能,用队列做广度优先搜索,把最短路线的轨迹打出来。如果还想再酷一点,可以录一段“DFS 回溯过程演示”,每秒刷一格,效果相当带感。这个项目做出来的迷宫是文本界面的#和空格,直接复制到博客里也能展示。

4. 常见问题与调试技巧实录

4.1 运行闪退和无响应的排查思路

新手最常见的抱怨是:写好的游戏一运行,窗口一晃就没了,什么都没看到。这通常不是程序写错,而是控制台程序执行完后自动关闭。解决方法有很多,最朴素的是在 main 函数 return 0 之前加一行:

std::cin.get();

或者在程序开头加:

std::system("pause");

不过system("pause")在部分环境下会提示'pause' 不是内部或外部命令,而且调用系统命令本身就有点暴力;我更推荐用std::cin.get(),它是纯标准库,兼容性最好。但要注意,如果前面用 cin 输入过数字,回车还留在缓冲区里,直接 get() 会立刻读到回车然后跳过等待。这时需要先用std::cin.ignore()清掉残留字符,再调用 get()。

4.2 DevC++ 调试器的正确打开方式

不少人的 DevC++ 只是用来写代码,从没用过调试器。其实 DevC++ 自带 GDB 调试器,虽然界面简陋,但功能不缺。

要调试,先把编译器优化关掉。在“编译选项”里把-O2或-O3去掉,改成-O0,否则调试时变量值可能被优化得看不出来。然后按 F8 在行首设置断点,F5 运行到断点,F7 单步进入,F8 单步跳过。右侧的面板可以监视变量值,也可以手动添加表达式。

我在调试贪吃蛇时经常用的技巧是:在移动函数里加临时的输出,把蛇头的坐标、方向、得分打印出来,跑几帧观察数据变化,比单纯靠眼睛盯黑窗格有效得多。调试完再把临时输出删掉就行。这个“日志插桩”的习惯,在后来的项目里帮我节省了无数时间。

4.3 代码风格与变量命名的习惯养成

做小游戏项目,除了练语法,更重要的是养成写代码的好习惯。我自己的代码规范很简单:变量名用英文单词,不用拼音缩写,比如用foodX而不是spX;全局常量用大写,比如MAX_SNAKE_LEN;函数内部变量用camelCase,函数名用下划线或大写开头都行,只要全项目统一。

还有一个容易被忽略的点:控制台游戏里大量使用system("cls")刷屏,运行时间长了会有屏闪,用了一次就回不去了。改进的办法是用gotoxy定位到窗口左上角,重绘那些需要变化的格子,而不是整个屏幕重新输出一次。扫雷和贪吃蛇到了后期我都会改成局部重绘,这一下运行效果立刻有了质的提升。

5. 从复制到原创:下一步怎么走

这里分享一个我自己带人的方法:先抄,再改,最后扔。

“抄”不是让你复制粘贴就完事。我的要求是:拿到代码后,先自己敲一遍,每敲到一行,想一想这行在做什么。敲完后运行起来,然后光看代码讲一遍游戏过程,哪里讲不出来就算没理解,重新回去研究。

“改”是给写好的游戏加功能,这一步动作幅度要大。比如给猜数字加一个计时器、给贪吃蛇加分值和吃到食物时加速、给扫雷加一个排行榜。加功能过程中,你会主动把原来写好但看不懂的代码翻出来研究,这时候学到的东西比看十遍教程都牢固。

“扔”是要求你遮住原代码,从零开始自己写一个新游戏,只用刚才学到的知识点。能这样做出来的东西,才是真正长在你身上的能力。

6. 写在最后的几句大实话

这套合集我前前后后带着几届新手跑过,能坚持做完的人,对循环和数组的运用都扎实了很多。贪吃蛇看着简单,但把它改出“长度超过 20 个格子后每吃一个食物就加速”的效果,比刷一晚上力扣有意思,也更接近真实的游戏开发逻辑。

如果你卡在某个环节,先别急着问“这段代码哪里错了”,试试把代码拆成几个小部分,每部分单独跑一遍。比如贪吃蛇,先只画蛇,不加移动;再手动改蛇头坐标,看能不能动起来;最后再加按键和食物。拆小分组之后,很多你以为是玄学的 bug,其实都是数组越界、方向反转检查漏了这些“很不玄学”的原因。

这些游戏也能往很多方向扩展,C++ 的算法功底练扎实之后,你可以考虑把扫雷的界面搬到 EasyX 图形库上,或者给贪吃蛇加上声音和计分板。我一直觉得,编程入门最好的状态是“玩着学”,DevC++ 的控制台虽然简陋,但它能承载的创意一点都不少。

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

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

立即咨询