简介:这是一份由C++编写的115个小游戏源码合集,涵盖坦克大战、五子棋、俄罗斯方块、三国杀、吃豆人、狼人杀等经典玩法,适合C++初学者和游戏开发爱好者用于练手与创意参考。压缩包共117个文件,主体为115个.cpp源码文件,另含1个可直接运行的exe演示文件和1个编译过程生成的out文件,整体仅1.14MB,便于快速下载与本地编译实验。资源依据游戏类型和操作方式做了初步归类,既有单人益智类也有双人对战类,部分文件附带了如“有BUG”等实现说明,能帮助读者提前规避问题。已有2043人学习下载,说明这套合集具有不错的参考价值。通过学习这些源码,可以掌握不同游戏框架下的逻辑设计、碰撞检测、界面绘制与控制响应等常见编程技巧,并从中获得改造与二次开发的灵感。
1. 115 个 C++ 小游戏源码:这份合集比你的 C++ 课本更接近实战
如果你是学完 C++ 语法就卡在“下一步写什么”的人,这份 115 个小游戏源码合集值得你下一个晚上读一遍。它不是什么教学框架,就是 115 个.cpp文件:五子棋、俄罗斯方块、坦克大战、狼人杀、三国杀 AI、UNO、吃豆人,从几十行的简单控制台程序到带 AI 决策的对战逻辑全都有,能编译、能跑、能改。对刚入门的人,它是一堆可拆解的 C++ 项目;对写过点东西的人,它是一份“别人代码里有哪些坑”的现成样本。我自己拿到后先干了三件事:跑通编译环境、挑几个经典游戏读核心逻辑、把作者在文件名里标出来的 BUG 当反面教材。这一篇就按这个顺序写。
2. 先把环境跑通:MinGW、VS Code 配置与 GBK 编码三件事
115 个文件解压出来,第一件事不是读代码,是让代码能编译。大多数人在这里就被三座大山卡住:中文文件名、源码的 GBK 编码、旧式控制台 API 在现代编辑器里的显示问题。这一步花 20 分钟搞定,后面 115 个文件随便玩。
2.1 中文源文件与 GBK 编码:先解决乱码再谈编译
这些文件绝大多数是老式控制台程序,源码保存在 GBK/GB2312 编码下,而 VS Code、新版记事本默认用 UTF-8 打开。直接双击打开,注释和printf里的中文全变乱码,甚至编译直接报错。我一般先不改文件,用 VS Code 右下角“重新打开编码”,选 GBK 看一眼再说。想批量转码就用命令:
# 把 GBK 编码的源码批量转成 UTF-8,输出到 utf8 目录 mkdir utf8 for f in *.cpp; do iconv -f GBK -t UTF-8 "$f" > "utf8/$f"; doneiconv是 Linux 下的命令,Windows 上可以用 Git Bash 跑,或者装一个 GNUWin32。-f GBK指定源编码,-t UTF-8指定目标编码。转码后 VS Code 打开不乱码,但编译时要注意:如果源码里写了中文字符串字面量,编译器默认按源文件编码去解析,转成 UTF-8 后老编译器可能不认,我实际用下来,MinGW 的g++加下面两个参数最省事。
g++ -std=c++17 -static-libgcc -static-libstdc++ -finput-charset=UTF-8 -fexec-charset=GBK 五子棋.cpp -o 五子棋.exe-finput-charset=UTF-8告诉编译器源码是 UTF-8;-fexec-charset=GBK让生成的可执行文件里的中文字符串按 GBK 输出,这样在 Windows 控制台里中文正常显示。
提示:如果你只是自己玩,不打算给别人发代码,最省心的方案是不转码,保持 GBK 源文件,用
-finput-charset=GBK编译,VS Code 里用 GBK 编码打开看代码。
2.2 MinGW 命令行编译:一条命令跑通
这套代码都是控制台程序,不依赖图形库,所以不需要 VS 全家桶。比起装 Visual C++ Redistributable,我一般直接上 MinGW-w64,解压就能用。编译单个文件:
g++ -std=c++17 -static-libgcc -static-libstdc++ 俄罗斯方块1\(上下左右控制\).cpp -o tetris.exe注意两点:文件名带括号的,bash 里要加转义反斜杠,或者用双引号把文件名包起来;-static-libgcc -static-libstdc++把 C/C++ 运行时静态链接进 exe,换机器不会提示缺 DLL。
如果嫌一条条敲太慢,写个脚本批量编译:
for f in *.cpp; do g++ -std=c++17 -O2 -static-libgcc -static-libstdc++ "$f" -o "${f%.cpp}.exe" 2>> build_err.log done-O2是优化级别,这种老代码开-O2没问题;2>> build_err.log把报错统一收进日志,编译完再看哪个文件没过。先批量编译一遍还有一个好处:你能快速看出这 115 个文件里有多少是“作者写完就没编译过”的,这种文件跳过不浪费时间。
2.3 VS Code 里配好 tasks.json:改完代码一键跑
命令行能用以后,再配一个 VS Code 的编译任务,做到“打开文件按一下 Ctrl+Shift+B 就能编译”。在项目根目录建.vscode/tasks.json:
{ "version": "2.0.0", "tasks": [ { "label": "build current file", "type": "shell", "command": "g++", "args": [ "-std=c++17", "-O2", "-static-libgcc", "-static-libstdc++", "-finput-charset=GBK", "-fexec-charset=GBK", "${file}", "-o", "${fileDirname}/${fileBasenameNoExtension}.exe" ], "group": { "kind": "build", "isDefault": true } } ] }${file}是 VS Code 内置变量,代表当前打开的文件;${fileBasenameNoExtension}是去尾缀的文件名。配好后,任何打开的文件都能一键编译,不用复制路径。再把终端改成“运行活动文件”或者自己配个快捷键跑 exe,就能实现“改完跑一把”的循环。
这套环境配下来,核心就三个点:编码要统一认知、编译器参数要带静态链接、VS Code 任务要会用变量。后面所有代码都在这个环境上跑。
2.4 建议先跑通的 6 个文件
115 个文件别按名字顺序看,我建议按下面这张表跑通一遍,每跑一个你就掌握一类控制台游戏的核心套路。
| 顺序 | 文件名 | 你要看什么 |
|---|---|---|
| 1 | 五子棋.cpp | 二维数组棋盘、落子与胜负判断 |
| 2 | 俄罗斯方块1(上下左右控制).cpp | 方块旋转、边界碰撞、按键控制 |
| 3 | 双人坦克.cpp | 双人键盘输入、子弹与墙壁碰撞 |
| 4 | UNO.cpp | 游戏状态流转、回合切换 |
| 5 | flappy_bird1(比2更精致,但有闪屏问题).cpp | 渲染刷新频率、动画平滑度 |
| 6 | 三国杀AI(偶尔有BUG).cpp | 复杂逻辑的状态管理、AI 决策分支 |
先跑这几个的好处是难度递进,从纯数组操作到多角色状态管理,每一层都有前面的基础。有些文件名自带问号,比如“牛顿的跳跃?评测器大战.cpp”,不用在意,作者写着玩的。跑通一轮,你对这套合集的质量心里就有数了。
3. 从俄罗斯方块看控制台游戏的基本盘:二维数组、旋转与延时循环
俄罗斯方块是这个合集里覆盖最全的一个主题,光名字里带“俄罗斯方块”的就有至少 4 个文件,而且作者在文件名里标注了差异:“上下左右控制”、“J,L左移右移;D变换方向”、“下落有BUG,不会死”。读这三份,相当于看一个游戏的三次迭代。
3.1 方块用数组表示:为什么是 4x4
俄罗斯方块的所有方块,本质都是二维数组里的形状。常见做法是定义 7 种方块,每种用 4x4 或 4x8 的数组存。合集里“俄罗斯方块2(J,L左移右移;D变换方向).cpp”把变换做成了按键触发,你读它的核心就能看到这种数组定义:
// 7 种方块的 4x4 模板,1 表示有方块,0 表示空 int blocks[7][4][4] = { // I 形 {{0,0,0,0},{1,1,1,1},{0,0,0,0},{0,0,0,0}}, // J 形 {{1,0,0,0},{1,1,1,0},{0,0,0,0},{0,0,0,0}}, // L 形 {{0,0,0,1},{1,1,1,0},{0,0,0,0},{0,0,0,0}}, // O 形 {{0,0,0,0},{0,1,1,0},{0,1,1,0},{0,0,0,0}}, // S 形 {{0,1,1,0},{1,1,0,0},{0,0,0,0},{0,0,0,0}}, // T 形 {{0,1,0,0},{1,1,1,0},{0,0,0,0},{0,0,0,0}}, // Z 形 {{1,1,0,0},{0,1,1,0},{0,0,0,0},{0,0,0,0}} };为什么用 4x4 而不是直接按形状的最小矩形?因为 4x4 是固定大小,旋转时不用重新分配内存,直接做数组变换就行。旋转的定义也很简单:转置之后把每一行反序,或者反过来。很多教程把旋转讲得玄乎,实际就是行列下标互换。
读这份代码你会注意到,作者用下标访问的方式很直接,没有任何抽象封装——这恰恰是新手友好的地方。你直接改blocks[2][1][3] = 0就能把 L 形挖掉一块,马上看到效果。
3.2 碰撞检测:下落前先问能不能走
碰撞检测是俄罗斯方块的核心逻辑,也是最容易出 BUG 的地方。合集里“俄罗斯方块3(下落有BUG,不会死)”就是这个问题:方块穿过已堆叠的方块继续下落,或者卡在边界里出不来,本质都是碰撞判定漏了条件。常见实现是写一个canMove函数:
// 判断方块在 (newX, newY) 位置是否合法 bool canMove(int block[4][4], int board[20][10], int newX, int newY) { for (int r = 0; r < 4; r++) { for (int c = 0; c < 4; c++) { if (block[r][c] == 0) continue; // 空位不参与检测 int boardX = newX + c; int boardY = newY + r; // 超边界:左墙、右墙、地面 if (boardX < 0 || boardX >= 10 || boardY >= 20) return false; // 与已堆叠的方块重叠 if (boardY >= 0 && board[boardY][boardX] != 0) return false; } } return true; }这里两个return false对应两类撞墙:第一类撞边界,第二类撞堆叠方块。注意boardY >= 0这个条件——方块刚生成时有一部分在棋盘上方(负坐标),这部分不能去碰棋盘数组,否则越界。
“下落有 BUG,不会死”这个文件名很诚实。按我的经验,这种 BUG 八成是下落时只检测了“是否到底”,没检测“是否与已有方块重叠”,或者检测时用了>而不是>=。你拿到这个文件后,直接搜canMove或者check,先看它的边界条件写全了没有。
3.3 控制台游戏的“帧”:Sleep 延时与 getch 输入
控制台游戏没有真正的帧率控制,主循环就是“检测输入→更新位置→重绘画面→延时”,四个步骤无限循环。经典的实现长这样:
while (true) { // 1. 输入 if (kbhit()) { char ch = getch(); if (ch == 'a') moveLeft(); else if (ch == 'd') moveRight(); else if (ch == 'w') rotate(); else if (ch == 's') softDrop(); } // 2. 逻辑:每帧下移一格 if (canMove(curBlock, board, curX, curY + 1)) { curY++; } else { mergeToBoard(curBlock, board, curX, curY); // 固定到棋盘 spawnNewBlock(); // 生成新方块 } // 3. 渲染 system("cls"); drawBoard(board, curBlock, curX, curY); // 4. 帧间隔 Sleep(100); }kbhit和getch是<conio.h>里的函数,kbhit检测键盘缓冲区是否有输入,有才去getch读取,这样不会阻塞主循环。Sleep(100)控制下落节奏,100 毫秒一帧。
合集里“俄罗斯方块1(上下左右控制).cpp”就是这种最朴素的写法,上下左右控制方向键。这块代码你如果直接编译跑,会感觉下落速度有点看运气:Sleep的精度和按键时getch的阻塞会让下落节奏不均匀。这也是为什么你能看到作者后来又写了“2”和“3”两个版本——他自己也在调。
3.4 这个合集为什么适合当教材
读这 4 个俄罗斯方块变体,你等于看完了“从能玩到玩得顺”的完整迭代。作者在文件名里标注的“有 BUG”“闪屏问题”“键位奇怪”,其实就是他的代码审查笔记。市面上很多教学代码是完美无瑕的,你读完只会抄;但这份合集里的标注 BUG,带着你主动去搜代码、判断哪里出了问题。
我读这类“带坑源码”的习惯是:先看文件名里的标注,再打字搜代码里的对应函数,最后动手改一行,确认自己的判断对不对。这份合集 115 个文件里有大量这种真实现场,比刷十道算法题都有用。
4. 拆两个有含金量的:五子棋 AI 与坦克大战双人键盘监听
跑完俄罗斯方块,接下来值得细读的是自带 AI 或双人交互的文件。五子棋 AI 能教你“电脑怎么下棋”,坦克大战的双人键盘能让你理解getch的底层行为——这两个是合集里最有 C++ 源码学习价值的主题。
4.1 五子棋 AI:打分函数比你想的简单
“五子棋AI对战.cpp”这类文件,核心不是搜索树,而是打分函数。简化版 AI 的思路是:遍历棋盘每个空点,按“这个位置对进攻有多有利、对防守有多重要”打一个分,然后下在分数最高的位置。打分逻辑一般长这样:
// 给某个空位置打分:同时考虑进攻和防守 int evaluatePoint(int board[15][15], int x, int y, int aiColor, int humanColor) { int score = 0; // 四个方向:横、竖、撇、捺 int dirs[4][2] = {{1,0},{0,1},{1,1},{1,-1}}; for (int i = 0; i < 4; i++) { // 进攻:自己颜色连续棋子数 * 进攻权重 int attack = countConsecutive(board, x, y, dirs[i][0], dirs[i][1], aiColor); score += attack * 10; // 防守:对方颜色连续棋子数 * 防守权重 int defend = countConsecutive(board, x, y, dirs[i][0], dirs[i][1], humanColor); score += defend * 8; } // 中心位置加成 int centerDist = abs(x - 7) + abs(y - 7); score += max(0, 14 - centerDist); return score; }countConsecutive是核心辅助函数:从(x, y)出发,沿给定方向数连续同色棋子数。进攻权重 10、防守权重 8,意思是“主动连子”比“堵对方”稍微优先一点——这个比例你可以自己调,调成 5 和 15 你就会得到一个很怂的 AI。
注意这里没有考虑跳子(比如X_XXX这种中间空一格的情况),所以这个 AI 水平不高,但逻辑清晰,适合读。你改这个文件的最佳入口就是调权重:把进攻调高它会疯狂冲四,防守调高它会一直堵你,直观感受“权重如何改变策略”。
4.2 UNO 与吃豆人:状态机是这类游戏共同的骨架
UNO 和吃豆人看起来一个卡牌一个动作,但代码结构其实是同一种东西:一个枚举状态加上一个大的switch。UNO 的核心是回合状态流转,吃豆人的核心是方向与移动。合集里的“UNO.cpp”和“吃豆人.cpp”都是这种结构:
enum GameState { WAIT_INPUT, // 等待玩家出牌 EXECUTE_CARD, // 执行牌的效果 NEXT_PLAYER, // 切换到下家 CHECK_WIN // 检查胜负 }; GameState state = WAIT_INPUT; while (state != GAME_OVER) { switch (state) { case WAIT_INPUT: // getch 读牌、合法性判断 state = EXECUTE_CARD; break; case EXECUTE_CARD: // 处理跳过、翻转、抽牌等效果 state = NEXT_PLAYER; break; case NEXT_PLAYER: // 换下家,重置状态 state = CHECK_WIN; break; case CHECK_WIN: // 手牌为空则结束,否则回到 WAIT_INPUT break; } }这种状态机写法是控制台游戏最常见的骨架。很多新手写游戏卡在“逻辑乱成一团”,就是没有用枚举把状态显式列出来。你读 UNO 那份代码时重点看:从“打出一张牌”到“下家行动”,中间要经过几个状态?每个状态维护哪些变量?读懂了,你写任何回合制小游戏都能直接套这个壳。
吃豆人同理。方向用enum Direction { UP, DOWN, LEFT, RIGHT },吃豆子检测就是“当前位置的迷宫数组值是否为 0”。你会在“吃豆人.cpp”里看到大量对二维数组的读改写,比五子棋更强调“移动”和“碰撞”的实时性。
4.3 坦克大战的双人键盘:getch 吃掉了方向键
双人游戏文件在合集里有好几个:“双人坦克.cpp”“双人对打(键位奇怪).cpp”“双人枪战.cpp”。其中“双人对打(键位奇怪)”这个标注,基本可以断定是作者踩了getch扩展键的坑。
方向键不是普通字符,按下左箭头,getch会返回两次:第一次是0xE0(或0x00),第二次才是真正的键码。只读一次getch的话,程序拿到的是0xE0,自然什么都不做,或者乱动。正确写法是检测到扩展键前缀后再读第二次:
int key = getch(); if (key == 0xE0 || key == 0x00) { key = getch(); // 第二次读取才是方向键真实键码 switch (key) { case 72: movePlayer1Up(); break; // ↑ case 80: movePlayer1Down(); break; // ↓ case 75: movePlayer1Left(); break; // ← case 77: movePlayer1Right(); break; // → } } else { // 普通字符键,比如 WASD 给玩家 2 用 switch (key) { case 'w': movePlayer2Up(); break; case 'a': movePlayer2Left(); break; case 's': movePlayer2Down(); break; case 'd': movePlayer2Right(); break; } }这也是“键位奇怪”的根源:作者大概率只getch了一次,方向键被吞了一半,游戏根本不受控制。你拿到“双人对打(键位奇怪).cpp”后,搜getch,看看是不是缺少第二次读取。改好之后这文件就正常了。
坦克大战的双人版还有一层:两台坦克的子弹碰撞、墙壁碰撞、出生点无敌判断。但输入这块通了,剩下的都是“判断坐标”的老套路。双人键盘监听在 C++ 控制台程序里没有太多高级玩法,把getch处理对,就解决了一大半。
5. 避坑:115 个文件里那些自带标注的坑
这份合集最有意思的地方是命名很诚实:“有 BUG”“闪屏问题”“键位奇怪”“偶尔有 BUG”全写在文件名里。这是作者留下的踩坑记录,省得自己再踩一遍。我把这些标注对应的坑梳理成五条,每一条都是“现象 → 原因 → 解决”的完整链路。
5.1 闪屏:system("cls") 的黑匣子代价
现象:跑 flappy_bird1(比 2 更精致,但有闪屏问题),画面一抖一抖的,晃动严重到没法玩。
原因:渲染循环每一帧都调用system("cls")清屏。这个是调用外部命令去清空整个控制台缓冲区,内部要做进程创建、控制台 IO 操作,速度远慢于绘图。画面等于“全黑一帧 + 重绘一帧”,肉眼可见闪烁。作者说“比 2 更精致”说明他加了更多绘制内容,结果cls的代价更明显。
解决:换成双缓冲离屏绘制,先画到一个大字符串里,再一次puts输出。简单做法:
string frame; frame.reserve(20 * 30); // 按控制台尺寸估算 for (int y = 0; y < 20; y++) { for (int x = 0; x < 30; x++) { frame += cells[y][x]; // 构造这一帧的画面 } frame += '\n'; } cout << frame; // 一帧一次性输出,代替 cls + 逐行绘制5.2 双击 exe 闪退:控制台程序的通病
现象:在 VS Code 终端编译成功,生成 exe,但到资源管理器里双击它,黑色窗口一闪就没了,什么都看不到。
原因:程序执行完主函数,进程直接退出,控制台窗口跟着销毁。这锅不在代码,在运行方式。
解决:临时做法是在文件末尾加一句getch()或者system("pause");正规做法是在终端里运行./game.exe,而不是双击。不要急着改源码——合集里有几十个文件都这样,你不可能每个都加system("pause")。用带着命令行参数的终端运行器,才是标准解。
注意:
system("pause")偶尔触发安全软件的拦截提示,因为它在内部调用 shell 命令。自己能接受就无所谓,但不建议批量往代码里塞。
5.3 键位奇怪:方向键的二次扫描
现象:“双人对打(键位奇怪)”里,按方向键没反应,或者按一次跳两格,而 ASWD 又正常。
原因:就是第 4 章说的扩展键问题。方向键在getch里返回两个值,第一值是0xE0或0x00。代码只读了一次,拿到的是前导码,后续逻辑全乱了。
解决:读两次。第一次读到的值如果是0xE0或0x00,就再读一次拿真实键码。搜这个文件里的getch,改成带二次读取的版本,这个“键位奇怪”的标题就属于历史了。
5.4 中文乱码:现代编辑器和老代码的代沟
现象:VS Code 里打开五子棋源码,窗口标题和菜单全乱码,编译后运行时中文也乱。
原因:源文件是 GBK,VS Code 用 UTF-8 解码,满屏“锟斤拷”。编译时没指定输入编码,printf里的中文字符串按错误编码进了二进制。
解决:要么 VS Code 右下角重新用 GBK 打开,要么-finput-charset=GBK -fexec-charset=GBK。这两个参数要成对用,前者告诉编译器源码怎么读,后者告诉生成程序运行时怎么输出。只加一个就会出现编译正常但运行时输出乱码。
5.5 三国杀 AI“偶尔有 BUG”:状态残留是硬伤
现象:“三国杀AI(偶尔有BUG).cpp”,AI 有时候不按规则出牌,或者跳过自己的回合,重开一局又正常。
原因:AI 的决策函数可能改了一些全局状态,本回合没清干净;比如“当前可选目标列表”在递归分支提前 return 时没还原。这种问题最难查,因为它不是必现,只有走到特定分支才触发。
解决:在决策函数入口把要用到的状态变量存一份备份,出口统一还原;或者在每次回合开始时重新初始化所有临时集合。这属于控制台程序里少数值得认真对待的“状态管理”问题。读完这个文件,你能体会到状态机的重要性——第 4 章那次枚举重构在这里就有反馈。
这五条踩完,115 个文件里八成的坑你已经见过同类。剩下的“gmon.out”这种文件,是作者某次用gprof性能分析留下的输出,不是源码,直接忽略就行。
6. 进阶用法:双缓冲、帧率与参数可调,老代码的“后悔药”
读完整套合集,最有价值的动作不是原样编译跑一遍,而是挑一个文件做“现代化改造”。我自己的基准改造顺序是:先干掉闪屏,再替换固定延时,最后把 AI 参数提成可调常量。
// 基础双缓冲模板:替代 system("cls") void render(const char* board, int rows, int cols) { static string frame; frame.clear(); frame.reserve(rows * (cols + 1)); for (int y = 0; y < rows; y++) { frame.append(board + y * cols, cols); frame += '\n'; } cout << frame; }这段代码把整帧画面合成进一个std::string,一次cout输出。相比每帧cls + 逐行绘制,输出次数从几十次降到一次,闪烁直接消失。接着把Sleep(x)替换成基于时间戳的帧率控制,让下落速度和机器性能脱钩:
auto last = chrono::steady_clock::now(); while (true) { auto now = chrono::steady_clock::now(); if (duration_cast<milliseconds>(now - last).count() >= tickMs) { update(); // 这里执行下落逻辑 last = now; } // 输入处理与渲染分开循环,输入不阻塞逻辑 }这样改完,“俄罗斯方块3(下落有BUG,不会死)”里的下落穿透问题也可能顺带暴露——因为帧率稳定后,卡顿不再掩盖碰撞检测的漏洞。最后把五子棋 AI 里的权重数字提成常量,想调 AI 强弱就不用改函数体,只改顶部参数表。
从那以后我每次拿到别人的源码,都强制自己走一遍“先看输入捕获、再看刷新方式、最后才看算法核心”的顺序,顺序反了,坑全踩一遍。这套合集让我用一晚上把别人踩过的坑预演了一遍,值了。希望帮到你。
本文还有配套的精品资源,点击获取