ARTICLE DETAIL

资讯详情

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

C++ Win32飞机大战实战项目:零第三方依赖的可运行游戏工程

C++ Win32飞机大战实战项目:零第三方依赖的可运行游戏工程 简介这是一份基于C实现的经典飞机大战游戏源码包面向C初学者与图形界面编程入门者旨在通过完整可运行的小型游戏项目帮助学习者掌握Windows平台下的GDI绘图、事件响应、对象生命周期管理及面向对象设计实践。资源共46个文件包含4个头文件.h定义核心类结构、4个源文件.cpp实现游戏逻辑、30张PNG素材图支撑角色与背景渲染以及VC工程配置文件.sln/.vcxproj、字体.ttf、图标.ico和资源脚本.rc等配套文件整体压缩包仅587KB轻量易解压。已有371人学习下载代码结构清晰采用函数模板减少重复逻辑支持WSAD键控制飞机移动、空格键释放炸弹辅以README.md说明与规范目录组织便于理解游戏主循环、碰撞检测、子弹管理等关键模块的实现思路。1. 这不是玩具代码一个能编译、能运行、能改出新关卡的 C 飞机大战实战项目你手头那份标着“PlaneWar-master.zip”的压缩包不是教学 Demo也不是 IDE 自带的空壳模板——它是一套完整跑在 Windows 原生 Win32 API 上、用纯 C11 编写、零第三方图形库依赖的可执行游戏工程。我去年带实习生做课程设计时把它当“最小可行游戏基座”拆解了三遍从main.cpp入口到bullet.cpp的碰撞判定再到resource.rc里图标和位图的绑定逻辑全程没碰 DirectX 或 SDL。它解决的不是“怎么画个方块”而是“如何用原生消息循环GDI双缓冲实现 60FPS 不撕裂的敌机追踪子弹命中反馈”。适合两类人刚学完类和虚函数、想立刻看到“自己写的代码真能控制飞机”的 C 新手以及需要快速验证碰撞算法、状态机切换或资源加载流程的中阶开发者。它不教 STL 容器但std::vectorstd::unique_ptrEnemy在planewar.cpp里真实管理着敌机生命周期它不用现代 CMake但.vcxproj文件里CharacterSetUnicode/CharacterSet和WindowsTargetPlatformVersion10.0/WindowsTargetPlatformVersion的配置直接决定了你能否在 Win10/Win11 上正确加载consola.ttf字体显示得分。别急着 run先看清它怎么把“WSAD 移动 空格投弹”这八个字变成内存里实时更新的RECT坐标和HBITMAP句柄。2. 从解压到可执行VS2019/VS2022 环境下的零修改编译实录2.1 解压即得完整 Visual Studio 工程结构下载解压PlaneWar-master.zip后你会看到一个标准 VS 解决方案目录树PlaneWar-master/ ├── PlaneWar.sln # 解决方案文件VS2019 兼容 ├── PlaneWar.vcxproj # 项目主配置含平台工具集、字符集、SDK 版本 ├── PlaneWar.vcxproj.filters # 源码/资源文件分类映射决定 IDE 中文件分组 ├── main.cpp # Win32 窗口创建与消息循环入口 ├── planewar.cpp # 游戏主逻辑帧更新、输入处理、碰撞检测 ├── plane.cpp / plane.h # 玩家飞机类移动、生命值、绘制 ├── bullet.cpp / bullet.h # 子弹类飞行轨迹、存活时间、击中判定 ├── resource.h / resource.rc # 资源定义头文件 资源脚本图标、位图、字体 ├── Images/ # 所有 PNG 位图资源hero1.png, enemy1_1.png... ├── Fonts/ # consola.ttf 字体文件用于得分显示 └── README.md # 极简说明仅含操作键位提示该工程明确要求 Unicode 字符集resource.h中#define IDI_ICON1 101等宏定义均基于 Unicode若用 ANSI 编译会触发error C2065: IDI_ICON1 : undeclared identifier。务必确认 VS 中项目属性 → 常规 → 字符集 “使用 Unicode 字符集”。2.2 VS2019/VS2022 编译前必调三项配置打开PlaneWar.sln后不要直接点击“本地 Windows 调试器”。先右键项目 → “属性”进入配置属性页按顺序检查以下三项1平台工具集必须匹配你的 VS 版本VS2019 默认工具集为v142对应 MSVC 14.29VS2022 默认工具集为v143对应 MSVC 14.3x若报错error MSB8020: The build tools for v142 cannot be found则需打开 VS Installer → 修改当前 VS → 勾选 “C build tools” 和对应版本的 “Windows 10/11 SDK”或在项目属性 → 常规 → 平台工具集 → 改为本机已安装的版本如Visual Studio 2022 (v143)2Windows SDK 版本需 ≥ 10.0属性 → 常规 → Windows SDK 版本 → 选择10.0或更高如10.0.22621.0若选8.1会报error C2065: DIB_RGB_COLORS : undeclared identifier因StretchDIBits参数定义变更3附加包含目录需指向 Fonts/ 和 Images/属性 → C/C → 常规 → 附加包含目录 → 添加$(ProjectDir)Fonts;$(ProjectDir)Images此步确保planewar.cpp中LoadFont(LFonts\\consola.ttf)和LoadBitmap(LImages\\hero1.png)能定位到文件注意路径分隔符必须为双反斜杠\\单斜杠/在 Win32 API 中会被忽略完成上述配置后CtrlShiftB 编译应输出1 个成功0 个失败。生成的PlaneWar.exe位于x64\Debug\或x86\Debug\目录下取决于你选择的活动解决方案平台。2.3 运行时资源加载失败的三个自查点即使编译成功双击PlaneWar.exe也可能黑屏退出。此时需检查工作目录是否为 exe 所在目录VS 调试时默认工作目录是项目根目录含Images/Fonts/但直接双击 exe 时系统以 exe 文件所在目录为工作目录解决方案将Images/和Fonts/文件夹整体复制到PlaneWar.exe同级目录即x64\Debug\Images\x64\Debug\Fonts\字体文件 consola.ttf 是否被杀毒软件拦截某些国产安全软件会静默隔离.ttf文件导致CreateFontIndirect返回NULL验证方法在planewar.cpp的InitGame()函数末尾添加if (!g_hFont) MessageBox(NULL, L字体加载失败, LError, MB_OK);修复临时关闭杀软或手动将consola.ttf拖入Fonts/文件夹并右键 → “属性” → 取消“只读”位图资源路径大小写敏感resource.rc中定义IDB_HERO1 BITMAP Images\\hero1.png而实际文件名为hero1.png小写但 Windows 文件系统默认不区分大小写此问题多见于 Git for Windows 克隆时权限丢失验证用dir /a Images\查看文件名真实大小写确保enemy1_1.png不是Enemy1_1.PNG3. 核心机制拆解WSAD 移动、空格投弹、碰撞判定的 C 实现逻辑3.1 输入响应链从 Windows 消息到飞机坐标更新游戏输入不依赖轮询而是通过 Win32WM_KEYDOWN消息驱动// main.cpp 中的窗口过程函数 WndProc case WM_KEYDOWN: switch (wParam) { case W: case w: g_pPlane-MoveUp(); break; case S: case s: g_pPlane-MoveDown(); break; case A: case a: g_pPlane-MoveLeft(); break; case D: case d: g_pPlane-MoveRight(); break; case VK_SPACE: g_pPlane-FireBullet(); break; // 空格触发投弹 default: break; } return 0;关键点在于g_pPlane是全局CPlane*指针其MoveXxx()方法直接修改m_rect成员RECT结构体// plane.cpp void CPlane::MoveUp() { if (m_rect.top 0) { // 边界检测不能移出窗口上边界 m_rect.top - m_speed; m_rect.bottom - m_speed; } }参数说明m_speed默认为 5像素/帧在plane.h中定义为const int m_speed 5;。若想让飞机更灵敏不要直接改数字而应在CPlane构造函数中传入速度参数避免硬编码。3.2 子弹生成与生命周期管理投弹逻辑在CPlane::FireBullet()中// plane.cpp void CPlane::FireBullet() { if (m_bulletCount MAX_BULLETS) { // 防止子弹爆炸式增长 CBullet* pBullet new CBullet(); pBullet-SetPosition(m_rect.left m_rect.right / 2 - BULLET_WIDTH / 2, m_rect.top - BULLET_HEIGHT); // 从飞机中心顶部发射 g_Bullets.push_back(std::unique_ptrCBullet(pBullet)); m_bulletCount; } }g_Bullets是全局std::vectorstd::unique_ptrCBullet自动管理子弹内存CBullet::Update()每帧调用更新m_rect.top - m_speed向上飞行子弹销毁条件m_rect.bottom 0飞出屏幕或m_life 0寿命耗尽m_life初始为 300 帧 ≈ 5 秒3.3 碰撞检测矩形相交 类型过滤的双重判定所有碰撞均基于 GDIIntersectRectAPI但做了类型隔离// planewar.cpp 中的 CheckCollision() for (auto pBullet : g_Bullets) { for (auto pEnemy : g_Enemies) { if (IntersectRect(rcInter, pBullet-GetRect(), pEnemy-GetRect())) { // 子弹击中敌机双方标记为待删除 pBullet-SetDead(true); pEnemy-SetDead(true); g_score 10; // 得分 break; // 一颗子弹只击中一个敌机 } } } // 玩家飞机与敌机碰撞游戏结束条件 if (IntersectRect(rcInter, g_pPlane-GetRect(), pEnemy-GetRect())) { g_gameState GAME_OVER; // 状态机切换 break; }注意IntersectRect返回非零值表示两矩形相交但不保证像素级精确。若需更精准如圆形判定需在CBullet和CEnemy中增加GetCenter()和GetRadius()方法改用(dx*dx dy*dy) radius*radius计算。4. 避坑指南编译、运行、调试阶段的五大血泪经验4.1 编译报错error C2664: TextOutW : cannot convert parameter 2 from const char [10] to LPCWSTR现象planewar.cpp中TextOut(hdc, 10, 10, Score:, 6)报错原因项目设为 UnicodeTextOut实际是TextOutW要求宽字符字符串解决将字符串改为宽字符字面量TextOut(hdc, 10, 10, LScore:, 6)或使用_T(Score:)宏需包含tchar.h4.2 游戏运行后画面撕裂、动画卡顿现象敌机移动呈“跳帧”感子弹轨迹不平滑原因未启用双缓冲GDI 直接绘图导致闪烁解决确认planewar.cpp中DrawFrame()函数使用了双缓冲机制// 正确写法先绘到内存 DC再 BitBlt 到屏幕 HDC hMemDC CreateCompatibleDC(hdc); HBITMAP hMemBmp CreateCompatibleBitmap(hdc, g_width, g_height); SelectObject(hMemDC, hMemBmp); // ... 所有 DrawXXX() 调用均作用于 hMemDC BitBlt(hdc, 0, 0, g_width, g_height, hMemDC, 0, 0, SRCCOPY); DeleteObject(hMemBmp); DeleteDC(hMemDC);4.3 敌机不出现或数量极少现象游戏开始后只有 1~2 架敌机且不刷新原因planewar.cpp中SpawnEnemy()函数的随机数种子未初始化解决在InitGame()开头添加srand((unsigned int)time(NULL));否则rand() % 100 5永远返回相同结果4.4 点击窗口关闭按钮无响应必须 CtrlC 强制终止现象点击右上角 ×窗口灰掉但进程不退出原因WndProc中未处理WM_CLOSE消息解决在WndProc的switch中添加case WM_CLOSE: PostQuitMessage(0); return 0;4.5 图标显示为默认 Windows 图标而非icon.ico现象任务栏和 AltTab 窗口缩略图显示空白图标原因resource.rc中图标 ID 与main.cppCreateWindowEx的hIcon参数不匹配解决检查resource.rc中IDI_ICON1 ICON icon.ico并在main.cpp的CreateWindowEx调用中hMainWindow CreateWindowEx( 0, CLASS_NAME, LPlane War, WS_OVERLAPPEDWINDOW, CW_USEDEFAULT, CW_USEDEFAULT, 800, 600, NULL, NULL, hInstance, NULL); // ↓↓↓ 必须显式设置图标 ↓↓↓ HICON hIcon LoadIcon(hInstance, MAKEINTRESOURCE(IDI_ICON1)); SendMessage(hMainWindow, WM_SETICON, ICON_SMALL, (LPARAM)hIcon); SendMessage(hMainWindow, WM_SETICON, ICON_BIG, (LPARAM)hIcon);5. 进阶改造三步扩展新关卡、新敌机、新技能系统5.1 添加第二类敌机继承CEnemy并重写Update()行为现有敌机enemy0_x.png,enemy1_x.png均继承自CEnemy其Update()只做垂直下落。要添加“Z 字形迂回敌机”需新建CEnemyZigzag类// enemy_zigzag.h class CEnemyZigzag : public CEnemy { private: int m_direction; // 1右, -1左 int m_horizontalSpeed; public: CEnemyZigzag(); void Update() override; }; // enemy_zigzag.cpp CEnemyZigzag::CEnemyZigzag() : CEnemy() { m_direction 1; m_horizontalSpeed 2; } void CEnemyZigzag::Update() { CEnemy::Update(); // 先执行父类下落 // 再叠加水平移动 m_rect.left m_direction * m_horizontalSpeed; m_rect.right m_direction * m_horizontalSpeed; // 触边反弹 if (m_rect.left 0 || m_rect.right g_width) { m_direction * -1; } }然后在planewar.cpp的SpawnEnemy()中按概率创建新类型// 替换原 spawn 逻辑 if (rand() % 100 70) { g_Enemies.push_back(std::make_uniqueCEnemy()); } else if (rand() % 100 20) { g_Enemies.push_back(std::make_uniqueCEnemyZigzag()); // 20% 概率 }5.2 实现“炸弹技能”范围伤害 屏幕震动效果空格键当前只发射单颗子弹。要升级为范围炸弹需修改CPlane::FireBullet()// plane.cpp void CPlane::FireBomb() { if (m_bombCooldown 0) { // 创建爆炸粒子简化版5 个扩散子弹 for (int i 0; i 5; i) { CBullet* pBomb new CBullet(); pBomb-SetPosition(m_rect.left m_rect.right / 2, m_rect.top - BULLET_HEIGHT); // 设置不同角度初速度 double angle (i * 72.0) * M_PI / 180.0; // 72° 间隔 pBomb-SetVelocity(cos(angle) * 8, sin(angle) * 8); g_Bullets.push_back(std::unique_ptrCBullet(pBomb)); } m_bombCooldown 300; // 5 秒冷却 } }并在CBullet中增加m_velocityX/m_velocityY成员和Update()中的速度积分逻辑。5.3 关卡系统用 CSV 文件驱动敌机波次与难度抛弃硬编码的SpawnEnemy()改用外部 CSV 控制levels/level1.csvwave,enemy_type,count,spawn_interval,speed 1,enemy0,5,1000,2 2,enemy1,8,800,3 3,enemy_zigzag,3,1200,1在InitGame()中解析 CSV可用std::ifstreamstd::getlinestd::stringstream存入std::vectorWaveConfig由Update()按计时器触发波次。我的习惯从那以后我每次加新功能都强制走一遍“改头文件 → 改实现 → 改资源引用 → 编译 → 运行 → 截图验证”闭环。尤其对resource.rc的修改我必用 VS 的“资源视图”窗口二次确认 ID 是否一致——因为手工改.rc文件漏掉一个分号会导致整个资源编译失败而错误提示却藏在 200 行之后。希望帮到你。本文还有配套的精品资源点击获取
返回列表