ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

C++飞机大战源码调试与扩展:从编译到跨平台工程

C++飞机大战源码调试与扩展:从编译到跨平台工程 简介这份C大作业飞机大战源码包面向高校学生与C初学者帮助读者通过一个完整可运行的2D游戏项目理解面向对象编程与Qt框架的实际应用。压缩包共78个文件约54.78MB以35个png与5个jpg图片、2个wav音频构成游戏素材12个cpp与10个h文件承载游戏逻辑、碰撞检测、动画与界面模块另含ui、qrc、pro等Qt工程配置及编译产物目录结构清晰便于按模块阅读。项目基于Qt 5.12.0与MSVC2017 32位环境构建README提供编译运行说明源码中可学习类的封装、继承与多态、QGraphicsView图形视图框架、信号槽机制以及游戏循环与资源加载等关键知识点。目前已有72人学习下载适合作为课程设计参考、Qt入门练手或二次开发基础通过运行与修改源码可切实提升调试与项目组织能力。1. 飞机大战源码到手之后先别急着编译搞清楚它到底能给你什么很多人拿到「C大作业飞机大战源码.zip」的第一反应是双击解压、找 .sln 文件、按 F5然后对着满屏报错发呆。我见过太多这样的情况压缩包里躺着十几个 .cpp 和 .h资源目录里一堆 BMP 和 WAVREADME 只有两行字连用的什么图形库都没写清楚。你以为是代码质量不行其实是你还没搞清楚这份源码的定位——它是一个教学性质的 C 大作业项目不是商业游戏引擎它的价值在于让你看懂一个完整的小型游戏循环是怎么搭起来的而不是让你直接拿去上线。飞机大战这个题材在 C 课程设计里出现频率极高原因很简单它覆盖了面向对象编程的核心知识点类与对象、继承、多态、封装同时涉及游戏开发的基础概念游戏循环、碰撞检测、帧率控制、资源管理代码量适中一个人两三周能写完。但正因为写的人多质量参差不齐你拿到的这份源码可能是学长传下来的也可能是网上随便找的里面的坑得你自己填。这篇文章要解决的问题很具体你手里有一份飞机大战源码你想把它跑起来、看懂它、改出自己想要的效果甚至基于它扩展成一个像样的作品。我会从环境配置讲到代码结构从编译报错讲到玩法扩展把每一步的参数和踩坑点都写清楚。适合正在做 C 大作业的学生也适合想通过小游戏入门 C 项目开发的初学者。如果你已经能熟练用 C 写图形程序这篇文章可能对你偏基础但里面的调试技巧和扩展思路仍然有参考价值。2. 把源码跑起来环境配置与第一次编译2.1 先判断这份源码用的什么图形库拿到源码后不要急着打开 IDE先做一件事用文本编辑器打开主文件通常是 main.cpp 或 game.cpp看开头几行 include 了什么。这一步决定了你后面要装什么环境。常见的飞机大战源码会用以下几种图形库之一图形库典型 include特点适合场景EasyX#include graphics.h仅 Windows中文文档多上手快课程设计、初学者SDL2#include SDL.h跨平台C 风格 API资料丰富想学正规游戏开发SFML#include SFML/Graphics.hpp跨平台C 风格面向对象想用现代 C 写游戏Qt#include QWidget重量级适合带 UI 的应用大作业要求带界面Win32 API#include windows.h原生代码量大不推荐新手老师指定要求如果你打开文件看到的是graphics.h和initgraph那基本可以确定是 EasyX。这是国内课程设计最常用的方案因为它足够简单安装也方便。但要注意EasyX 只支持 Windows Visual Studio 的组合如果你用的是 VS Code 或者 CLion配置起来会麻烦很多。提示如果源码里同时出现了graphics.h和SDL.h说明作者可能做了多平台适配或者是从不同项目拼凑的这种情况要格外小心很可能编译不过。2.2 Visual Studio 配置 EasyX 的完整步骤假设你确认了源码用的是 EasyX下面是从零配置的流程。我以 Visual Studio 2022 社区版为例其他版本操作类似。第一步确认你已经安装了「使用 C 的桌面开发」工作负载。打开 Visual Studio Installer点「修改」勾选这个工作负载确保右侧的「MSVC v143 生成工具」和「Windows 10/11 SDK」都打上了勾。这一步很多人会漏掉导致后面找不到windows.h。第二步下载 EasyX 库。去 EasyX 官网easyx.cn下载最新版它是一个安装程序运行后会检测你电脑上安装的 Visual Studio 版本自动把头文件和库文件复制到对应目录。安装完成后你可以在 VS 的安装目录下找到EasyX\include\graphics.h和EasyX\lib\VC\amd64\EasyXa.lib这样的文件。第三步打开源码的 .sln 文件。如果没有 .sln就新建一个空项目把所有 .cpp 和 .h 文件添加进去。然后右键项目 → 属性 → C/C → 常规 → 附加包含目录添加 EasyX 的 include 路径。再点链接器 → 常规 → 附加库目录添加 EasyX 的 lib 路径。最后在链接器 → 输入 → 附加依赖项里加上EasyXa.libDebug x64或EasyXw.libRelease x64。// 验证 EasyX 是否配置成功的测试代码 #include graphics.h #include conio.h int main() { // 初始化一个 640x480 的窗口 initgraph(640, 480); // 设置背景色为黑色 setbkcolor(BLACK); cleardevice(); // 画一个白色圆圈圆心 (320, 240)半径 50 setlinecolor(WHITE); circle(320, 240, 50); // 等待按键防止窗口一闪而过 _getch(); // 关闭图形窗口 closegraph(); return 0; }这段代码的逻辑很直白initgraph创建窗口cleardevice用背景色清屏circle画圆_getch暂停程序。如果你能编译运行并看到一个带白圈的黑色窗口说明 EasyX 环境没问题。参数说明initgraph(width, height)的两个参数是窗口的像素宽高常见飞机大战用 480x640 或 640x480取决于横屏还是竖屏。2.3 编译报错的三种常见类型和处理方法即使环境配好了第一次编译大概率还是会报错。我把常见错误归成三类你对照着排查。第一类是「找不到头文件」报错信息类似cannot open source file graphics.h。这说明附加包含目录没设对或者 EasyX 根本没装上。解决方法是回到 2.2 的第二步重新检查路径注意路径里不要有中文和空格。第二类是「无法解析的外部符号」报错信息里会出现_initgraph、_closegraph这样的函数名。这是链接阶段找不到库文件说明附加依赖项没写对或者库文件的位数和项目平台不匹配。比如你的项目是 x64但链接的是 32 位的库就会报这个错。检查项目属性 → 链接器 → 高级 → 目标计算机确保是 x64。第三类是「C4996 错误」提示某个函数不安全比如fopen、strcpy。这是 Visual Studio 的安全检查机制不是代码本身的问题。最简单的解决办法是在文件最开头加一行#define _CRT_SECURE_NO_WARNINGS或者在项目属性 → C/C → 预处理器 → 预处理器定义里加上这个宏。// 在 main.cpp 最开头加上这一行解决大部分 C4996 报错 #define _CRT_SECURE_NO_WARNINGS #include graphics.h // ... 后面的代码不变这个宏的作用是告诉编译器跳过安全警告对于课程设计级别的项目来说完全够用。但如果你以后写正式项目建议还是用fopen_s、strcpy_s这些安全版本而不是简单粗暴地屏蔽警告。3. 读懂代码结构飞机大战的类设计与游戏循环3.1 一份典型源码的文件组织方式飞机大战源码的文件组织通常有两种风格一种是「一个文件走天下」所有代码都塞在 main.cpp 里用几个 struct 或 class 凑合另一种是「按职责拆分」每个游戏对象一个类每个类一对 .h/.cpp。你拿到的源码大概率是前者因为课程设计赶工期的时候没人有心思做工程化拆分。先看一个典型的单文件结构长什么样// 典型的单文件飞机大战结构简化示意 #include graphics.h #include vector #include ctime // 全局常量 const int WIDTH 480; const int HEIGHT 640; // 玩家飞机 struct Player { int x, y; int speed; bool alive; }; // 子弹 struct Bullet { int x, y; int speed; bool active; }; // 敌机 struct Enemy { int x, y; int speed; bool alive; }; // 全局变量 Player player; std::vectorBullet bullets; std::vectorEnemy enemies; // 初始化函数 void initGame() { player.x WIDTH / 2; player.y HEIGHT - 80; player.speed 5; player.alive true; } // 游戏主循环 void gameLoop() { while (player.alive) { // 处理输入 // 更新逻辑 // 渲染画面 // 控制帧率 } } int main() { initgraph(WIDTH, HEIGHT); initGame(); gameLoop(); closegraph(); return 0; }这种结构的优点是直观所有东西都在一个文件里改起来方便。缺点是随着功能增加文件会膨胀到上千行找代码靠 CtrlF改一处可能影响另一处。如果你只是想跑通交作业这种结构够用但如果你想把它改成一个像样的项目建议做一次拆分。3.2 游戏循环的三个核心阶段不管源码怎么组织游戏循环的逻辑都是三段式输入处理、状态更新、画面渲染。这三段的顺序不能乱否则会出现「按键没反应」或者「画面撕裂」的问题。输入处理阶段负责读取键盘状态。EasyX 提供了两种方式一种是GetAsyncKeyState可以检测按键是否按下适合做持续移动另一种是_getch会阻塞程序等待按键适合做菜单选择。飞机大战里移动飞机要用前者。// 输入处理检测方向键和空格键 void handleInput() { // 检测左方向键注意 GetAsyncKeyState 返回值的最高位为 1 表示按下 if (GetAsyncKeyState(VK_LEFT) 0x8000) { player.x - player.speed; } if (GetAsyncKeyState(VK_RIGHT) 0x8000) { player.x player.speed; } if (GetAsyncKeyState(VK_UP) 0x8000) { player.y - player.speed; } if (GetAsyncKeyState(VK_DOWN) 0x8000) { player.y player.speed; } // 检测空格键发射子弹 if (GetAsyncKeyState(VK_SPACE) 0x8000) { // 发射逻辑后面会讲 } // 边界限制防止飞机飞出窗口 if (player.x 0) player.x 0; if (player.x WIDTH - 40) player.x WIDTH - 40; if (player.y 0) player.y 0; if (player.y HEIGHT - 40) player.y HEIGHT - 40; }这里的关键参数是0x8000它是一个位掩码用来提取GetAsyncKeyState返回值的最高位。如果只写if (GetAsyncKeyState(VK_LEFT))在某些情况下会误判因为返回值的最低位表示「上次调用后是否按过」而不是「当前是否按下」。这个坑我踩过当时调试了半天才发现飞机偶尔会自己动。状态更新阶段负责移动子弹、移动敌机、检测碰撞、生成新敌人。这部分逻辑最复杂也最容易出 bug。碰撞检测通常用矩形相交判断简单但有效。// 矩形碰撞检测判断两个矩形是否相交 bool isCollide(int x1, int y1, int w1, int h1, int x2, int y2, int w2, int h2) { // 两个矩形不相交的条件一个在另一个的左边/右边/上边/下边 if (x1 w1 x2) return false; if (x2 w2 x1) return false; if (y1 h1 y2) return false; if (y2 h2 y1) return false; return true; }这个函数的参数是两组 (x, y, width, height)分别代表两个矩形的左上角坐标和宽高。逻辑是只要有一个方向完全错开就不碰撞四个方向都没错开就碰撞。注意这里的坐标是左上角不是中心点如果你的源码用的是中心点坐标需要先转换。渲染阶段负责把内存里的状态画到屏幕上。EasyX 的渲染方式是「双缓冲」先用BeginBatchDraw开启批量绘制画完所有东西后用FlushBatchDraw一次性刷新最后用EndBatchDraw结束。如果不这样做画面会闪烁得厉害。// 渲染阶段双缓冲绘制 void render() { BeginBatchDraw(); cleardevice(); // 清屏 // 画玩家飞机用矩形代替图片实际项目用 putimage setfillcolor(BLUE); fillrectangle(player.x, player.y, player.x 40, player.y 40); // 画子弹 setfillcolor(YELLOW); for (auto b : bullets) { if (b.active) { fillrectangle(b.x, b.y, b.x 4, b.y 12); } } // 画敌机 setfillcolor(RED); for (auto e : enemies) { if (e.alive) { fillrectangle(e.x, e.y, e.x 36, e.y 36); } } FlushBatchDraw(); EndBatchDraw(); }参数说明fillrectangle(left, top, right, bottom)的四个参数是矩形的左上角和右下角坐标。setfillcolor设置填充色颜色常量如BLUE、YELLOW、RED是 EasyX 预定义的。实际项目中你会用putimage加载飞机图片但原理一样只是把填充矩形换成贴图。3.3 帧率控制为什么你的飞机飞得太快或太慢游戏循环如果不加帧率控制会以 CPU 的最大速度运行导致飞机瞬间飞出屏幕。常见的控制方式是Sleep函数每帧暂停一段时间。// 帧率控制目标 60 帧每秒 const int FRAME_DELAY 16; // 1000ms / 60 ≈ 16.67ms void gameLoop() { while (player.alive) { DWORD frameStart GetTickCount(); // 记录帧开始时间 handleInput(); update(); render(); // 计算本帧耗时补足到 16ms DWORD frameTime GetTickCount() - frameStart; if (frameTime FRAME_DELAY) { Sleep(FRAME_DELAY - frameTime); } } }这里用GetTickCount记录帧开始时间渲染完后计算耗时如果不到 16ms 就补睡差值。这样做的目的是让每帧的间隔尽量稳定在 16ms 左右对应约 60 帧每秒。如果你直接Sleep(16)不考虑渲染本身的时间实际帧率会低于 60因为渲染也要花时间。注意GetTickCount的精度是毫秒级对于 60 帧的游戏够用但如果你要做 144 帧的高刷游戏需要用QueryPerformanceCounter这种微秒级计时器。课程设计用GetTickCount足够了。4. 避坑与排查飞机大战源码最常见的五个翻车点4.1 现象编译通过但运行闪退没有任何报错原因最常见的是资源文件路径不对。源码里用loadimage加载图片时路径可能是相对路径比如res/plane.png但你的工作目录和源码作者的不一样。Visual Studio 默认的工作目录是项目目录不是 exe 所在目录所以图片找不到就会加载失败后续绘制时访问空指针导致闪退。解决把资源文件夹复制到项目目录下或者在项目属性 → 调试 → 工作目录里设置成$(ProjectDir)。更稳妥的做法是用绝对路径但这样换电脑就失效了。我一般会在代码里加一个检查// 加载图片并检查是否成功 IMAGE img; if (loadimage(img, _T(res/plane.png)) ! 0) { // 加载失败弹出提示并退出 MessageBox(NULL, _T(图片加载失败请检查 res 目录), _T(错误), MB_OK); exit(-1); }loadimage的返回值是 0 表示成功非 0 表示失败。加上这个检查后至少你能知道是资源问题而不是对着闪退干瞪眼。4.2 现象飞机移动卡顿按键响应延迟明显原因可能是帧率控制没做好也可能是输入检测用了阻塞式函数。如果你在游戏循环里用了_getch()程序会停在那里等按键整个循环就卡住了。另一个可能是Sleep时间设得太长比如设了 50ms帧率只有 20看起来就卡。解决确认输入用的是GetAsyncKeyState而不是_getch。检查Sleep的参数60 帧对应 16ms30 帧对应 33ms。如果你想要更流畅的效果可以不做帧率限制让循环全速跑但这样 CPU 占用会很高。折中方案是用Sleep(1)让出 CPU 时间片同时用计时器控制逻辑更新频率。4.3 现象子弹和敌机碰撞检测不准确有时候穿过去了原因这是典型的「高速物体穿透」问题。如果子弹速度很快比如每帧移动 20 像素而敌机高度只有 36 像素子弹可能在两帧之间从敌机上方直接跳到下方碰撞检测就漏掉了。解决有两种方案。第一种是减小子弹速度但这样游戏手感会变差。第二种是做「连续碰撞检测」也就是在子弹的移动路径上采样多个点逐个检测。简单做法是每帧把子弹移动分成若干小步// 子弹移动时分步检测碰撞 void moveBullet(Bullet b) { int steps b.speed / 5 1; // 每步最多移动 5 像素 int stepY b.speed / steps; for (int i 0; i steps; i) { b.y - stepY; // 每移动一步就检测一次碰撞 for (auto e : enemies) { if (e.alive isCollide(b.x, b.y, 4, 12, e.x, e.y, 36, 36)) { b.active false; e.alive false; // 加分逻辑... return; } } } }这个方案的核心思想是把一帧的大位移拆成多个小位移每个小位移后都做一次碰撞检测。代价是计算量增加但对于飞机大战这种规模的项目性能完全不是问题。4.4 现象敌机生成位置重叠一堆飞机叠在一起原因随机数生成有问题。很多源码用rand() % WIDTH来生成敌机的 x 坐标但rand()没有播种每次运行结果一样。更关键的是如果生成间隔太短或者没有检查新敌机和已有敌机的位置关系就会重叠。解决首先用srand(time(NULL))播种让每次运行的随机序列不同。其次在生成新敌机时检查是否和现有敌机太近// 生成敌机避免和现有敌机重叠 void spawnEnemy() { int maxAttempts 10; // 最多尝试 10 次 for (int i 0; i maxAttempts; i) { int x rand() % (WIDTH - 36); int y -36; // 从屏幕上方进入 bool overlap false; for (auto e : enemies) { if (e.alive abs(e.x - x) 40 abs(e.y - y) 40) { overlap true; break; } } if (!overlap) { Enemy e; e.x x; e.y y; e.speed 2 rand() % 3; // 速度 2~4 e.alive true; enemies.push_back(e); return; } } // 尝试 10 次都重叠就放弃这次生成 }参数说明rand() % (WIDTH - 36)保证敌机 x 坐标在窗口内36是敌机宽度。abs(e.x - x) 40判断两个敌机的水平距离是否小于 40 像素如果是就认为太近。maxAttempts防止死循环如果尝试多次都找不到合适位置就跳过这次生成。4.5 现象程序运行一段时间后越来越卡最后卡死原因内存泄漏或者容器无限增长。最常见的是子弹和敌机被标记为active false或alive false后没有从 vector 里删除导致 vector 越来越大每帧遍历的时间越来越长。解决定期清理不活跃的对象。可以在每帧更新后加一个清理步骤// 清理不活跃的子弹和敌机 void cleanup() { // 移除所有 active 为 false 的子弹 bullets.erase( std::remove_if(bullets.begin(), bullets.end(), [](const Bullet b) { return !b.active; }), bullets.end() ); // 移除所有 alive 为 false 的敌机 enemies.erase( std::remove_if(enemies.begin(), enemies.end(), [](const Enemy e) { return !e.alive; }), enemies.end() ); }这里用了std::remove_if配合 lambda 表达式把不满足条件的元素移到末尾然后erase一次性删除。这是 C 里删除 vector 元素的标准写法比手动遍历删除效率高也不容易出错。注意remove_if本身不删除元素只是重新排列真正删除的是erase。5. 从能跑到能看用随机数和状态机把玩法做扎实5.1 用 C 随机数让敌机行为不那么「傻」原始源码里的敌机大概率是直线往下飞玩两分钟就腻了。要让敌机有点「智能」可以用随机数控制它的横向移动和发射子弹的行为。C11 引入了random库比rand()更灵活但很多课程设计还在用老式的rand()。两种方式我都写一下你根据源码的风格选。// 方式一传统 rand()简单够用 #include cstdlib #include ctime void initRandom() { srand((unsigned)time(NULL)); // 播种只需调用一次 } int randomInt(int min, int max) { return min rand() % (max - min 1); } // 方式二C11 random更均匀推荐 #include random std::mt19937 gen(std::random_device{}()); // 梅森旋转引擎 int randomInt(int min, int max) { std::uniform_int_distribution dist(min, max); return dist(gen); }方式一的rand()在 Windows 上周期较短而且取模运算会导致分布不均匀低位比高位随机性差。方式二用梅森旋转算法周期长分布均匀是现代 C 的推荐做法。如果你只是做课程设计方式一够用如果你想写得更专业用方式二。用随机数控制敌机行为的例子// 敌机更新逻辑随机左右移动 随机发射子弹 void updateEnemy(Enemy e, float deltaTime) { // 每帧有 2% 的概率改变移动方向 if (randomInt(1, 100) 2) { e.horizontalSpeed randomInt(-2, 2); } e.x e.horizontalSpeed; // 边界反弹 if (e.x 0 || e.x WIDTH - 36) { e.horizontalSpeed -e.horizontalSpeed; } e.y e.speed; // 每帧有 0.5% 的概率发射子弹 if (randomInt(1, 1000) 5) { // 生成敌方子弹... } }参数说明randomInt(1, 100) 2表示 2% 的概率这个值可以根据难度调整。horizontalSpeed的范围是 -2 到 2负数向左正数向右0 表示不横向移动。randomInt(1, 1000) 5是 0.5% 的概率控制敌机射击频率太高会让玩家没法躲。5.2 用状态机管理游戏流程很多源码的游戏流程是线性的开始 → 游戏中 → 结束。但如果你想加个开始菜单、暂停功能、关卡切换就需要状态机。状态机说白了就是一个变量记录当前处于哪个状态然后在主循环里根据状态决定做什么。// 游戏状态枚举 enum GameState { STATE_MENU, // 主菜单 STATE_PLAYING, // 游戏中 STATE_PAUSED, // 暂停 STATE_GAME_OVER // 游戏结束 }; GameState currentState STATE_MENU; void gameLoop() { while (true) { switch (currentState) { case STATE_MENU: handleMenuInput(); renderMenu(); break; case STATE_PLAYING: handleInput(); update(); render(); break; case STATE_PAUSED: handlePauseInput(); renderPauseOverlay(); break; case STATE_GAME_OVER: handleGameOverInput(); renderGameOver(); break; } // 帧率控制... } }这个结构的优点是清晰每个状态的处理逻辑分开加新状态只需要加一个 case。缺点是所有状态共享同一个循环如果某个状态的处理很耗时会影响其他状态的响应。对于飞机大战来说这个缺点可以忽略。状态切换的例子在游戏中按 ESC 键进入暂停再按 ESC 回到游戏玩家生命值为 0 时进入游戏结束状态。// 在 STATE_PLAYING 的处理里检测暂停 if (GetAsyncKeyState(VK_ESCAPE) 0x8000) { currentState STATE_PAUSED; // 防止按键重复触发等待按键释放 while (GetAsyncKeyState(VK_ESCAPE) 0x8000) { Sleep(10); } }这里加了一个等待按键释放的循环目的是防止一次按下被多次检测到。如果不加这个按一下 ESC 可能会在暂停和游戏之间快速切换看起来像卡住了。5.3 用结构体数组管理多关卡配置如果你想让游戏有多个关卡不要把每关的参数硬编码在 if-else 里用结构体数组更清晰。// 关卡配置结构体 struct LevelConfig { int enemySpawnInterval; // 敌机生成间隔帧数 int enemySpeedMin; // 敌机最小速度 int enemySpeedMax; // 敌机最大速度 int enemyShootRate; // 敌机射击概率千分之几 int targetScore; // 过关所需分数 }; // 关卡配置表 LevelConfig levels[] { {60, 1, 2, 2, 100}, // 第 1 关敌机慢射击少 {45, 2, 3, 5, 250}, // 第 2 关速度提升 {30, 3, 4, 8, 500}, // 第 3 关生成更快 {20, 4, 5, 12, 1000}, // 第 4 关高压 }; int currentLevel 0; // 当前关卡索引参数说明enemySpawnInterval越小敌机生成越快enemySpeedMin和enemySpeedMax控制敌机下落速度范围enemyShootRate是千分比比如 5 表示 0.5% 的概率targetScore是过关分数。调整这些参数就能改变难度曲线不用改逻辑代码。提示关卡配置表放在全局作用域方便调整。如果你想让游戏更耐玩可以在每关结束后根据玩家表现动态调整下一关的参数而不是用固定表。6. 把源码变成自己的作品三个能写进简历的扩展方向6.1 加一个简单的粒子爆炸效果飞机被击毁时如果只是消失视觉反馈太弱。加一个粒子爆炸效果代码量不大但看起来专业很多。思路是敌机被击毁时在它的位置生成一批小粒子每个粒子有随机速度和方向每帧移动并逐渐消失。// 粒子结构体 struct Particle { float x, y; // 位置 float vx, vy; // 速度 int life; // 剩余生命帧数 COLORREF color; // 颜色 }; std::vectorParticle particles; // 生成爆炸粒子 void spawnExplosion(int x, int y) { for (int i 0; i 20; i) { Particle p; p.x (float)x; p.y (float)y; // 随机速度方向范围 -3 到 3 p.vx (randomInt(-30, 30)) / 10.0f; p.vy (randomInt(-30, 30)) / 10.0f; p.life randomInt(15, 30); // 存活 15~30 帧 p.color RGB(255, randomInt(100, 200), 0); // 橙黄色 particles.push_back(p); } } // 更新粒子 void updateParticles() { for (auto p : particles) { p.x p.vx; p.y p.vy; p.life--; } // 移除生命耗尽的粒子 particles.erase( std::remove_if(particles.begin(), particles.end(), [](const Particle p) { return p.life 0; }), particles.end() ); } // 渲染粒子 void renderParticles() { for (auto p : particles) { // 根据剩余生命调整亮度越暗表示越接近消失 int brightness p.life * 8; if (brightness 255) brightness 255; setfillcolor(RGB(brightness, brightness / 2, 0)); fillcircle((int)p.x, (int)p.y, 2); } }参数说明randomInt(-30, 30) / 10.0f生成 -3.0 到 3.0 之间的浮点速度除以 10 是为了让速度变化更细腻。p.life控制粒子存活时间15 到 30 帧大约对应 0.25 到 0.5 秒。fillcircle画实心圆半径 2 像素看起来像小火花。这个效果加进去之后每次击毁敌机都有反馈游戏的「打击感」会提升一个档次。而且代码不复杂面试的时候也可以拿出来讲。6.2 用文件存储最高分和游戏设置课程设计如果只把数据存在内存里关掉程序就没了显得不完整。加一个简单的文件读写把最高分和音量设置存下来代码量很小但让项目看起来更像一个「产品」。// 存档结构体 struct SaveData { int highScore; // 最高分 int soundVolume; // 音量 0~100 int lastLevel; // 上次玩的关卡 }; // 保存存档 void saveGame(const SaveData data) { FILE* fp fopen(save.dat, wb); if (fp NULL) { MessageBox(NULL, _T(存档失败), _T(错误), MB_OK); return; } fwrite(data, sizeof(SaveData), 1, fp); fclose(fp); } // 读取存档 SaveData loadGame() { SaveData data {0, 50, 0}; // 默认值 FILE* fp fopen(save.dat, rb); if (fp NULL) { return data; // 文件不存在返回默认值 } fread(data, sizeof(SaveData), 1, fp); fclose(fp); return data; }参数说明fopen的wb表示二进制写入rb表示二进制读取。fwrite和fread的第三个参数是元素个数这里是 1。用二进制格式的好处是读写快坏处是文件不可读调试的时候看不到内容。如果你想用文本格式可以用fprintf和fscanf但解析起来麻烦一些。注意save.dat的路径是相对路径实际保存位置取决于程序的工作目录。如果你希望存档和 exe 放在一起可以用GetModuleFileName获取 exe 路径然后拼接文件名。6.3 用 CMake 把项目变成跨平台工程如果你想让这个项目在简历上更有分量可以把它从 Visual Studio 工程改成 CMake 工程。CMake 是 C 世界的事实标准构建工具很多公司招人时会看你是否熟悉 CMake。改造的过程不复杂但能让你理解项目构建的底层逻辑。# CMakeLists.txt cmake_minimum_required(VERSION 3.15) project(PlaneWar) # 设置 C 标准 set(CMAKE_CXX_STANDARD 17) set(CMAKE_CXX_STANDARD_REQUIRED ON) # 根据平台链接不同的图形库 if(WIN32) # Windows 下用 EasyX include_directories(C:/EasyX/include) link_directories(C:/EasyX/lib/VC/x64) add_executable(PlaneWar src/main.cpp src/game.cpp src/player.cpp src/enemy.cpp ) target_link_libraries(PlaneWar EasyXa) else() # Linux/macOS 下用 SDL2 find_package(SDL2 REQUIRED) add_executable(PlaneWar src/main.cpp src/game.cpp src/player.cpp src/enemy.cpp ) target_link_libraries(PlaneWar SDL2::SDL2 SDL2::SDL2main) endif()这个 CMakeLists 的逻辑是先设置 C17 标准然后根据操作系统选择不同的图形库。Windows 用 EasyX其他平台用 SDL2。add_executable列出所有源文件target_link_libraries链接对应的库。改造过程中你会遇到一个问题EasyX 和 SDL2 的 API 完全不同源码里所有绘图函数都要改。所以更现实的做法是先抽象一层绘图接口把initgraph、putimage、fillrectangle这些调用封装成自己的函数然后在不同平台下实现不同的版本。这个工作量不小但做完之后你对「跨平台」的理解会深很多。// 绘图接口抽象层头文件 class Renderer { public: virtual void init(int width, int height) 0; virtual void clear() 0; virtual void drawRect(int x, int y, int w, int h, int color) 0; virtual void drawImage(int x, int y, const char* path) 0; virtual void present() 0; virtual ~Renderer() {} }; // EasyX 实现 class EasyXRenderer : public Renderer { public: void init(int width, int height) override { initgraph(width, height); } void clear() override { cleardevice(); } void drawRect(int x, int y, int w, int h, int color) override { setfillcolor(color); fillrectangle(x, y, x w, y h); } // ... 其他实现 };这个抽象层的意义在于游戏逻辑代码只依赖Renderer接口不依赖具体的图形库。换库的时候只需要写一个新的实现类游戏逻辑不用动。这是软件工程里「依赖倒置原则」的典型应用面试的时候能讲清楚这个比单纯说「我会 C」有说服力得多。我自己的习惯是每做完一个课程设计就花半天时间把它整理成一个能拿得出手的项目。整理的过程包括把代码拆成合理的文件结构、加注释、写 README、用 CMake 重新组织构建。这些工作不会让游戏本身变得更好玩但会让你的项目在 GitHub 上看起来更专业面试的时候也更有东西可聊。飞机大战这个题材虽然简单但如果你能把上面说的粒子效果、存档系统、跨平台构建都做出来它就不再是一个「大作业」而是一个能展示你工程能力的作品。希望帮到你。本文还有配套的精品资源点击获取
返回列表