简介:这是一份基于C语言和C++实现经典“炸弹人”小游戏的完整源码包,主要面向刚接触游戏编程的初学者,适合用于编程练手或课程设计。压缩包共含9个文件,其中8个.cpp源文件分模块实现了地图生成、角色控制、炸弹放置、爆炸回溯、敌人移动和碰撞检测等核心功能,另有1个.wmv格式演示录像,便于对照运行效果理解代码逻辑;整个资源仅3.94MB,小巧易得。目前已有2095人学习下载,颇受入门者欢迎。深入阅读源码后,读者不仅能巩固C语言的数组、指针、结构体等基础语法,还能体会C++中类与对象、封装继承以及多线程计时等特性在真实项目中的运用,同时掌握游戏主循环、事件响应和定时炸弹等经典设计思路。对于希望从控制台程序迈向图形化小游戏开发的人来说,这是一份难得的阶梯性实践资料。
1. 等把炸弹人从零跑通,你才算真正用过 C/C++:这份源码包的定位与价值
很多人觉得炸弹人是新手玩具,但真把它从零写到跑起来,你才会发现数组、指针、循环、定时器这些东西不是背会就行,而是要在游戏主循环里互相配合。这份资源包正是一套 C/C++ 实现的控制台版炸弹人,压缩包里能看到 8 份编号不同的“炸弹人.cpp”,还有一段 .wmv 的运行演示,标题里的“c基础”和“炸弹人c++”两条标签点出了它的完整定位:先用 C 的数组和字符矩阵把地图做出来,再拿 C++ 的类去封装炸弹和敌人。它适合刚学完语法、想找 C++ 小游戏源码练手的人,也适合把 C 语言基础捡起来再战一遍的人。你要是冲着“能跑”去下载,会错过真正值钱的部分——炸弹倒计时、碰撞检测和地图数据是怎么组织的。
2. 压缩包结构先读懂再动手:8 份 cpp 与 .wmv 演示的定位
2.1 编号 1~8 的 cpp 代表两种常见组织方式
拿到压缩包,我一般不会急着编译,先把文件列表按编号过一遍。这份资源包里能稳定看到的就是编号 1~8 的“炸弹人.cpp”和一份“炸弹人.wmv”,这种命名在网上下载的 C/C++ 游戏源码里非常典型。它对应两种组织逻辑:一种是作者按开发阶段保存,编号靠前的 cpp 是纯 C 思路、甚至只有一个文件,编号靠后的 cpp 逐渐引入结构体、类,就成了“炸弹人c++”版本;另一种是按模块划分,比如入口、地图、玩家、炸弹、敌人、主循环各一个文件,数字只是保存顺序。
我建议先别猜,直接把每个 cpp 的 main 函数找出来,代码量最小的那份先编译。看清楚哪一份能单独跑,再决定以哪份为基准。这个过程能帮你快速判断一份 C/C++ 小游戏源码包的“主版本”是哪一版,而不是对着 8 个文件挨个试错。下载资源里常见的坑是:版本太多,改到最后不知道自己在改哪一份。
| 文件线索 | 常见定位 | 我的判断方式 |
|---|---|---|
| 编号小的 cpp | 早期版本 / 单一文件版 | 先看是否包含 main 函数,代码量最少可先编译 |
| 编号大的 cpp | 成熟版本 / 模块拆分版 | 看是否有类定义、多个头文件引用、函数拆分更细致 |
| .wmv 演示视频 | 作者录制的运行效果 | 先看一遍,确认最终画面、关卡结构再动代码 |
看完演示视频,你会对这份资源最终要长成什么样有数。视频里如果有明显的按键停顿、花色输出,说明作者用到的可能是 _getch 加光标定位的方案,这些都可以作为后面复现的参照。很多人一上来就跳过了 wmv,结果自己写的渲染效果和原版差很远,回头排查半天才发现是输出方式选错了。
2.2 技术栈判断:控制台、标准库和少量 Windows API
通过标签和文件构成基本能判断,这个项目会用到 iostream 或 printf 做输出,使用 ctime 处理时间相关逻辑,地图渲染采用字符矩阵,输入处理大概率落在 Windows 的 conio.h 上。这种组合不需要额外图形库,也正因为它只依赖标准库和少量系统调用,才适合拿来练“C 基础”。
编译控制台程序,在 Windows 上我一般推荐两个路径:一个是 VSCode 配置 C/C++ 环境后装 MinGW-w64,用 g++ 直接编;另一个是直接开 Visual Studio 的空 C++ 控制台项目。先看最小可编译的那份 cpp,用命令行编译最直接:
g++ 炸弹人.cpp -o bomber.exe如果你的某个版本已经按模块拆成多文件,需要把所有参与编译的 cpp 都写进命令行:
g++ main.cpp map.cpp player.cpp bomb.cpp -o bomber.exe这里 -o 参数指定输出文件名。多文件编译时链接失败,通常是某个被调用的函数不在传入的 cpp 里,或者依赖的头文件路径没配好。遇到 undefined reference 先别怀疑编译器,去查函数定义在哪个文件,再把它加进编译命令就行。
动手前还有一件小事:把整个压缩包先复制一份做备份。这份资源里有多个版本,你改着改着就会遇到“明明昨天还能跑,今天怎么编译不过”的情况,备份一份“原始版”相当于是给自己的后悔药。这个习惯我后面几乎每次拆源码包都会用,尤其这种带编号文件的包,没有备份基本等于裸奔。
3. 地图与移动先落地:二维数组、字符串初始化与碰撞检测
3.1 地图建模:二维字符数组为什么比 int 数组更顺手
炸弹人的地图本质是一张网格,网格里每一格只有几种状态:空地、墙体、砖块、出生点。C 语言基础里正好用二维数组覆盖这个场景。常见做法是定义这样一个二维字符数组:
#define ROWS 13 #define COLS 15 char map[ROWS][COLS] = { "#############", "#P # B#", "# ### #", "# B #", "#############" };注意看,每一行字符串的长度必须和 COLS 匹配,尾部还有个隐藏的 \0。用字符串数组初始化地图,比逐格写 char 赋值直观得多,这是 C++ 字符串数组初始化里最典型的一种用法,也是新手最容易在长度上翻车的地方。字符含义可以统一约定:# 是墙体,B 是砖块,空格是空地,P 是玩家出生点。
为什么不用 int 数组?因为打印的时候要把每个数字再转成符号,多一道转换逻辑。字符数组直接把要显示的内容放进地图,printf 逐行输出就能当画面用。更深一层,map[row][col] 在指针层面的写法是((map + row) + col),这份资源如果往后拆解代码,嵌套数组和指针的关系是绕不开的 C 基础考点。
3.2 玩家移动:先计算目标格,再决定是否真的走
移动逻辑是所有 2D 游戏最核心的通用套路:读方向键、计算目标坐标、检查目标格能不能走、最后才更新玩家坐标。新手最容易犯的错误是先把坐标改了,再去看碰撞,结果角色卡进墙里还要倒推。我在游戏项目里写移动,永远先算“未来位置”:
int movePlayer(int dx, int dy) { int nx = player.x + dx; int ny = player.y + dy; if (nx < 0 || nx >= COLS) return 0; if (ny < 0 || ny >= ROWS) return 0; if (map[ny][nx] == '#') return 0; player.x = nx; player.y = ny; return 1; }dx 和 dy 的取值只有四种组合:上移是 (0, -1),下移是 (0, 1),左移是 (-1, 0),右移是 (1, 0)。这里必须先做边界检查再碰 map,否则数组越界后读到的是乱七八糟的脏数据,表现就是地图花掉或者程序闪退。碰撞检测的粒度到这里只判定了墙体,砖块 B 能不能走要看游戏规则——经典炸弹人的设定里,砖块可以走上去但不能直接被踩碎,需要在它旁边放炸弹才能炸开。
输入这部分,Windows 控制台最常用的是 _kbhit 配合 _getch:_kbhit 判断有没有按键,_getch 读取按键码。方向键比较特殊,它会先回一个前缀码,再回一个扫描码,所以要用两个 _getch 读完整:
if (_kbhit()) { int key = _getch(); if (key == 224 || key == 0) { key = _getch(); } if (key == 72) movePlayer(0, -1); // 上 if (key == 80) movePlayer(0, 1); // 下 if (key == 75) movePlayer(-1, 0); // 左 if (key == 77) movePlayer(1, 0); // 右 }_geth 返回 224 或 0 时说明后面还有第二个字节。这个细节不处理,最常见的结果就是按一下方向键角色走两格,或者方向完全错乱。代码里的 72、80、75、77 分别是方向键的扫描码,换到不同编译器下基本一致。
3.3 画面刷新:整屏清屏 vs 光标定位重绘
控制台游戏的第二个经典问题是刷新方式的取舍。最简单的方法每帧来一次 system("cls"),然后重新打印整个地图。缺点非常明显:闪屏严重,尤其地图到 13×15 以后,连续清屏重绘会看到画面在抖。
我一般会改成光标定位后局部重绘,Windows 下用 SetConsoleCursorPosition 把光标挪回左上角,重新从 (0,0) 开始画地图:
void gotoxy(int x, int y) { COORD pos = { (SHORT)x, (SHORT)y }; HANDLE hOut = GetStdHandle(STD_OUTPUT_HANDLE); SetConsoleCursorPosition(hOut, pos); }这个函数里 COORD 是 Windows API 自带的坐标结构,x 和 y 是控制台字符坐标,单位不是像素,而是字符格。每次进入渲染帧前调用 gotoxy(0,0),再刷一遍 map,画面就不会像 cls 那样先清屏再闪烁。如果资源包里原本用的是 cls 方案,想改善手感,把渲染函数改成这个方案是从能跑到跑顺的第一步。
4. 炸弹倒计时与爆炸范围:对象封装、主循环定时器与四向扩散
4.1 用对象封装炸弹:从散落全局变量到一个列表
当场上同时存在多颗炸弹时,每一颗都要记录自己的坐标、剩余时间、爆炸范围。用散落的全局变量不是不行,但会产生大量重复代码。标签“炸弹人c++”已经暗示,这份资源在演进后大概率引入了面向对象。实际操作中用结构体或类都可以:
struct Bomb { int x, y; int timer; int range; bool alive; }; #include <vector> std::vector<Bomb> bombs;timer 的单位是“帧”,不是秒。炸弹放出去之后,主循环每跑一帧就把所有炸弹的 timer 减 1,减到 0 就触发爆炸。这里用帧而不是用真实时间,目的是让倒计时和画面刷新同步。range 是爆炸半径,经典炸弹人默认是 1,也就是上下左右各扩一格;吃到道具后可以长到 2 到 3。为什么用 vector 而不是固定数组?因为你不知道玩家一口气会放多少颗炸弹,vector 能动态增长,而且它属于 STL 里最基础、最容易上手的容器。
每一帧对炸弹的更新,就是遍历 bombs,把 timer 减 1,alive 标记为 false 的清理出列表。看起来简单,但这个“遍历-修改-回收”的模式是很多游戏逻辑的雏形,比到处 new 和 delete 安全得多。
4.2 定时器设计:主循环帧计数,而不是 Sleep 死等
新手写炸弹倒计时最常见的是这样:放了炸弹,然后 Sleep(1000)。表面看时间到了,其实整个游戏卡死了一秒,这一秒里方向键没反应,敌人也不动,体验非常糟糕。还有人会为这个去开多线程,让炸弹线程单独计时,结果炸弹列表 vector 在主线程和炸弹线程同时被改,程序崩溃概率直线上升。
对这个量级的控制台游戏,最稳的方案是有一个固定节奏的主循环,循环里做三件事:读输入、更新逻辑、渲染画面。炸弹倒计时的等待完全可以用帧计数替代:
clock_t lastFrameTime = clock(); while (running) { clock_t now = clock(); int elapsed = (int)(now - lastFrameTime); if (elapsed >= 50) { lastFrameTime = now; handleInput(); updateLogic(); render(); } }50 毫秒是一帧,也就是每秒大约 20 帧,对控制台炸弹人足够顺滑。想更流畅就改成 30 毫秒,想更慢就改成 80 毫秒。updateLogic 里遍历 bombs 做 timer 递减,完全不会有 Sleep 带来的阻塞,也不需要额外线程。clock_t 返回的是毫秒级时间差,C 基础阶段用 clock() 比 C++11 的 chrono 更好理解,代码也能直接对应到 ctime 头文件的使用。
如果这份资源里有任何一处用了 Sleep 做计时,建议第一时间改成帧计数方案。改了之后你会明显感觉到整个程序“活”了,移动、放炸弹、敌人行动可以同时进行,这才是游戏该有的状态。
4.3 爆炸范围的四向扩散:碰到墙要停,碰到砖块破坏后也停
炸弹爆炸不是全场一下子炸光,而是从炸弹位置向上下左右四个方向蔓延,蔓延到墙体必须停住,蔓延到砖块则把砖块炸成空地并停住。代码上就是两层循环,外层控制四个方向,内层控制距离:
int dirs[4][2] = { {1,0},{-1,0},{0,1},{0,-1} }; for (int i = 0; i < 4; i++) { for (int step = 1; step <= b.range; step++) { int ex = b.x + dirs[i][0] * step; int ey = b.y + dirs[i][1] * step; if (ex < 0 || ex >= COLS || ey < 0 || ey >= ROWS) break; if (map[ey][ex] == '#') break; if (map[ey][ex] == 'B') { map[ey][ex] = ' '; break; } } }step 从 1 开始是因为炸弹自身那一格不算爆炸蔓延。碰到砖块那行,是把砖块改成空地后还要 break 一下,否则会继续穿透到砖块后面的格子。边界检查放在 map 访问之前,这是任何地图操作都必须遵守的顺序,避免越界。这个四向扩散逻辑不复杂,但它同时覆盖了循环、break、数组越界和规则优先级,是这份资源里最有练习价值的几段代码之一。
爆炸对人和敌人的判定,常见做法是把爆炸经过的格子临时打上标记,比如把 map 改成 '',然后遍历玩家和所有敌人的坐标,凡是在 '' 格子上的人都被炸到。要注意区分“玩家主动走进爆炸格”和“爆炸蔓延过玩家所在格”这两种情况,处理顺序不对会出现无敌帧。
5. 常见问题与排查:编码、运行库、输入缓冲与炸弹延迟五处现场
同一个炸弹人项目,不同人跑出来的毛病几乎一样。下面这五条我每次帮人调试时几乎都会撞见,按“现象、原因、解决”整理出来,后面复现这份资源时可以直接对号入座。
- 地图和提示全是乱码,英文正常、中文是“锟斤拷”一类的东西
现象:程序跑起来,英文能显示,中文提示和特殊字符全部乱掉。
原因:源文件保存的是 UTF-8 编码,而 Windows 控制台默认代码页是 GBK。字符串常量在编译时按源文件编码存进程序,输出时又按控制台代码页解释,两边不一致就只能得到乱码。很多 VSCode 配 C/C++ 环境踩到第一个坑就是这个。
解决:两种方案任选。要么把源文件在编辑器里另存为 GBK 或 ANSI 编码再编译;要么在 main 最开始强制设置控制台代码页:
#ifdef _WIN32 #include <windows.h> SetConsoleOutputCP(CP_UTF8); #endif注意这行代码必须在任何输出之前执行。如果已经是 GBK 编码,就不需要再调它。
- 编好的 exe 拷到别的电脑双击没反应,提示缺 DLL
现象:自己电脑编译运行没问题,发给别人或者换一台电脑,提示找不到 VCRUNTIME140.dll 或 MSVCP140.dll。
原因:编译器默认用动态连接方式,程序依赖 Visual C++ Redistributable 运行库。目标机器上没装对应版本的运行库,自然跑不起来。
解决:发布前把编译方式改成静态连接。Visual Studio 里在项目属性中把运行库改成“多线程静态 (/MT)”,MinGW 下加 -static 编译:
g++ 炸弹人.cpp -o bomber.exe -static这样生成的 exe 不再依赖外部运行库。如果一定要动态方式分发,那就让目标机器装对应的 VC++ Redistributable 包,这也是这套资源不带任何绿色主程序文件时会遇到的主要问题。
- 按方向键没有实时反应,必须敲一下回车才动
现象:角色不是按下方向键就动,而是要按一次回车才走一格。
原因:getchar、cin 等输入方式都带行缓冲,要等到换行符才把数据交给程序。游戏是实时交互,根本不走“回车确认”这个逻辑。
解决:换用 conio.h 里的 _getch(),它直接读取按键,不需要回车,也没有回显。代码见 3.2 节。注意古代 BC++ 时代的 getch() 在新编译环境里可能被标记为已弃用,优先用 _getch 或 _getwch 并确认你的环境认这个写法。
- 按一次方向键角色走两格
现象:明明只按了一下方向键,角色一口气跑了两格。
原因:常见于方向键读取只处理了扫描码,没处理前缀码。方向键按下时,_getch 会先返回 224 或 0,再返回真正的方向扫描码。如果第一次 _getch 把 224 当成一个普通按键,就会误触一次;第二次再读扫描码又会触发一次,加起来就是两格。
解决:按 3.2 节里的写法,先判断 key == 224 或 key == 0,是则再读一次。把这个逻辑封装成 getDirection() 函数,后面所有按键处理都走这一个入口。
- 放完炸弹整个人卡住,一秒后才能继续操作
现象:炸弹放下去,所有操作停住,1 秒后恢复,炸弹也刚好爆炸。
原因:代码里用了 Sleep(1000) 来等倒计时,主线程被阻塞,输入和敌人移动全部跟着停摆。有的版本更复杂,用线程做倒计时,结果炸弹 vector 在遍历时被改,甚至越界崩溃。
解决:删掉所有 Sleep 延时,改成主循环帧计数。炸弹的 timer 在 updateLogic 里每帧减一,减到 0 就爆炸,见 4.2 节。这是“看起来简单但体验差距巨大”的改动,改完整个程序像换了一个游戏。
6. 从能跑到能改:加道具系统时真正要动的三个参数点
想要给这份炸弹人加道具、加存档,第一步不是写新代码,而是把散落的魔法数字收拢到固定位置。我见过太多人在地图里写死坐标、在爆炸判定里写死范围,改一次要翻遍整个文件。
const int MAP_ROWS = 13; const int MAP_COLS = 15; const int DEFAULT_BOMB_RANGE = 1; const int MAX_BOMB_COUNT = 3;把地图尺寸、默认爆炸半径、同屏最大炸弹数都定义成常量,之后新增道具时只改常量再重新编译,代码逻辑完全不用动。
道具本身用枚举比用字符串可靠得多:
enum Item { ITEM_NONE, ITEM_BOMB_UP, ITEM_RANGE_UP, ITEM_SPEED_UP };枚举值直接存进地图二维数组,比如用 '1'、'2'、'3' 这样的单字符表示道具,地图解析时 switch 一下就能对应到道具类型。想加“本地双人对战”“更高难度敌人”时,关卡布局也可以单独用文本文件存,运行时读进来,而不是写死在源码里。
存档就更简单了,把玩家坐标、当前关卡、剩余炸弹数写成一行文本:
FILE* fp = fopen("save.txt", "w"); if (fp) { fprintf(fp, "%d %d %d %d\n", player.x, player.y, level, score); fclose(fp); }读档用 fscanf 反向读回,配合文件操作相关知识点正好把 C 基础里的文件部分也用上。从那以后,我每次拿到这种带编号的源码包,都会强制自己先跑一遍原始版本、做一份备份、再动手改功能,反复避免“我以为改的是最新版”这种低级问题。这套流程帮我避开了绝大多数返工,希望帮到你。
本文还有配套的精品资源,点击获取