ARTICLE DETAIL

资讯详情

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

基于C++与EasyX的飞机大战小游戏开发实战

基于C++与EasyX的飞机大战小游戏开发实战 简介这是一份基于C开发的飞机大战小游戏完整设计源码主要面向希望入门游戏编程的C学习者以及需要参考完整项目结构进行课程设计或兴趣开发的读者。资源压缩包共71个文件其中24张PNG图片构成飞机、敌机、背景与爆炸特效等界面素材16个C源文件分别实现滚动背景、英雄机移动、子弹发射、碰撞检测、敌机生成、爆炸动画、多关卡难度升级、背景音乐与分数更新等核心逻辑同时包含Visual Studio工程文件、编译产出的exe程序及相关辅助文件压缩包整体约64.23MB。已有887人浏览学习。尤为难得的是源码按v1.0至v15.0的版本递进组织每个cpp文件对应一个独立功能阶段读者可以沿着开发主线逐版本掌握从基础界面到完整玩法的落地过程编译好的exe也可直接运行便于先体验再对照代码理解。无论是学习图形绘制、事件响应还是游戏状态管理都能从中获得完整参考。1. 基于C的飞机大战小游戏设计源码到底解决什么问题期末课程设计又落在飞机大战上或者C语法学完半本却发现写不出一个能运行的东西——这两种情况最后都会搜到同一个标题基于C的飞机大战小游戏设计源码。说白了这是一个用C在Windows窗口里实现纵版射击小游戏的完整工程核心包含窗口初始化、游戏主循环、玩家控制、子弹与敌机管理、碰撞检测、计分与音效这几条主线。它能解决的是新手从语法到工程的断层你看着类和STL都眼熟却不知道它们在一个实时项目里怎么配合它也能当课程设计的底子跑通之后再往上加Boss、关卡、道具都不伤筋骨。适合刚入门C、想找第一个可运行项目或者正在为课程设计选题的读者。下文按我复现这个项目时的实际做法把架构选型、核心玩法、踩坑记录和优化验证完整过一遍。2. 先把架构立住EasyX选型、类设计与游戏主循环怎么写2.1 图形库选型从控制台字符版到EasyX为什么更划算飞机大战是个状态密集、每帧重绘的游戏第一件要解决的事是画面输出。常见做法有三条路控制台字符版、SDL/SFML、EasyX。控制台版用printf画飞机、getch读按键确实能跑但画面是字符拼出来的碰撞全靠坐标运算做出来不像游戏课程设计答辩时很吃亏SDL和SFML功能强可新手要先配置环境、链接库、处理窗口事件常常还没开始写玩法就卡在CMake和链接错误上。我的选择是EasyX它是Windows下的轻量图形库装好后在VS里包含graphics.h就能用initgraph开窗口、putimage贴图、BeginBatchDraw做双缓冲API贴近Windows编程但不需要自己写消息循环。如果读者习惯在VS Code里配C/C环境也有MinGW版的EasyX可用只是要手动拷贝头文件和库建议先在VS社区版把项目跑通再换环境。选型还有一个现实理由EasyX的学习曲线几乎为零熟练之后一天内就能把玩家移动和子弹发射做出来。C游戏练手项目最大的风险不是玩法逻辑而是环境配置劝退图形库越简单代码越早进入正题。顺带说一句网上有不少人用C语言在控制台做过飞机大战那个方案更适合练数组和指针不适合做图形界面和碰撞体系本项目的目标是“看起来像个真游戏”EasyX是性价比最高的中间点。2.2 核心类的划分Player、Enemy、Bullet与Game主控窗口初始化之后很多人习惯把所有状态堆在全局变量里int playerX、char bullets[100]、int enemyCount……代码一开始能跑加功能时就会乱成一团。我折中推荐四个类/结构Player、Enemy、Bullet、Game。前三个描述游戏对象Game管生命周期和主循环调度。下面这个版本是我实际用的骨架class Player { public: int x, y; // 左上角像素坐标 int w, h; // 碰撞箱尺寸 int hp; float speed; // 像素/秒 void move(float dx, float dy) { x (int)(dx * speed); y (int)(dy * speed); } }; struct Bullet { int x, y, w, h; float speed; // 像素/秒 bool alive; }; struct Enemy { int x, y, w, h; float speed; int hp; bool alive; };这段代码里有两个细节值得注意。第一坐标用int、速度用float坐标是int可以保证putimage贴图不会出现像素偏移速度是float配合帧时间dt做插值否则同样的速度在不同帧率下表现不一样。第二w和h是碰撞箱尺寸后续调手感时可以单独缩小不用改贴图源码。hp放在对象内部而不是全局记录是为了后续做Boss和道具扩展——我刚开始把敌机血量写在全局数组里加Boss时几乎重写了整段逻辑教训很深。2.3 游戏主循环帧率控制与输入检测的骨架飞机大战本质上是一个循环读输入、更新状态、画画面。所有玩法都挂在这个循环上循环结构一错后面加再多功能都白搭。常见的主循环写法是这样的#include graphics.h #include windows.h #include mmsystem.h #pragma comment(lib, winmm.lib) const int WINDOW_W 800; const int WINDOW_H 600; bool gameover false; int main() { initgraph(WINDOW_W, WINDOW_H); BeginBatchDraw(); // 双缓冲先画到内存再一次性提交 Player player{ 400, 500, 50, 50, 3, 300.0f }; timeBeginPeriod(1); // 提高Sleep精度到1ms级别 DWORD prevTime timeGetTime(); while (!gameover) { DWORD now timeGetTime(); float dt (now - prevTime) / 1000.0f; // 当前帧耗时单位秒 if (dt 0.05f) dt 0.05f; // 防止调试卡顿后瞬移 prevTime now; // 1. 输入读取键盘状态 // 2. 更新玩家移动、子弹位移、敌机生成、碰撞检测 // 3. 绘制清屏后重绘所有对象 FlushBatchDraw(); // 提交后台画面一帧结束 DWORD cost timeGetTime() - now; if (cost 16) Sleep(16 - cost); // 目标约60FPS } EndBatchDraw(); closegraph(); return 0; }这里需要解释三个关键点。第一timeGetTime()返回毫秒时间比GetTickCount()精度更高配timeBeginPeriod(1)可以把Sleep误差压到个位数毫秒不要直接用Sleep(16)当作帧率控制Windows下的Sleep本身误差可能超过10毫秒必须用实际耗时dt来驱动所有移动逻辑。第二dt要设上限否则调试器断点卡了半秒恢复后所有对象会瞬移一大段距离这属于血泪经验。第三BeginBatchDraw和FlushBatchDraw必须成对使用前者打开双缓冲后者把后台画面整帧提交少了任何一边要么闪屏严重要么画面卡住不动。3. 从零实现核心玩法玩家移动、子弹发射、敌机生成与碰撞检测3.1 窗口初始化与图片资源loadimage的正确用法在EasyX里贴图核心是loadimage加putimage。图片格式建议用PNG或JPG放在源码目录下。loadimage的第三、四个参数可以强制缩放图片这比在外部PS里切图省事得多但要注意如果图片加载失败EasyX会弹窗报错而不是静默返回所以路径写错时先检查工作目录别急着怀疑代码。#include graphics.h IMAGE imgPlayer, imgEnemy, imgBullet; void loadResources() { loadimage(imgPlayer, _T(player.png), 60, 60); loadimage(imgEnemy, _T(enemy.png), 50, 50); loadimage(imgBullet, _T(bullet.png), 10, 20); } void drawObjects(Player player, std::vectorBullet bullets, std::vectorEnemy enemies) { cleardevice(); putimage(player.x, player.y, imgPlayer); for (auto b : bullets) putimage(b.x, b.y, imgBullet); for (auto e : enemies) putimage(e.x, e.y, imgEnemy); }逻辑说明drawObjects每帧先cleardevice清屏再按玩家、子弹、敌机的顺序绘制顺序决定了遮罩关系——玩家在最上层。参数里的std::vector引用传递是为了避免拷贝整块内存这是使用STL容器时容易被忽略的性能点。另一个实用建议如果手头没有飞机素材先用solidpolygon画三角形占位把玩法逻辑跑通再换图不必一开始就在美术资源上卡住。3.2 玩家移动与边界限制用GetAsyncKeyState实现持续按键飞机移动看起来简单但输入方案的选择直接影响手感。常见误用是_kbhit加getch那套方案的问题是按一下只触发一次按住方向键不会持续移动而且可能会阻塞主循环。正确做法是用GetAsyncKeyState它返回当前按键的状态支持多键同时检测代码也不复杂。void handleInput(Player player, float dt) { float dx 0, dy 0; if (GetAsyncKeyState(VK_LEFT) 0x8000) dx - 1; if (GetAsyncKeyState(VK_RIGHT) 0x8000) dx 1; if (GetAsyncKeyState(VK_UP) 0x8000) dy - 1; if (GetAsyncKeyState(VK_DOWN) 0x8000) dy 1; player.move(dx, dy); // 边界约束把玩家限制在窗口内 player.x max(0, min(player.x, WINDOW_W - player.w)); player.y max(0, min(player.y, WINDOW_H - player.h)); }两个参数要点。第一dx和dy先取-1/0/1再在move里统一乘速度这样斜向移动的速度不会比直线快出约1.4倍如果直接在按键分支里写player.x - player.speed * dt斜向移动就会失控偏快。第二边界约束用max(0, min(...))夹值而不是判断越界后return前者代码更短而且不会出现某个方向夹住后另一个方向也失灵的情况。顺手提一下如果希望飞机半边露在屏幕外也允许把夹值的0改成负值即可比如max(-player.w / 2, ...)。3.3 子弹发射与对象管理vector增删的正确姿势子弹是游戏中高频创建、高频销毁的对象直接用new和delete管理会陷入内存泄漏的泥潭。常见做法是让vector直接存Bullet值对象每帧更新后统一清理死亡对象这里用到了STL里的remove_if配合erase这是清理容器元素的标准写法。std::vectorBullet bullets; float fireCd 0; void updateBullets(Player player, float dt) { fireCd - dt; if ((GetAsyncKeyState(VK_SPACE) 0x8000) fireCd 0) { Bullet b; b.x player.x player.w / 2 - b.w / 2; b.y player.y - b.h; b.speed 600.0f; b.alive true; bullets.push_back(b); fireCd 0.15f; // 发射冷却约每秒6发 } for (auto b : bullets) { b.y - (int)(b.speed * dt); if (b.y -b.h) b.alive false; // 飞出屏幕则标记死亡 } bullets.erase( std::remove_if(bullets.begin(), bullets.end(), [](const Bullet b) { return !b.alive; }), bullets.end()); }三个细节这里说透。第一子弹出生点必须做偏移计算player.x player.w / 2 - b.w / 2否则子弹会从玩家左翅膀射出去这是最简单的坐标错误但能让你盯屏幕怀疑人生。第二冷却时间fireCd用dt递减保证不同帧率下射速一致0.15秒的间隔约等于每秒6发这个数值可以根据难度调整。第三remove_if只是把死亡元素移到末尾真正释放空间的是erase两者必须配合不要在遍历的过程中直接调erase那样会导致迭代器失效是vector操作里最经典的翻车现场。3.4 敌机生成与移动随机坐标的设计边界敌机生成的逻辑可以拆成两个部分什么时间生成、生成在什么位置。时间由冷却管理位置用随机数控制但随机数的边界必须仔细算否则会出现敌机一半在屏幕外的尴尬画面。std::vectorEnemy enemies; float enemySpawnCd 1.0f; void updateEnemies(float dt) { enemySpawnCd - dt; if (enemySpawnCd 0) { int ex rand() % (WINDOW_W - 50); // 减掉敌机宽度保证不越界 Enemy e{ ex, -50, 50, 50, 160.0f, 1, true }; enemies.push_back(e); enemySpawnCd 1.0f; // 初始1秒一架后续随关卡缩短 } for (int i 0; i (int)enemies.size(); i) { enemies[i].y (int)(enemies[i].speed * dt); if (enemies[i].y WINDOW_H) enemies[i].alive false; } enemies.erase(std::remove_if(enemies.begin(), enemies.end(), [](const Enemy e) { return !e.alive; }), enemies.end()); }这里最值得说的是rand() % (WINDOW_W - 50)这行如果不减掉敌机宽度随机出的x可以让敌机右边界超出屏幕看起来就像卡在墙里。y初始设为-50让敌机从屏幕外“飞进来”而不是在画面顶部突然闪现飞出底边后标记死亡并清理避免容器无限膨胀。移动方向统一向下速度按秒计算后续加难度时的调整方式就是改speed和enemySpawnCd这两个量。3.5 碰撞检测AABB矩形碰撞为什么够用飞机大战最实用的碰撞检测是AABB矩形相交判定也就是检查两个矩形是否重叠。理由很直接飞机和子弹的移动速度在单帧内的位移通常小于自身尺寸矩形碰撞在游戏里的感知误差几乎为零而像素级碰撞要逐像素读取图片掩码开销大、代码复杂、收益又低。bool checkHit(int ax, int ay, int aw, int ah, int bx, int by, int bw, int bh) { return ax bx bw ax aw bx ay by bh ay ah by; } void handleCollision(Enemy e, Bullet b) { if (!e.alive || !b.alive) return; if (checkHit(e.x, e.y, e.w, e.h, b.x, b.y, b.w, b.h)) { e.hp--; b.alive false; if (e.hp 0) { e.alive false; score 100; } } }checkHit的逻辑是标准的四点判断A的右边界大于B的左边界、A的左边界小于B的右边界、上下方向同理四个条件同时满足就是相交。在实际调参时有个玄学点玩家碰撞箱的w/h通常比贴图视觉尺寸缩水20%以上敌机碰撞箱缩水10%左右这样玩家不会在视觉上还没碰到敌机就判定死亡但子弹的碰撞箱保持原尺寸不能缩否则会出现子弹穿过敌机边缘的视觉穿帮。这两条是手感校准的核心官方API里没有完全靠调试经验积累。4. C飞机大战避坑记录五个反复出现的经典问题4.1 画面闪屏双缓冲到底要怎么开现象运行后画面不停闪烁子弹和敌机数量越多越明显。原因没有开启双缓冲。默认putimage直接写入显存每次cleardevice清屏之后再逐像素重绘屏幕会先变空再填充刷新跟不上就闪。另一个隐藏原因是开了BeginBatchDraw但每帧没有调用FlushBatchDraw导致后台画面从不提交屏幕上要么卡住要么只画出半帧。解决initgraph之后立刻调用BeginBatchDraw()每帧所有绘制代码结束后调用FlushBatchDraw()退出前调用EndBatchDraw()。注意FlushBatchDraw放进循环末尾而不是清屏之后否则你只是把双缓冲变成了单缓冲。如果照做还闪检查是否有其他程序窗口叠在游戏窗口上干扰刷新这类环境问题比重写代码更常见。4.2 中文乱码outtextxy打印“得分”变成问号现象HUD上的中文文字显示成一串问号或乱码。原因EasyX的outtextxy依赖字符集设置。VS项目默认字符集会根据新建工程类型不同而不同如果项目是Unicode字符集而代码里传入的是普通const char*字符串窄字符会被错误解释成宽字符中文自然乱码反过来多字节字符集下传宽字符串也一样。解决最省事的是把项目属性改成“使用多字节字符集”然后把源文件另存为GBK编码如果坚持Unicode代码里必须写成outtextxy(x, y, _T(得分))用_T宏自动匹配字符集。我个人推荐前一种方案代码里写普通字符串就能显示中文不需要到处加_T。另外std::to_string返回的std::string在转成const char*时也要确认字符集匹配否则数字本身没问题拼接的中文会先乱。4.3 内存悄悄上涨vector里存指针却忘了delete现象游戏长时间运行后内存占用持续上升每炸一架敌机任务管理器里的内存就涨一点。原因典型写法是vectorEnemy* enemies每次生成敌机时new Enemy销毁时只从容器里erase了指针没有delete底层对象。指针本身没有析构函数容器销毁时也不会替它释放堆内存泄漏就这样发生了。解决优先让容器直接存值对象vectorEnemy生命周期完全交给容器如果非要存指针至少要用unique_ptr包一层让它在erase时自动释放。这里给一个用智能指针的等价实现std::vectorstd::unique_ptrEnemy enemies; enemies.push_back(std::make_uniqueEnemy(ex, -50, 50, 50, 160.0f, 1, true)); enemies.erase(std::remove_if(enemies.begin(), enemies.end(), [](const std::unique_ptrEnemy e) { return !e-alive; }), enemies.end());这段代码的要点是unique_ptr本身没有拷贝语义所以remove_if的谓词要接收const unique_ptr访问成员用e-。切换到这个写法之后内存基本是平稳曲线不会再一局打下来涨出几十MB。4.4 帧率忽高忽低两台电脑上的游戏速度不一样现象同一份源码在自己电脑上正常拿到室友电脑上子弹快了一倍或者游戏运行中偶发卡顿后下一帧所有东西猛跳一段。原因老式写法用固定像素位移比如每帧移动10像素60FPS下每秒移动600像素30FPS下只有300像素性能差异直接体现为速度差异而Sleep(16)并不精确系统负载高时会睡到30毫秒甚至更久。解决所有移动统一改用速度乘dt速度单位是像素/秒位移由每帧真实耗时决定。同时给dt设上限例如超过0.05秒就当0.05算这样哪怕调试器停了几秒恢复后也不会瞬移。正确写法如下player.x (int)(300.0f * dt); // 每秒向右300像素 enemies[i].y (int)(160.0f * dt);这套方案的逻辑是把“位移”从“帧率相关”变成“时间相关”游戏逻辑不再依赖机器性能低配电脑只是帧率低但游戏速度一致。配合timeBeginPeriod(1)之后60帧目标基本能稳定在55到60之间。4.5 子弹穿模与莫名死亡碰撞箱设置的两大误区现象子弹看起来穿过了敌机却没有命中判定或者玩家离敌机还有一段距离突然就掉血了。原因两个极端。第一子弹速度过快单帧位移大于敌机碰撞箱尺寸上一帧在敌机左边、下一帧已经在右边矩形相交的瞬间被跳过去这就是“穿模”第二玩家碰撞箱比飞机贴图的视觉尺寸大视觉上还没碰到两个矩形已经相交就会显得“莫名死亡”。解决高速子弹的每帧位移控制在目标对象最小边的一半以内如果不能降速就做线性扫掠检测把上一帧位置和这一帧位置连成线段再与目标矩形求交。玩家碰撞箱按贴图视觉尺寸的70%左右设置比如飞机贴图60x60碰撞箱就写42x42敌机则按90%设置保留容错空间。这块没有公式能一次到位属于调参玄学跑起来反复试就对了。5. 让游戏有反馈计分HUD、音效播放、关卡难度与爆炸特效5.1 计分与生命值HUD文本绘制的三个隐藏细节一个没有HUD的射击游戏会让玩家不知道自己打了几架、还剩几条命游戏正反馈直接减半。EasyX里显示文本的API就三个settextstyle、setbkmode、outtextxy但真正写起来有三个隐藏细节容易忽略。#include string int score 0; int playerHp 3; void drawHUD() { setbkmode(TRANSPARENT); // 文本背景透明 settextstyle(32, 0, _T(微软雅黑)); settextcolor(RGB(255, 255, 255)); std::string scoreText 得分 std::to_string(score); outtextxy(20, 20, scoreText.c_str()); std::string hpText 生命 std::to_string(playerHp); outtextxy(20, 60, hpText.c_str()); }三个细节逐一说。第一setbkmode(TRANSPARENT)必须在绘制文本之前调用否则文字背后会有一个与背景色不同的矩形色块看起来像贴了张补丁。第二settextstyle的参数顺序是高度、宽度、字体名宽度传0表示按高度等比缩放这个顺序记反是常事。第三std::to_string拼接中文字符串时要确认工程字符集与outtextxy匹配这一点承接上一章的乱码避坑如果你改的是多字节字符集这里直接传const char*没问题。5.2 音效播放PlaySound的SND_ASYNC与覆盖陷阱音效是游戏反馈里最提手感的一环。Windows的PlaySound可以播放wav文件不需要引入第三方音频库但它的播放机制是“单声道共享”新音效会打断正在播放的音效这个特性在射击游戏里直接引发覆盖问题一按空格爆炸声和射击声互相抢声道。#pragma comment(lib, winmm.lib) #include mmsystem.h void playShoot() { PlaySound(_T(shoot.wav), NULL, SND_FILENAME | SND_ASYNC | SND_NOSTOP); } void playBoom() { PlaySound(_T(boom.wav), NULL, SND_FILENAME | SND_ASYNC); }参数说明SND_FILENAME表示第一个参数是文件名SND_ASYNC表示异步播放、不阻塞主循环SND_NOSTOP表示如果当前已有音效在播放新音效不打断旧的。射击音效频率高需要SND_NOSTOP防止连续射击时声音被反复掐断爆炸音效比射击声重要所以不加NOSTOP让它直接覆盖。注意PlaySound只认wav格式mp3需要MCI接口或解码库音效文件尽量别放中文路径下部分系统环境对中文路径支持不好播放会静默失败——这类问题排查起来最容易让人怀疑人生。5.3 关卡难度递增用分数驱动而非状态机给飞机大战加难度时很多新手会写成switch(level)状态机每个关卡单独写一套生成脚本代码量瞬间膨胀调数值还要一个关卡一个关卡改。实际上纵版射击游戏的难度是连续变量直接用分数映射出生间隔、敌机速度和血量代码更短平衡性也更好调。int getLevel() { return score / 500 1; } void updateDifficulty(float enemySpawnCd, float enemySpeed, int enemyMaxHp) { int level getLevel(); enemySpawnCd max(0.3f, 1.0f - (level - 1) * 0.1f); // 间隔逐渐缩短 enemySpeed 160.0f (level - 1) * 30.0f; // 速度逐渐加快 enemyMaxHp 1 (level - 1) / 3; // 每3关加1点血量 }这段逻辑的核心是分数每增加500等级升1出生间隔从1秒向0.3秒收敛速度从160像素/秒向上累加。用max函数保底是为了防止数值溢出——如果分数无限涨enemySpawnCd不能低于0.3秒否则后期敌机刷得比子弹还密游戏变成不可玩状态。Boss的实现也走同一套数值逻辑只是Boss的碰撞箱更大、血量更多再单独画一条血条即可void drawBossHp(int bx, int by, int bw, int curHp, int maxHp) { setfillcolor(RGB(60, 60, 60)); solidrectangle(bx, by, bx bw, by 10); // 灰色底条 setfillcolor(RGB(255, 0, 0)); solidrectangle(bx, by, bx (int)(bw * curHp / (float)maxHp), by 10); }血条绘制的关键点在最后一行bw * curHp / (float)maxHp必须转浮点再算否则整数除法会让Boss掉一格血时血条视觉上纹丝不动。solidrectangle接收四个坐标参数分别是左上角x、y和右下角x、y这点也容易写混。5.4 爆炸粒子效果对象池思路解决高并发卡顿敌机爆炸一瞬间只消失太干加几个粒子就能做出明显的打击反馈。粒子的本质就是一组有初速度、衰减和生命周期的点数量不大时直接用vector管理最直观。struct Particle { float x, y, vx, vy; int life; // 剩余帧数 COLORREF color; }; std::vectorParticle particles; void spawnExplosion(int x, int y) { for (int i 0; i 25; i) { float angle (rand() % 360) * 3.14159265f / 180.0f; float speed 80.0f rand() % 120; particles.push_back({ (float)x, (float)y, cos(angle) * speed, sin(angle) * speed, 20 rand() % 15, RGB(255, 180, 0) }); } } void updateParticles(float dt) { for (auto p : particles) { p.x p.vx * dt; p.y p.vy * dt; p.vx * 0.96f; // 每帧衰减模拟空气阻力 p.vy * 0.96f; p.life--; } particles.erase(std::remove_if(particles.begin(), particles.end(), [](const Particle p) { return p.life 0; }), particles.end()); }粒子速度方向用极坐标生成随机角度加随机速度cos和sin分解到x、y轴。衰减系数0.96是调出来的值太小粒子飞不远、太大粒子飘太久这里给了一个可用的起点。性能上有个隐患后期同时在场粒子可能上千每帧erase会引发内存搬移。常见做法是预分配一个最大容量的数组用life 0标记死亡清理时把尾部活跃粒子交换到空位上而不是erase这套对象池思路等到Boss战同时炸几十架敌机时帧率差距会非常明显。6. 验证与优化从能跑到跑得好的三个自检方法6.1 FPS自检让数据告诉你循环是否健康在主循环里算一帧耗时在HUD角落画出来。fps (int)(1.0f / dt 0.5f)稳定在55到60就算合格。如果FPS跌破40优先检查每帧是否做了无谓的动态分配比如在循环里频繁push_back再erase大对象其次检查贴图尺寸是否过大EasyX在低配机器上对大图缩放的开销不小。char fpsBuf[32]; sprintf_s(fpsBuf, FPS:%d, (int)(1.0f / dt 0.5f)); outtextxy(WINDOW_W - 120, 20, fpsBuf);6.2 内存泄漏检测用CRT报告验证对象管理VS的Debug模式自带内存泄漏检测在入口处加一行_CrtSetDbgFlag(_CRTDBG_ALLOC_MEM_DF | _CRTDBG_LEAK_CHECK_DF);程序退出时输出窗口会列出所有未释放的堆内存。验证方法是让游戏跑五分钟再退出如果报告里有不断累积的块基本就是容器里的裸指针忘了delete。切换到值对象或unique_ptr之后这条报告应该清零。6.3 边界测试用极端参数校准碰撞手感把子弹速度临时改成2000像素/秒看是否穿模把玩家碰撞箱临时缩到10x10看是否还能被敌机撞死。这类临时改动是验证碰撞体系最直接的手段测完再恢复正式值。手感校准没有捷径我从最初的60x60碰撞箱一路缩到42x42才找到一个“玩家骂娘率”最低的配置这个值比任何教程给的都适合你手头的美术素材。我第一次写这个游戏时把所有对象都塞进定长数组预设上限1000结果游戏后期子弹打光、敌机静默消失调了整整一个晚上才发现是数组越界静默吞数据。后来换了vector加对象池才算明白“小游戏”的工程性一点都不小——框架选型、数据结构、字符集、帧率控制这些坑随便哪一个都能让新手卡上一整天。希望这篇笔记帮你在同一个项目上少走我走过的弯路把时间花在玩法打磨上希望帮到你。本文还有配套的精品资源点击获取
返回列表