简介一份适合C初学者的期末大作业——深海炸弹小游戏玩家操控潜艇在深海环境中发射炸弹炸毁目标涵盖碰撞检测、计分系统与简单图形界面是理解面向对象编程和游戏逻辑的完整练手项目。资源共79个文件压缩包约41.92MB包含C源码.cpp/.h、可直接运行的exe、Visual Studio工程配置.sln/.vcxproj、项目报告书docx以及流程图和运行截图等辅助文档。已有1168人浏览学习。随附的实验报告详细叙述设计思路、技术实现步骤与排错记录能帮助学习者快速把握从需求分析到编码实现的完整流程代码中潜艇、爆炸等模块划分清晰初学者可通过修改参数直接体验物理模拟与界面交互也能将整体框架作为后续课程设计的起点。全部材料组织紧凑尤其适合作为期末答辩的参考范例。1. 从“深海炸弹”到 C 期末大作业一个能拿出手的练手项目“c期末大作业深海炸弹小游戏”这标题翻成大白话就是一套能直接跑、能交差、还能让老师看到你确实会 C 的课程设计。它的玩法设定很适合刚学完语法的初学者——你控制的潜艇在一片海底网格里巡逻发现目标就投放深水炸弹炸弹延迟爆炸命中加分炸到水雷或自己就扣命难度逐关提升。这个方案的实用点在于它把 C 里最容易考到的知识点全部串了起来循环、二维数组、随机数、类与函数、文件读写而且完全不依赖图形库控制台就能跑。适合的人群是已经过了语法关但不知道从哪开始做第一个完整项目的人。如果你不想选“学生管理系统”这种写烂了的题目深海炸弹的规则自由度和代码量都正合适下文就从定规则开始一步步把它变成你自己的源码、exe 和报告书。2. 拆解项目需求先定游戏规则再定代码结构2.1 三种常见玩法与选型理由“深海炸弹”在网上能看到好几种做法我见过最多的是以下三种先对比再选型比直接写代码省事得多。第一种是扫雷变体在网格里埋“水雷”玩家翻格子数字表示附近雷数。实现最简单几十行就能跑但“投弹”“延时爆炸”的动感完全丢了玩起来和扫雷没区别老师也容易觉得是换皮作业。第二种是投弹炸目标玩家在网格里移动按方向往相邻格投弹炸弹带 fuse 延时倒计时结束才爆炸爆炸半径内命中目标才算分。延时、随机数、碰撞判定全都有用武之地控制台界面完全能表现代码量大概在 400 到 600 行正好是期末大作业的舒适区间。第三种是纵版躲避目标或水雷主动向玩家移动需要很短的键盘响应周期也就是实时刷新。控制台做实时渲染很吃力要么清屏重绘导致闪烁要么引入 Windows API 做双缓冲对初学者偏难期末前容易翻车。玩法类型预估代码量涉及核心知识点期末大作业适配度扫雷变体150~250 行二维数组、随机数、递归展开中容易被质疑换皮投弹炸目标400~600 行类、随机数、延时、碰撞判定、文件读写高规则和作业契合纵版躲避700 行以上实时刷新、双缓冲、状态机低调试成本太高我一般建议选第二种理由很实在它能让“深水炸弹延迟爆炸”这个特性变成游戏的核心机制而不是可有可无的装饰。投弹后玩家要预判目标移动炸弹要等 fuse 归零才判定这三秒的等待时间天然制造了游戏张力报告书里写“游戏机制说明”时也有内容可写。对初学者来说它不需要外部图形库Visual Studio 或 VSCode 装了 C 插件就能编译环境问题少一半。2.2 类设计的最小集合为什么不用“炫技”写法很多初学者拿到这种题第一反应是把所有逻辑塞进 main 函数几十个全局变量满天飞结果代码能跑但报告书的模块图画不出来。另一种极端是上来就用继承多态给鱼、潜艇、水雷各建一个子类最后发现它们除了分数和图标根本不需要多态。我倾向于做一个三层最小结构地图和玩家状态归 Game 类管目标是纯数据结构炸弹是带倒计时的轻量对象。代码先写成头文件声明方便后面画类图和写报告书。// game.h - 核心类型声明整套逻辑围绕这三组数据结构展开 #ifndef GAME_H #define GAME_H #include vector #include string // 目标对象鱼/潜艇/水雷共用一套字段用 type 区分 struct Target { int x, y; // 网格坐标 int type; // 0鱼 1潜艇 2水雷 int score; // 命中分值鱼5 潜艇10 水雷-10 bool alive; // 是否存活炸到后置 false }; // 炸弹对象只关心落点和剩余延时 struct Bomb { int x, y; // 落点坐标投弹瞬间的格子 int fuse; // 剩余延时单位毫秒 }; class Game { public: Game(int w, int h, int level); void initMap(); // 生成当前关卡目标并刷新地图 bool movePlayer(int dx, int dy); // 尝试移动玩家撞水雷返回 false void dropBomb(int dir); // 按方向投弹0上 1下 2左 3右 void update(int dt); // 减少炸弹 fuse归零就爆炸 void explode(int bx, int by); // 爆炸判定曼哈顿距离 1 的目标受损 bool levelFinished() const; // 本关目标是否全部清空 void saveScore(const std::string name); // 追加写入排行榜 int loadTopScore() const; // 读取历史最高分 private: int w_, h_, level_; int lives_, score_; int playerX_, playerY_; std::vectorstd::vectorint grid_; // 0空 1鱼 2潜艇 3水雷 9玩家 std::vectorTarget targets_; std::vectorBomb bombs_; }; #endif这里为什么用std::vectorstd::vectorint而不是普通二维数组因为关卡难度提升后目标数量会变用 vector 可以动态处理不用重编译就改变数组尺寸。为什么 Target 用 struct 而不是 class它只是状态数据没有行为写成 struct 减少代码量报告书里解释为“数据实体”也说得通。Bomb 单独拆出来是因为它要在 update 里被遍历、删除和 Target 混在一起会让删除逻辑变复杂。整个设计核心是“谁有状态谁拥有数据”不提前为不会用到的多态买单。2.3 报告书需要的几张表开工前先留的文档位写代码前先把两张表建好后面报告书基本是填空不需要期末前熬夜补文档。第一张是功能清单表把游戏要做的功能拆成验收项每完成一项就打勾。这样写代码时有明确目标报告书的功能需求分析也有了原材料。功能项不用写得太细按“移动、投弹、爆炸判定、计分、关卡、存档”六个模块列就行。编号功能模块验收标准完成状态F1玩家移动WASD 控制潜艇在网格内移动不越界开发时逐项打勾F2投弹与延时空格投弹炸弹 fuse 递减归零爆炸—F3爆炸判定曼哈顿距离 1 以内目标扣血并加分—F4关卡推进击杀全部目标进入下一关目标数递增—F5排行榜存档文件写入/读取最高分—第二张是测试用例表写代码前把典型输入和预期结果列好。比如“玩家移动到边界再按 d”“投弹后 1.2 秒内目标不消失”“炸到水雷扣一条命并扣分”这些用例在开发期每轮编译后跑一遍能省下大量排错时间。很多同学排名倒数第一周才随手截几张运行图当测试记录老师看得出来。这两张表的模板放进报告书的“需求分析”和“测试”章节只需替换数据不用重新写。3. 用 C 在本地跑通最小游戏核心代码与参数设置3.1 环境准备Visual Studio 与 VSCode 的取舍写代码前先解决环境初学者最容易在这里卡住半天。学校机房多数装的是 Visual Studio那就在 VS 里建“空项目”把 .cpp 和 .h 拖进去直接编译如果自己电脑用的是 VSCode需要先把 C 编译环境配好。vscode 配置 c/c 环境的常规做法是安装 C/C 扩展和编译器Windows 上装 MinGW-w64然后配置 tasks.json 和 launch.json 两个文件分别负责编译命令和调试器路径。我自己的习惯是只要目标是“课程设计能交差”优先用 Visual Studio省掉 tasks.json 的折腾。VSCode 适合你想顺便把调试工具链搞清楚的情况但期末时间紧不要在环境上恋战。两个环境对代码本身没有影响下面的代码在两个环境里都能编译。项目文件结构建议保持扁平方便打包和写报告deep_bomb/ ├── main.cpp // 主循环、菜单、输入分发 ├── game.cpp // Game 类实现 ├── game.h // 类型和类声明 ├── score.dat // 运行时自动生成的排行榜文件 └── 项目报告书.docx // 最后提交的文档main.cpp 只做三件事调用菜单、循环调用输入读取和 Game::update、刷新屏幕。游戏逻辑不要堆在 main 里否则报告书的“模块划分”一节没法写。3.2 地图与目标生成用 c随机数控制刷怪密度海底网格的地图是 12×12 的二维数组玩家出生在左下角。目标生成的密度决定了游戏难度这里我用的参数是“目标数量 3 关卡 × 2”第一关 5 个目标第五关 13 个既能感到压力又不会让地图塞满。随机数用 C11 的std::mt19937引擎比老的rand()分布更均匀而且报告书里能写“采用了梅森旋转算法”这是加分项。// game.cpp - 生成当前关卡的地图和目标 #include random #include game.h std::mt19937 rng(std::random_device{}()); void Game::initMap() { // 网格全部置空然后重新生成目标和玩家位置 grid_.assign(h_, std::vectorint(w_, 0)); // 坐标范围限制在 1 ~ w-2 / 1 ~ h-2最外圈留着画边框 std::uniform_int_distributionint xGen(1, w_ - 2); std::uniform_int_distributionint yGen(1, h_ - 2); int targetCount 3 level_ * 2; targets_.clear(); for (int i 0; i targetCount; i) { Target t; t.x xGen(rng); t.y yGen(rng); // 如果新生成的坐标已经被占用回退一次循环重新抽 if (grid_[t.y][t.x] ! 0) { --i; continue; } // 三种目标各占一部分水雷数量最少 t.type i % 3; t.score (t.type 0) ? 5 : (t.type 1 ? 10 : -10); t.alive true; targets_.push_back(t); grid_[t.y][t.x] t.type 1; // 1鱼 2潜艇 3水雷 } // 玩家出生在左下角 playerX_ 1; playerY_ h_ - 2; grid_[playerY_][playerX_] 9; }这段代码有三个参数值得记住目标数量公式3 level_ * 2、随机数引擎mt19937、坐标边界1 ~ w-2。坐标边界是很多初学者忽略的细节如果目标生成在 0 或 w-1 的位置屏幕边框会被目标覆盖显示混乱。随机数引擎必须是全局对象如果在initMap内部每次重新构造种子就会重复结果是每关地图一模一样——这是 c随机数最经典的翻车现场。3.3 投弹与爆炸判定延时、半径与计分的调参逻辑投弹是整个游戏的核心动作直接输入方向键会跟移动冲突所以我的方案是方向键移动、空格投弹投弹方向取“玩家当前面对的方向”。实现上用一个静态数组把方向映射到位移量投弹瞬间计算落点坐标把炸弹放进炸弹数组。// game.cpp - 投弹与爆炸判定 const int dx[4] { 0, 0, -1, 1 }; const int dy[4] { -1, 1, 0, 0 }; void Game::dropBomb(int dir) { Bomb b; b.x playerX_ dx[dir]; b.y playerY_ dy[dir]; // 延时随关卡缩短第一关 1200ms每关减 80ms最低 560ms b.fuse 1200 - level_ * 80; if (b.fuse 560) b.fuse 560; bombs_.push_back(b); } void Game::update(int dt) { // dt 为主循环每次 Sleep 的毫秒数主循环固定传 50 for (auto it bombs_.begin(); it ! bombs_.end();) { it-fuse - dt; if (it-fuse 0) { explode(it-x, it-y); it bombs_.erase(it); // 注意 erase 返回下一个迭代器 } else { it; } } } void Game::explode(int bx, int by) { // 爆炸半径取曼哈顿距离 1形成十字形爆炸范围 for (auto t : targets_) { if (!t.alive) continue; if (abs(t.x - bx) abs(t.y - by) 1) { t.alive false; grid_[t.y][t.x] 0; score_ t.score; if (t.score 0) lives_--; // 炸到水雷扣血 } } // 玩家站在爆炸范围内同样扣血 if (abs(playerX_ - bx) abs(playerY_ - by) 1) { lives_--; } }爆炸半径我特意用曼哈顿距离而不是直线距离。曼哈顿距离计算快、判定的格子形状接近十字在网格游戏里视觉上更清晰如果用欧氏距离半径 1其实也是同样的四个邻居格直径再大就会出现斜对角也算命中的情况玩家会觉得“明明隔着一格还被炸到”。fuse 的数值是试玩调出来的1200 毫秒在终端界面上是“深呼吸等爆炸”的长度低于 560 毫秒就变成秒爆失去了投弹的预判感。主循环里Sleep(50)update 时 dt 固定传 50fuse 就会均匀递减。3.4 存档与排行榜文件读写的最小实现排行榜是期末大作业里性价比很高的功能十几行代码就能让报告书多一节。我采用的方案是每次得分写入文本文件读取时逐行解析找最大值。写文件用追加模式这样历史记录不丢失读文件时如果文件不存在或内容为空返回 0 而不是崩溃。// game.cpp - 排行榜读写 #include fstream #include sstream #include algorithm void Game::saveScore(const std::string name) { std::ofstream fout(score.dat, std::ios::app); if (!fout.is_open()) { // 磁盘满或权限不足时静默失败游戏不中断 return; } fout name score_ std::endl; } int Game::loadTopScore() const { std::ifstream fin(score.dat); int best 0; std::string line; while (std::getline(fin, line)) { std::istringstream iss(line); std::string name; int s 0; iss name s; best std::max(best, s); } return best; }这里要提醒一个新手常踩的坑相对路径score.dat是以“程序当前工作目录”为基准的。在 Visual Studio 里按 F5 运行工作目录是项目目录在文件管理器里双击 exe工作目录是 exe 所在目录。同一个程序两种运行方式读到的文件位置不同排查时容易怀疑人生。解决办法后面第 5 章单独说这里先知道现象即可。4. 编译出 exe 文件与写项目报告书交付物怎么凑齐4.1 从源码到 exeRelease 配置与运行库依赖期末提交的 exe 要让老师能直接双击运行所以必须用 Release 配置编译而不是 Debug。Debug 版本体积大、依赖调试运行库换到没有装 Visual Studio 的机器上会报“缺少 VCRUNTIME140D.dll”。我在网上看到很多同学在群里问为什么 exe 双击没反应十有八九是 Debug 版直接拷出去了。用 Visual Studio 的话操作顺序是顶部配置管理器从 Debug 切换到 Release平台选 x64 或 x86 都行但要和报告书一致项目属性 - C/C - 代码生成 - 运行库改成“多线程 /MT”。这一步非常关键它把 C 运行库静态链接进 exe目标机器不需要额外安装 microsoft visual c redistributable。如果不改Release 版虽然能跑但换到别的电脑依然可能缺运行库。命令行玩家可以用 g 直接编译同样要加静态链接和优化参数# 编译并生成 release 版可执行文件 g -stdc17 -O2 -static -o deep_bomb.exe main.cpp game.cpp-stdc17指定语言标准-O2开启优化-static让可执行文件尽量静态链接运行时。生成后马上做一件事把 exe 复制到一个不包含 C 环境的目录比如桌面新建的测试文件夹双击运行确认不报错。这一步叫“换机测试”是踩坑血泪经验换来的一定要做。4.2 项目报告书结构七段式写法与每段需要的数据报告书是期末作业的另一半分数很多同学代码写完却不知道报告写什么。我的建议是七段式每段都有现成的素材来源章节写什么素材来源需求分析游戏玩法、功能清单表第 2 章建好的功能清单总体设计模块图、类图、流程图game.h 里的类声明详细实现核心代码片段与参数解释随机数、投弹、存档三个函数测试测试用例表、运行截图开发期打的勾和缓存截图运行说明编译步骤、操作按键Release 编译命令总结与收获遇到的问题、解决过程第 5 章的踩坑记录参考文献教材、C 标准文档条目教材和网上教程流程图不需要画得漂亮用简单的方框和箭头说明主循环即可初始化 - 显示地图 - 读输入 - 移动/投弹 - 更新炸弹 - 判定爆炸 - 检查关卡状态。画图工具用 Visio 或 Draw.io导出图片贴进文档。类图直接根据 game.h 的结构画三个类/结构体和它们的成员变量、方法对齐排布就好不需要用 UML 工具反向生成。4.3 演示与验收给老师展示什么、怎么防止现场翻车期末验收往往只有三分钟演示顺序比内容更重要。我建议按“进游戏 - 击杀目标 - 展示存档”三步走。进游戏时要强调操作方式WASD 移动、空格投弹让老师知道怎么玩击杀目标时选一个视野内的鱼投弹、等待、爆炸、加分这一串动作流畅展示最后退出游戏重新打开显示排行榜读取成功。演示前留一个防翻车清单把控制台字体调大教室投影仪上小字看不清关闭中文输入法空格键在中文输入状态下可能截获提前确认 exe 在老师机器上能跑方法是提交前做一次换机测试。另外准备一个“如果目标在屏幕边缘怎么投弹”的预案这类问题现场被问的概率不低解释清楚投弹坐标计算即可。5. 从入门到翻车深海炸弹小游戏初学者常踩的 5 个坑5.1 现象双击 exe 窗口一闪而过内容完全看不到原因程序执行完 main 直接退出控制台窗口被系统关闭。这在“投弹炸目标”这类循环型游戏里反而少见更多出现在菜单或展示页面里。解决在 main 尾部加一个阻塞等待输入的语句比如std::cout 按任意键退出 std::endl; _getch();。注意不要用system(pause)它依赖系统命令在部分环境会额外打印提示且可能被安全软件拦截代码里用_getch()更干净。5.2 现象每次重开游戏地图上目标位置完全一样随机数不随机原因随机数种子没有重新初始化或者srand(time(0))写在initMap函数内部而time(0)一秒内的多次调用返回相同值。另一种可能是用了全局rand()但忘记调用srand。解决使用std::random_device为mt19937提供种子种子来源是硬件熵每次运行都不同。如果坚持用rand()只允许在 main 开头调用一次srand(time(nullptr))。报告书里提一句“采用梅森旋转算法生成随机数”会比写srand更有说服力。5.3 现象按方向键没反应或者投弹后要再按一下回车才生效原因_getch()读取方向键时会先返回 0xE0 或 0x00再返回实际按键码只判断一个字节自然没反应。投弹要回车的现象更常见于把scanf和_getch()混用的情况scanf留下的换行符滞留在输入缓冲区_getch()读到的就是残留的\n。解决要么只用字符键 WASD 和空格避免方向键的编码问题要么在读取时单独判断 0xE0 前缀。我一般建议只认字符键因为方向键在换机测试时还可能出现不同键盘布局的差异。输入相关逻辑单独封装成readInput()函数这样排错时只需要看一个地方。5.4 现象在 VS 里运行能存档双击 exe 却读不到排行榜存档文件不知去向原因相对路径score.dat是相对“当前工作目录”的。VS 调试时工作目录是项目目录双击 exe 时工作目录是 exe 所在目录两种运行方式读写的位置不同看起来就像存档凭空消失。解决最简单的方案是统一以 exe 所在目录为基准在程序启动时用GetModuleFileName获取 exe 完整路径再拼接score.dat。代价是代码里要多写一个辅助函数但换机测试时不会再出问题。如果不想写 Windows API另一个方案是把存档路径写死在报告书的运行说明里让老师知道“存档文件生成在 exe 旁边”也算交代清楚。5.5 现象写报告书时没内容可写只能临时截几张运行图原因开发时没有留痕习惯代码写完再看函数名和写的时候为什么要那么设计已经记不清了。解决开发过程中每完成一个小功能就打开截图工具存一张运行界面每次修 bug把“现象 - 原因 - 解决”三行写到项目根目录的 NOTES.md 里。报告书的测试和总结两章直接粘贴 NOTES.md 再润色正好比临时编内容快得多内容也更真实可信。6. 把自己的项目做“厚”三个进阶改造与一个验收清单代码跑通只是及格线想拿高分要加点别人没有的东西。我这里说三个低成本高回报的改造方向。第一个是目标动态移动。只在潜艇类型上加简单的巡逻逻辑每五帧让潜艇朝着玩家方向接近一格。这会让玩家投弹时必须预判落点游戏性立刻上了一个台阶而且实现只需在 update 函数里加一段判断。// 只让潜艇( type 1 )动鱼和水雷保持静止 if (frameCount % 5 0 t.alive t.type 1) { if (t.x ! playerX_) t.x (t.x playerX_) ? 1 : -1; // 撞到玩家判定为游戏结束这里先简化为扣血 }第二个是计分与排行榜联动。把通关时间和击杀数写入存档排行榜从“只看分数”变成“分数相同比时间”写报告书时能多讲一段算法思路。第三个改造是爆炸特效爆炸瞬间在命令行用星号画一个十字形标记停留两帧再清除代码量很小但演示效果明显。提交前对着下面的验收清单过一遍全部打勾再提交代码在 Release 配置下零警告编译exe 换机测试双击可运行存档文件自动生成且能被读取关卡推进和扣命逻辑各跑通一次报告书中的类图与源码一致。这几年带过的学生里最可惜的不是代码写不出来的而是写出来了却因为没编译成 Release、没做换机测试这种小问题被扣分。我自己的教训是第一次做深海炸弹时炸弹设成瞬间爆炸完全没有深水炸弹“投下去等几秒”的压迫感后来把 fuse 延时加回来才真正对味。这个项目值不值得做本质是看你想从中带走什么按上面的步骤走完一遍源码、exe、报告书三件套自然就齐了。希望帮到你。本文还有配套的精品资源点击获取