简介一个基于 C/C 实现的经典“炸弹人”小游戏项目很适合编程初学者作为游戏入门练习。项目以 C 语言基础语法与数据结构为根再借助 C 的类与对象组织角色、炸弹、地图等模块完整呈现了从玩家控制、炸弹放置到爆炸判定、障碍物消除与敌人 AI 的经典玩法。代码结构围绕游戏逻辑拆分既便于理解循环、条件、数组、指针、结构体等基础概念也能体会面向对象设计带来的可维护性涉及计时、碰撞检测等关键点是游戏编程入门相当合适的练习素材。资源包共 9 个文件其中 8 个 cpp 源文件构成完整工程另含 1 个 wmv 演示视频方便对照运行效果整体压缩后仅 3.94MB轻量易用已有 2095 人浏览或学习该资源。对想用 C/C 快速完成从控制台到小游戏实践项目的同学这套代码既能加深对基础语法的掌握也能为进一步学习图形界面、多线程等进阶技术打下基础。1. 一个只能跑控制台的炸弹人凭什么值得当 C/C 练手项目这两天我拆了个「炸弹人_c基础_炸弹人_炸弹人c」源码包外面看着不起眼一首歌的时间就决定的游戏。拆完最大的感受是控制台版的炸弹人不该被嫌弃土它恰好把 C 语言里数组、指针、结构体这些基本功和 C 的类、枚举、std::vector 串在了一条线上。新手从控制台版开始比一上来就碰图形库、碰引擎要踏实得多。这份资源适合两类人一类是刚学完 C 语言基础教材里的题目快做吐了想找个 2000 行左右的完整项目验证自己另一类是 C/C 转做小游戏门槛在犹豫想先搞清楚「游戏循环、碰撞检测、定时器倒计时」这些概念到底怎么落到代码里。它解决的问题也很具体一个玩家角色在地图上移动放炸弹炸软墙、炸敌人同时别把自己炸死。所见即所得跑起来就能看见自己写的逻辑在干活。我用 Dev-C 和 VSCode 各编译了一遍能过、能跑、能玩视频文件 bomb.wmv 就是演示录像。往下我会把源码拆成可消化的小块讲清楚地图、主角、炸弹、爆炸波及、敌人 AI 各自由谁负责再补上实打实的避坑记录。2. 拆包先看骨架地图、角色、输入轮询各自用 C 还是 C2.1 压缩包里到底有什么这份项目压缩包是「炸弹人.rar」里面躺着一份炸弹人.wmv 演示录像和若干个 .cpp 源文件没有工程文件也没有 .h 头文件。也就是说拿到手的第一步不是打开 IDE 点「运行」而是先想清楚这些 .cpp 是同一个程序的不同版本还是不同模块。我拆开后判断这是不同阶段的迭代版本编号大的是改过 bug 的完整版。我没有改动文件本身的逻辑先按文件名排了个推测表文件名推测重点炸弹人 1.cpp最早版通常只有地图绘制和角色移动炸弹人 2.cpp开始有按键响应和游戏主循环炸弹人 3.cpp加入炸弹放置和简单倒计时炸弹人 4.cpp加入爆炸范围计算炸弹人 5.cpp加入软墙破坏和物品掉落炸弹人 6.cpp / 7.cpp修正交互逻辑加入胜负判定炸弹人 8.cpp相对完整的版本可能带简单帧率控制新手读这份资源别从 1 开始逐行背从 5 之后的版本往回读容易头大。正确顺序是先看编号最大的因为里面的地图数据结构和游戏循环最完整遇到一个函数看不懂再往回找编号小的版本看它最初怎么写观察作者是怎么一步步把烂思路修好的这对写代码的习惯养成比跑通程序本身更有价值。2.2 地图数据结构C 数组和 C vector 选谁炸弹人最底层的结构是地图。常见实现方式是用二维数组存格子的类型每个格子映射一个字符输出到控制台。在纯 C 写法里是 int map[13][15]在 C 写法里会换成 std::vectorstd::vector 以便在运行时动态重置关卡。这份资源里两种风格都有影子核心逻辑基本一致。#include iostream #include vector // 地图格子枚举0空地 1硬墙 2软墙 3炸弹 4火焰 enum Cell { EMPTY 0, WALL 1, SOFT_WALL 2, BOMB 3, FIRE 4 }; // C 风格用 vector 做动态地图方便每关重新生成 std::vectorstd::vectorint initMap(int rows, int cols) { std::vectorstd::vectorint map(rows, std::vectorint(cols, EMPTY)); for (int i 0; i rows; i) { for (int j 0; j cols; j) { // 四周边界放硬墙内部按奇偶位置生成软墙 if (i 0 || j 0 || i rows - 1 || j cols - 1) { map[i][j] WALL; } else if (i % 2 0 j % 2 0) { map[i][j] WALL; } else if (rand() % 100 40) { map[i][j] SOFT_WALL; } } } return map; }地图长宽一般取奇数常见 11x13 或 13x15这样四周硬墙固定后内部还能留出相对对称的通道。硬墙用奇偶位固定生成是为了保证玩家从出生点往四个方向移动时不会出现完全封死的死角软墙用 rand() % 100 40 决定密度数值可调改到 60 就是苦难模式。它对应的 C 写法是用 int map[13][15] 全局数组对 C 基础学习者来说更直观但因为大小编译期锁定后面想加关卡、换地图尺寸就得改常量再重编。项目里多版本演进的一大动力就是有人把 C 数组版重构成 vector 版重启关卡时重新 resize 就行。我建议你两个版本都跑一遍重点体会「为什么 C 更适合做游戏对象管理」这个感觉。2.3 角色对象结构体够用类更好扩展炸弹人里玩家、敌人、炸弹本质上都是「有坐标、有状态、有存活标记」的物件。纯 C 版会用 struct 定义再写一个 update() 函数操作它C 版会写成 class Player把移动函数收进类内部。这个小细节恰好是理解面向对象入门的关键。看过这份源码后你的收益是平时课本上讲类和对象的差别是空的摆一个炸弹人实例你就懂了。#include iostream // 一个最小可用的玩家结构 struct Player { int x, y; // 当前所在格子坐标 bool alive; // 是否存活 int bombCount; // 可放置炸弹数量 int bombPower; // 爆炸范围半径 }; // C 风格把行为绑定到数据上 class Bomber { public: int x, y; int bombCount; int bombPower; bool alive; // 尝试向上移动一格collision 由外部地图判断 bool moveUp() { if (y - 1 0) { // 只做边界粗检真正的阻挡判断在外面做 y y - 1; return true; } return false; } };在旧版小游戏里常见做法是先写 struct因为字段少直接访问比 getter/setter 省事到后面加入敌人 AI、道具、无敌帧后用 class 把属性包起来才能避免全局变量满天飞。推荐按这条路径读源码先看 struct 版本怎么被硬编码逻辑操作到改不动再看 class 版本如何靠成员函数把移动、放炸弹、死亡处理拆干净这样你能直观感受到演进的意义。移动函数只改坐标不校验能否穿过墙体。真正的碰撞检测放在游戏主循环里先保存旧坐标计算新坐标查到目标格子是否可以走不可走就回退旧坐标。这样保证角色永远不会嵌进墙里。2.4 控制台输入轮询_getch 和 kbhit 的角色控制台游戏的输入不靠回车键而是靠 _getch() 读取单个按键配合 kbhit() 判断当前是否有按键按下。这份资源里几乎所有版本都用这套组合唯一要注意的是 conio.h 不是标准 C/C 头文件Windows 下有效Linux 下编译会直接报错找不到。这也是初学者最容易对源码产生「怎么在他机器上能跑我机器上不行」疑惑的重灾区。#include conio.h // Windows 下的 _getch / kbhit #include iostream // 阻塞读取按一下方向键角色就动一次 // _getch() 不回显、不需要回车适合控制台游戏 int key _getch(); // 非阻塞判断有没有按键在排队 // 没有关键就返回 0立即继续下一帧 if (kbhit()) { key _getch(); if (key w || key 72) { /* 上方向键 */ } else if (key s || key 80) { /* 下方向键 */ } }方向键和 WASD 的区别在于方向键按下时 _getch() 会返回两个值第一个是 224表示功能键第二个才是 72、80、75、77 这类方向码。如果只读一个值你会发现自己按下方向键根本没反应或者角色原地乱蹦这是控制台游戏输入最常见的坑。前两章的源码框架解决了「程序结构怎么摆」的问题接下来进入真正的炸弹人核心逻辑。3. 把游戏循环拆开移动、炸弹倒计时、爆炸蔓延和敌人的随机走位3.1 主循环用什么驱动、在哪放延时这份资源的 5 号版本里主循环基本定型while(true) 里做一轮操作、刷新地图、暂停一秒的一部分。控制台版的帧率不是靠垂直同步就是 sleep 一段短时间常见做法是 Sleep(50)也就是每秒刷 20 次左右。Sleep 放错位置会导致角色移动一卡一顿这个坑我放到第五章重点说。#include Windows.h // Sleep 所在头文件 #include cstdlib // system(cls) 刷新屏幕 while (true) { // 1. 检测 input 更新目标方向 if (kbhit()) { input _getch(); } // 2. 更新玩家位置碰撞检测在这层做 updatePlayer(input); // 3. 更新炸弹计时、爆炸扩散 updateBombs(); // 4. 刷新屏幕 system(cls); drawMap(); // 5. 控制帧率 Sleep(50); }注意第一步先只记方向不立刻移动到 updatePlayer 里统一移动原因是玩家按下方向键的频率跟 Sleep 周期不一致按下的时候角色应该在下一帧才走一格才能避免按得快角色飘起来。第三步更新炸弹不能放在绘制之后否则爆炸倒计时和画面上看到的爆炸位置差一帧。控制台清屏用 system(cls) 在 Windows 下最快但每次清屏会闪屏更稳的写法是 gotoxy 光标回 0,0 直接重绘减少闪烁。源码里的扫尾版本有用后者看编号靠后的 .cpp 能发现。3.2 炸弹放置与倒计时fuseTimer 数值统一是关键炸弹的核心数据结构就是一个数组/vector记录每颗炸弹的 x、y、剩余时间。所有炸弹共享同一个 tick用玩家移动次数或帧数来递减。把炸弹的 fuseTimer 改成毫秒又在玩家的帧循环里每帧减 1会直接把炸弹变成一放就炸。它的正确思路是一个全局游戏 tick 每帧走一格炸弹到时立刻爆炸。struct Bomb { int x, y; int fuseTimer; // 倒计时单位是「帧」不是毫秒 int power; // 爆炸半径 bool exploded; }; void updateBombs(std::vectorBomb bombs, int gameTick) { for (auto b : bombs) { if (b.exploded) continue; b.fuseTimer - 1; // 每调用一次减 1调用频率由 Sleep 决定 if (b.fuseTimer 0) { explodeBomb(b); b.exploded true; } } }放置炸弹时限制同屏炸弹数量常见写法是炸弹数量上限 1~3 个玩家按空格时遍历 vector若场上炸弹数小于上限且当前位置不是炸弹才 push 一颗新炸弹。用键盘的空格键还是闪光弹之类见不同版本的按键绑定开局看 .cpp 里 switch 部分即可。为了画面可读地图格子在主角站的位置绘制成 P炸弹绘制成 B火焰绘制成 *。这些字符在 drawMap 里统一 map 到输出改程序变量时不要直接在绘制里硬编码多个字符否则后期调爆炸半径会改到崩。3.3 爆炸蔓延四方向扩散不是 if 套 if是半径递减爆炸是实现时最容易偷懒的地方。刚写 C 程序的人常用四段 if 分别处理上下左右每段再写三个 if 来覆盖三格距离结果半屏代码全是重复。正确的做法是利用方向数组一个 for 循环把四个方向的格子全检查掉。#include vector // 四个方向向量上、下、左、右 const int dx[4] {0, 0, -1, 1}; const int dy[4] {-1, 1, 0, 0}; void explodeBomb(const Bomb bomb, std::vectorstd::vectorint map) { // power 是半径向外逐格寻找 for (int dir 0; dir 4; dir) { for (int step 1; step bomb.power; step) { int nx bomb.x dx[dir] * step; int ny bomb.y dy[dir] * step; if (nx 0 || nx map.size() || ny 0 || ny map[0].size()) { break; } if (map[nx][ny] WALL) { break; // 硬墙挡住不穿透 } if (map[nx][ny] SOFT_WALL) { map[nx][ny] EMPTY; // 软墙炸掉后变空地 break; // 但爆炸不继续穿 } map[nx][ny] FIRE; // 空地变成火焰 } } }第一层 for 循环控制四个方向第二层 for 从 1 到 power 逐格找碰到硬墙立刻 break碰到软墙把它炸空同时 break。这里第二个 break 是新手最容易漏掉的一个小细节少了它火焰会穿透软墙烧到墙后面很不自然的爆炸机制。炸弹中心格子本身也要设置成 FIRE。爆炸产生的 FIRE 是临时状态玩炸弹人的都知道不能让它永久占着地图否则下一个回合还没放炸弹整块地图就乱了。通常在 explodeBomb 里调用一次 setFireTimer()把火焰的剩余帧存起来在 updateBombs 的同一帧循环里递减烧完置回 EMPTY。注意必须在 update 阶段做不能在 draw 阶段做否则画面要重复渲染两遍。3.4 敌人 AI 的随机走位rand 要种否则每局一样这段资源的敌人 AI 相对简单敌人智能随机选一个可行方向移动到死角就回头。核心是随机初始化。老版本的 C 程序容易忘记 srand(time(NULL)) 或者只用 srand(rand())那样每局游戏敌人走位完全一样趣味性打折。另一个坑是用 rand()%4 均匀决定方向遇到撞墙后不加处理敌人会卡在墙边抖动。#include ctime #include cstdlib void initRandom() { // 一定要在程序一开始只调用一次 srand(static_castunsigned int(time(nullptr))); } int getRandomDirection() { // 0上 1下 2左 3右 return rand() % 4; }敌人每帧随机的方向未必可行处理逻辑是先记录之前的方向随机得到一个目标方向后检查目标格子是否可走不能走就保留之前方向。这样不会出现敌人翻脸站在原地不走的现象。除了随机想一下之外不少改进版加了简单的追玩家逻辑每隔一个固定周期直线朝着玩家所在行/列移动追不到就换随机。这样难度曲线比纯随机好很多。这张地图的碰撞检测同时服务玩家和敌人角色移动前先查目标格子是不是 WALL 或 SOFT_WALLBOMB 也不能走FIRE 对玩家是即死对敌人直接清除。地图上另一个重要的判定是玩家和敌人的对象坐标必须用方块格子的行列不能用像素坐标否则控制台版看起来会像角色飘在格子之间。4. 把源码编译跑起来Dev-C 和 VSCode 两种环境的完整操作4.1 用 Dev-C 跑通最早的那个版本Dev-C 是 C 基础学习阶段最常见的 IDE虽然老但对这种单文件小游戏而言打开即跑所有依赖都打包在内置环境里。资源压缩包里每个 .cpp 都是独立的可以任意一个单独新建一个控制台项目测试。最稳的跑法是新建空项目、把源文件加进去编译而不是直接双击 .cpp 文件。具体步骤打开 Dev-C文件 → 新建 → 源代码把压缩包里编号 8 的内容整体粘贴进去如果你的版本有编码问题会产生乱码。项目 → 编译运行或者直接按 F11。如果弹出「源文件未编译」先检查编译器选项里是否选中了「支持 C11」这份资源用到 vector 和 auto 的版本需要 C11老 Dev-C 默认设置是 C98会报一堆看不懂的语法错误。4.2 VSCode 配置 C/C 环境tasks.json 里一次调好现在更多人习惯用 VSCode。配置 C/C 环境的核心是三步装 C/C 扩展装 MinGW-w64写一个 tasks.json 负责编译。网上很多说着「一行命令跑 C」的帖子其实变量名各有各的坑下面这份是我常用的配置模板MinGW 装在 C:\mingw64 时可用。{ version: 2.0.0, tasks: [ { label: build bomber, type: shell, command: g, args: [ -g, bomber.cpp, -o, bomber.exe ], group: { kind: build, isDefault: true }, problemMatcher: [$gcc] } ] }注意命令里的 bomber.cpp 要替换成你保存的文件名。如果你把源文件放在子目录里args 要写成子目录路径例如 src/bomber.cpp。另外 -g 表示生成调试信息用于断点观察变量调试游戏循环时这个参数很值得保留。编译成功后在终端运行 ./bomber.exe打开游戏窗口。如果直接双击 .exe运行起来屏幕上显示地图和角色说明控制台环境没问题。若是报错缺少 libgcc_s_seh-1.dll 或 libwinpthread-1.dll那是 MinGW 的 bin 目录没有加到 PATH 环境变量把 C:\mingw64\bin 加进系统 PATH 再重新打开终端即可。4.3 运行后和 WMV 对照功能没实现对不上时的排查顺序资源里附带的炸弹人.wmv 是演示录像运行源码后要做的第一件事不是玩而是对照录像确认基本行为一致角色能不能上下左右走、空格能不能放炸弹、炸弹倒计时是不是 3 秒左右爆炸、爆炸是不是十字形蔓延、软墙炸开后有没有变空地。如果发现角色不动先确认终端窗口处于激活状态点击窗口后再按键盘如果空格放不出炸弹检查按键绑定是空格还是 J 或回车如果炸弹立即爆炸检查 fuseTimer 初始值。这种「功能对不上录像」的问题排查顺序固定先看输入读到没有再查状态机是否更新最后查地图绘制。顺序反过来容易在绘制函数里绕半天最后发现是输入没捕获到。4.4 帧率、字符乱码和控制台大小的一并解决运行最经常遇到的三个现象是闪屏严重、上下键没反应、中文显示乱码。闪屏用 gotoxy 代替 system(cls)方向键问题按第二章所述读两个字节收到 224 后再读一个方向码中文乱码绝大多数因为源文件是 UTF-8 编码而 Windows 控制台默认用 GBK 显示。最省事的解决办法是在 main 开头加一句#include Windows.h int main() { SetConsoleOutputCP(CP_UTF8); // 后续游戏逻辑 }如果你的 Dev-C 版本较旧加了这句还是乱码那就把源文件另存为 ANSI 编码再不行就直接全部改成英文输出。一个小游戏界面不是重点FIRE、BOMB、P 都写成英文完全不影响玩。VSCode 环境下如果按 F5 调试弹出「无法打开专属的终端」需要在 launch.json 里把 externalConsole 设为 true否则控制台游戏跑起来输入完全无法响应看起来像死机。5. 避坑记录控制台版炸弹人最容易翻车的五个细节5.1 角色移动卡顿到完全不像游戏现象按键后角色大概半秒才走一格松开按键还继续走。原因Sleep(50) 放到了角色移动的逻辑后面且用了阻塞输入 _getch() 当主循环。每个按键都要等 Sleep 执行完而且 _getch() 会等用户按键主循环被卡死在等待上。解决把输入改成 if(kbhit()) 组合Sleep 移到循环末尾如果想流畅一点把主循环拆成「读输入、更新逻辑、绘制、Sleep(16)」四步。Sleep 值不要小于 10 毫秒否则画面闪得厉害。5.2 炸弹一放就炸或者永远不炸现象按空格瞬间地图上生成火焰有时炸弹放置后半小时不爆。原因炸弹的 fuseTimer 初始单位用的是毫秒但 updateBombs 每帧递减 1。如果初始值写的是 300 但每帧减 1那 300 帧才炸一次如果初始值是 3 却按每秒 50 帧递减那就一放就炸。单位的错位是大多数人第一次接触炸弹游戏的通病。解决全程序统一以「帧」为计时单位初始化 fuseTimer 150表示 150 帧后爆炸配合 Sleep(50) 就是 7.5 秒。调整爆炸时间改 Sleep 值或者改初始值选一个方式固定下来别两处一起改。5.3 角色穿墙或者卡进墙里现象按住方向键能穿墙走或者走到墙边缘纹丝不动再按其他键没反应。原因移动函数里的碰撞检测是在更新坐标之后才做的等于「先走过去发现是墙再退回来」但回退的是临时变量地图上的实际坐标已经变了而边缘的坐标若越界没处理会导致角色卡死。解决统一用旧坐标 目标坐标判断。目标格子可行才提交新坐标否则坐标保持不动。尤其注意地图二维数组的行列对应关系i 是行对应 yj 是列对应 x写反了地图看上去就是转置的。5.4 火焰穿透软墙或距离和炸弹威力对不上现象炸弹放下去火焰连着穿过 3 面软墙烧到最外面或者明明 power 是 2爆炸只烧了一格。原因爆炸蔓延逻辑里软墙分支没有加 break循环继续往外扩散另一个原因是爆炸以像素步长递增而不是按格子步长导致半径实际距离和视觉不匹配。解决在 explodeBomb 的软墙分支将格子置空后直接 break。火焰生成时要判定目标格子当前状态空地变火焰软墙炸掉变火焰硬墙停炸弹变火焰且连锁引爆。火焰自身不要参与二次扩散否则会递归蔓延出奇怪的形状。5.5 VSCode 编译通过但运行就报 access violation 类错误现象双击编译生成的 exe 弹出 C0000005 相关报错杀毒软件也弹拦截提示。原因这个项目用到了 conio.h 和 Windows.h 里的控制台 API直接在双击运行时可能没用对控制台句柄但多数情况是数组越界修改到了不可写内存基本都发生在地图数组越界写操作比如把 nx 和 ny 直接当下标时行列写反导致访问超界。解决图上所有数组访问前统一加边界判断。控制台版本的错误通常会在退出时暴露因为越界写破坏了栈数据。如果有 Visual Studio也可以在 Debug 下观察「调用堆栈」找到越界那次写发生在哪个函数。C/C 游戏源码出这类错九成是数组下标问题不是编译器问题。6. 进阶玩法把单人炸弹人改成双人局并顺手验证爆炸矩阵如果想把这份资源的价值吃透不建议停留在抄一遍跑一遍继续往下改造一两次才会真正把它变成自己的东西。我最常推荐的改造是从单人改成双人乱斗地图同一张两个玩家分别用 WASD 和方向键移动空格和回车放炸弹谁被炸到先输一局。这个改造能逼你把 Player 抽成两个独立对象也自然复习了类与对象章节的内容。#include vector struct PlayerInput { char up, down, left, right, bomb; }; // P1 用 WASDP2 用方向键互不冲突 PlayerInput p1 {w, s, a, d, }; PlayerInput p2 {72, 80, 75, 77, \r};方向键在控制台读取时第一个字节是 224所以玩家 2 的 up/down 不能直接跟字符比需要先读两个字节。做双人版时建议把 input 读取封装成一个 PollKey() 函数返回统一的方向枚举这样玩家 1 的 WASD 和玩家 2 的方向键在游戏逻辑层不再区别对待。改造完成后再回去看源码里单人移动的 switch你会很自然地认同「不该把读取和逻辑混在一起」这个设计原则。另外一个有价值的练习是给爆炸算法写个小验证把地图输出成一个 0/1 矩阵1 表示被火焰覆盖然后手动构造一张已知地图断言某个坐标的火焰是否存在。我一般会在改爆炸半径后跑一遍这个自检因为肉眼扫控制台根本没法确认每个角落的扩散边界。用断言 assert 或者打印结果对比都可以核心是让地图初始状态可预期。最后说个习惯。以前我拿到这类源码第一反应都是赶紧编译跑起来看效果跑完才发现自己什么都没学到。后来我强制自己切换流程先打开源文件按函数名列个清单搞清楚每个函数负责什么再去找读代码时最费解的那一段最后才动手跑。这个顺序用久了至少能保证别把时间浪费在反复试错上。希望帮到你。本文还有配套的精品资源点击获取