ARTICLE DETAIL

资讯详情

深耕网站视觉设计与运营推广的一线实战洞察。

C++飞机大战源代码解析:从类设计到固定帧率游戏循环实战

C++飞机大战源代码解析:从类设计到固定帧率游戏循环实战 简介这是一份面向C初学者与课程设计需求的经典飞机大战小游戏完整源码适合想通过实战项目巩固面向对象编程、图形绘制与游戏循环的开发者练手。资源包共46个文件约587KB以30个png图片素材、4个h头文件与4个cpp源文件为核心另含vcxproj工程文件、sln解决方案、ttf字体、ico图标及README说明结构清晰可直接用Visual Studio打开编译运行。项目实现了WSAD键控制飞机移动、空格释放炸弹等基础玩法玩家需在躲避敌机的同时消灭更多敌人并借助函数模板降低重复代码体现了一定的代码复用设计思路。目前已有370人学习下载。通过阅读源码读者可掌握精灵绘制、碰撞检测、敌机生成与子弹管理等核心逻辑理解游戏主循环与资源加载流程并借鉴模板封装与模块划分方式为后续扩展关卡、道具或音效打下基础。1. 从一份 C 飞机大战源代码说起它到底能帮你解决什么很多人第一次搜「基于C实现的经典小游戏飞机大战源代码」动机其实很朴素C 语法书翻了大半本指针、类、继承都看过但一到自己动手就不知道从哪下手。飞机大战这个题材刚好卡在一个舒服的位置——它比控制台里打印九九乘法表复杂又没到需要引擎和美术资源的程度一个文件几百行就能跑起来键盘一按飞机就动子弹一发射就能打中敌机反馈是即时的。对刚学完 C 基础、想找一个能完整跑通的小项目练手的人来说这份源代码的价值不在于「游戏多好玩」而在于它把类与对象、循环与状态更新、数组或容器管理、随机数、碰撞检测这些零散知识点串成了一条能运行的线。你照着敲一遍、改几个参数、看它怎么崩、再修回来这个过程比看十篇教程都实在。这一章先把这份源代码的定位、适用人群和它真正能训练到的能力讲清楚后面几章再拆实现、参数和踩坑。2. 飞机大战源代码的骨架类怎么分、循环怎么转2.1 为什么用类来拆飞机、子弹和敌机一份能读得下去的 C 飞机大战源代码通常不会把所有逻辑塞进 main 函数。常见做法是先把游戏里会动、会消失、会被打中的东西抽象成类玩家飞机、子弹、敌机有的还会加一个基类把「位置、速度、存活状态、绘制」这些共性抽出来。这样做的直接好处是新增一种敌机或者换一种子弹不用去改主循环里一大坨 if-else只要继承基类、改几个成员初值就行。从训练角度看这一步对应的是 C 里「封装」和「继承」最朴素的用法。玩家飞机有坐标 x、y有移动速度有生命值子弹有坐标、有速度、有是否命中敌机除了坐标和速度可能还有血量、分数。把这些字段放进类里再用一个容器统一管理主循环里就只剩「更新所有对象、检测碰撞、重绘」三件事。新手最容易犯的错是把所有变量写成全局的几十个变量铺在文件顶部改一个动全身这也是为什么建议一开始就按类拆。2.2 主循环固定帧率比「能跑就行」重要飞机大战这类实时小游戏核心是一个不断重复的游戏循环。它每一帧要做的事基本固定处理输入、更新所有对象位置、检测碰撞、清理死亡对象、重绘画面、控制帧率。很多网上流传的源代码为了简单直接用while(true)加一个Sleep或delay帧率飘忽不定飞机移动速度时快时慢这就是典型的「能跑但手感差」。下面这段是一个最小可用的主循环骨架用固定时间步长推进逻辑避免不同机器上速度不一致#include chrono #include thread const double FRAME_TIME 1.0 / 60.0; // 目标 60 帧 void gameLoop() { auto last std::chrono::steady_clock::now(); double accumulator 0.0; while (running) { auto now std::chrono::steady_clock::now(); double delta std::chrono::durationdouble(now - last).count(); last now; accumulator delta; // 固定步长更新保证物理逻辑稳定 while (accumulator FRAME_TIME) { handleInput(); // 读取键盘状态 updateObjects(FRAME_TIME); // 按固定步长移动 checkCollisions(); // 碰撞检测 removeDeadObjects();// 清理越界/死亡对象 accumulator - FRAME_TIME; } render(); // 绘制当前帧 std::this_thread::sleep_for(std::chrono::milliseconds(1)); } }逻辑说明steady_clock取的是单调递增时间不受系统时间调整影响适合做游戏计时。accumulator累积真实流逝时间只有攒够一个FRAME_TIME才推进一次逻辑这样即使某帧渲染慢了逻辑步长依然固定。参数上FRAME_TIME设成 1/60 是常见选择改小会让逻辑更细腻但更吃 CPU改大则移动变粗糙。sleep_for那 1 毫秒是为了让出 CPU避免空转把单核跑满不是用来控制帧率的——帧率由 accumulator 控制。2.3 对象管理数组、vector 还是对象池子弹和敌机是高频生成、高频销毁的对象。新手常见写法是每帧新建一堆对象、删掉一堆对象短时间看不出问题但子弹一多就会卡。更稳的做法是用std::vector存对象死亡时标记alive false统一在removeDeadObjects里用 erase-remove 惯用法清理#include vector #include algorithm struct Bullet { double x, y; double vy; bool alive true; }; std::vectorBullet bullets; void removeDeadObjects() { bullets.erase( std::remove_if(bullets.begin(), bullets.end(), [](const Bullet b) { return !b.alive; }), bullets.end()); }逻辑说明std::remove_if把存活对象挪到前面返回新的逻辑结尾erase再真正删除尾部元素这是 C 里删除容器元素的固定套路比边遍历边 erase 安全。参数上alive标志位是关键所有越界、命中的对象都只置 false不立刻删避免在遍历过程中改动容器导致迭代器失效。如果对象数量进一步增大可以考虑对象池把死亡对象回收复用减少内存分配但对飞机大战这个量级vector 加标志位已经够用。3. 让飞机真的动起来输入、随机数与碰撞检测3.1 键盘输入别用阻塞式读取飞机大战要的是「按住方向键持续移动」而不是「按一下动一格」。如果用std::cin或阻塞式getch游戏会卡在等待输入上主循环根本转不起来。常见做法是用平台相关的非阻塞键盘检测比如 Windows 下用GetAsyncKeyState或者用跨平台库统一处理。下面是一个 Windows 下的简化示例#include windows.h struct Player { double x 400, y 500; double speed 300.0; // 像素/秒 }; void handleInput(Player p, double dt) { if (GetAsyncKeyState(VK_LEFT) 0x8000) p.x - p.speed * dt; if (GetAsyncKeyState(VK_RIGHT) 0x8000) p.x p.speed * dt; if (GetAsyncKeyState(VK_UP) 0x8000) p.y - p.speed * dt; if (GetAsyncKeyState(VK_DOWN) 0x8000) p.y p.speed * dt; // 边界限制防止飞出屏幕 if (p.x 0) p.x 0; if (p.x 800) p.x 800; if (p.y 0) p.y 0; if (p.y 600) p.y 600; }逻辑说明GetAsyncKeyState返回值的最高位为 1 表示按键当前按下 0x8000就是取这个位。乘以dt是为了让移动速度与帧率无关speed单位是像素每秒改大飞机更灵敏。边界限制那几行是必须的否则飞机能飞出可视区域玩家找不到自己。注意这里没有用else if因为斜向移动需要同时响应两个方向键。3.2 随机数敌机生成位置别用 rand()%n敌机从上方随机位置出现是飞机大战的基本玩法。很多人顺手写rand() % 800能跑但有两个问题一是rand()质量一般二是取模会引入低位偏差。C11 之后更推荐用random里的设施#include random std::mt19937 gen(std::random_device{}()); std::uniform_int_distributionint xDist(0, 800); std::uniform_real_distributiondouble speedDist(80.0, 200.0); void spawnEnemy(std::vectorEnemy enemies) { Enemy e; e.x xDist(gen); // 水平位置均匀分布 e.y -50; // 从屏幕上方外进入 e.vy speedDist(gen); // 下落速度随机 e.alive true; enemies.push_back(e); }逻辑说明mt19937是梅森旋转算法周期长、分布均匀random_device用来做种子避免每次运行敌机位置完全一样。uniform_int_distribution和uniform_real_distribution分别生成整数和浮点均匀分布比手动取模更可控。参数上xDist的范围要和窗口宽度一致speedDist的下限别设太小否则敌机慢得像静止上限别超过玩家反应极限一般 80 到 200 像素每秒比较合适。3.3 碰撞检测矩形相交就够用飞机大战不需要像素级碰撞矩形包围盒相交检测又快又够用。核心判断是两个矩形在 x 轴和 y 轴上的投影都重叠就算碰撞。struct Rect { double x, y, w, h; }; bool isCollide(const Rect a, const Rect b) { return a.x b.x b.w a.x a.w b.x a.y b.y b.h a.y a.h b.y; }逻辑说明四个条件分别判断 a 的左边界在 b 右边界左侧、a 右边界在 b 左边界右侧、a 上边界在 b 下边界上方、a 下边界在 b 上边界下方全部满足才算相交。参数上x, y一般取对象左上角坐标w, h是宽高。如果对象坐标存的是中心点记得先换算成左上角再传进来否则碰撞会整体偏移半个身位这是很常见的翻车点。4. 编译、运行与调试把源代码跑起来要过的几道坎4.1 环境选择Visual Studio、Dev-C 还是 VS Code拿到一份 C 飞机大战源代码第一道坎往往不是代码本身而是环境。Windows 上最常见的是 Visual Studio新建空项目、把源文件加进去、直接 F5 就能跑缺点是体积大。Dev-C 轻量但默认标准较老遇到random、chrono这些 C11 设施可能报错需要在编译器选项里加-stdc11或更高。VS Code 本身只是编辑器要自己配 MinGW 或 MSVC 工具链配不好就会出现「找不到头文件」「链接失败」这类问题。如果你只是想把这份源代码跑起来看效果我一般建议先用 Visual Studio 或 Dev-C 把流程走通确认代码逻辑没问题再考虑换到 VS Code 做长期开发。环境配置本身是个独立话题卡住了先搜「vscode 配置 c/c 环境」这类关键词按步骤来别一上来就折腾 CMake。4.2 常见编译错误对照报错信息常见原因处理方式random file not found编译器标准太老加-stdc11或换新版编译器undefined reference to ...函数声明了没实现或没链接对应库检查实现文件是否加入编译cannot open source file windows.h在非 Windows 平台编译换平台相关代码或改用跨平台库expected ;类定义或结构体末尾漏分号补上分号程序一闪而过主循环结束后窗口立即关闭在退出前加暂停或断点这张表里的问题前三个占了新手卡壳的绝大多数。尤其是标准版本问题很多老教程默认 C98代码里没有random你拿新代码去老环境编译必然报错。遇到报错先看第一行编译器通常会把真正原因放在最前面。4.3 调试先让飞机动再让子弹飞调试游戏代码有个顺序原则先保证主循环能转、飞机能响应键盘再加子弹再加敌机最后加碰撞和计分。很多人一上来就把完整代码全贴进去一跑就崩然后对着几百行找问题效率极低。正确做法是分阶段验证每加一个功能就运行一次确认没问题再继续。如果飞机不动先检查输入处理有没有被调用、GetAsyncKeyState的返回值判断对不对如果子弹不出现检查生成条件、容器有没有被清空、绘制循环有没有遍历到如果碰撞不触发打印两个矩形的坐标和宽高看是不是坐标系不一致。调试游戏最有效的工具就是打印关键变量别嫌土。5. 避坑与排查飞机大战源代码里最容易翻车的 5 个点5.1 现象飞机移动速度忽快忽慢原因主循环没有固定时间步长直接用while加Sleep每帧实际耗时受系统调度影响导致移动距离不一致。解决按第 2.2 节的固定步长方案用steady_clock计算 delta累积到固定步长再更新逻辑移动速度乘以固定步长而不是真实 delta。5.2 现象子弹或敌机越来越多游戏越来越卡原因死亡对象只置了标志位但没有真正从容器里删除或者每帧都在新建对象而没有回收。解决统一在每帧末尾用 erase-remove 清理alive false的对象如果对象数量确实大考虑对象池复用。检查绘制循环是否只遍历存活对象。5.3 现象碰撞检测偶尔失灵明明打中了却没反应原因坐标系不统一有的对象存左上角有的存中心点或者碰撞矩形宽高设得比实际图片小很多。解决统一所有对象的坐标约定建议都用左上角加宽高碰撞矩形可以比视觉图片略小一点但不能小太多否则会出现「擦边不算」的玄学问题。打印碰撞时的矩形参数核对。5.4 现象换台电脑运行敌机生成位置完全一样原因随机数种子固定比如srand(0)或mt19937 gen(0)每次运行序列相同。解决用std::random_device{}()做种子或者用当前时间做种子。注意random_device在某些老编译器上可能退化成固定值如果发现还是重复可以混入时间戳。5.5 现象程序运行一段时间后崩溃原因常见于在遍历容器时删除元素导致迭代器失效或者数组越界访问。解决删除元素统一用 erase-remove不要在 for 循环里直接 erase所有数组和 vector 访问前检查下标范围。如果用了裸指针检查有没有释放后继续使用。6. 从能跑到好用给飞机大战加一点进阶手感把基础版本跑通之后真正拉开差距的是手感。同样是飞机大战有的版本玩起来黏糊糊有的版本一上手就顺差别往往在几个细节上。第一个是移动加速度直接改位置是瞬移感改成速度渐变会舒服很多按键时给一个加速度松开时给一个反向阻尼飞机不会急停。第二个是射击节奏别让玩家按住就无限连发加一个冷却时间比如 0.15 秒一发既防止子弹刷屏也让操作有节奏。第三个是敌机生成曲线前期慢、后期快用时间做参数调整生成间隔比固定间隔耐玩得多。// 带加速度和阻尼的移动 struct Player { double x 400, y 500; double vx 0, vy 0; double accel 2000.0; // 加速度 double damping 0.85; // 阻尼系数 }; void handleInput(Player p, double dt) { if (GetAsyncKeyState(VK_LEFT) 0x8000) p.vx - p.accel * dt; if (GetAsyncKeyState(VK_RIGHT) 0x8000) p.vx p.accel * dt; if (GetAsyncKeyState(VK_UP) 0x8000) p.vy - p.accel * dt; if (GetAsyncKeyState(VK_DOWN) 0x8000) p.vy p.accel * dt; p.vx * p.damping; // 阻尼让速度逐渐衰减 p.vy * p.damping; p.x p.vx * dt; p.y p.vy * dt; }逻辑说明按键时改变的是速度而不是位置松开后阻尼让速度慢慢归零飞机就有了惯性。accel越大越灵敏damping越小停得越快这两个参数需要一起调一般accel在 1500 到 3000、damping在 0.8 到 0.95 之间试。注意阻尼是每帧乘一次和帧率相关严格来说应该用pow(damping, dt * 60)做帧率无关处理但对小游戏来说直接乘也能接受。验证改动有没有效果最直接的办法是录一段自己玩的视频回放看飞机响应跟不跟手、子弹密度是否合适。我自己的习惯是每改一个手感参数就玩三局三局都觉得别扭就回退别一次改一堆参数否则出了问题都不知道是哪个引起的。飞机大战这个项目不大但正是这种小项目才能让你把 C 的类、容器、循环、随机数真正用一遍而不是停留在语法层面。希望帮到你。本文还有配套的精品资源点击获取
返回列表