C++游戏开发入门:从零构建飞机大战游戏,掌握图形编程与面向对象实践

📅 2026/8/5 12:17:29
C++游戏开发入门:从零构建飞机大战游戏,掌握图形编程与面向对象实践
1. 项目概述从零到一用C构建你的第一个图形化小游戏如果你正在学习C并且已经厌倦了在控制台里打印“Hello World”或者做各种数学计算那么亲手实现一个图形化的《飞机大战》游戏绝对是一个里程碑式的项目。这不仅仅是把语法知识串联起来更是第一次真正意义上“创造”出一个有画面、有交互、能玩的程序。很多初学者卡在理论和实践之间知道类、知道循环但不知道如何让一个飞机在屏幕上动起来如何让子弹击中敌机并产生爆炸效果。这个项目就是打通这“最后一公里”的绝佳实践。我当年学C的时候也是通过做小游戏才真正开窍的。你会发现之前那些枯燥的指针、内存管理、面向对象概念突然都有了落地的场景。为什么需要这个指针因为你要动态管理敌机对象。为什么要用类因为你的飞机、子弹、敌机都有共同的属性和行为。这个项目麻雀虽小五脏俱全涵盖了游戏循环、图形渲染、碰撞检测、事件处理、对象管理等多个核心游戏开发概念。它不依赖任何复杂的游戏引擎仅仅使用一个轻量级的图形库就能让你看清游戏运行的本质逻辑。整个实现过程我们将使用Visual Studio作为开发环境并引入EasyX这个专为C/C设计的图形库。EasyX的API非常简洁类似于早期Turbo C的graphics.h但它兼容现代的Visual Studio让你能快速地在窗口上画点、线、圆、图片并处理键盘鼠标消息。这比直接上手DirectX或OpenGL要友好得多能让你专注于游戏逻辑本身。最终你将得到一个拥有玩家飞机、多种敌机、子弹射击、分数统计、生命值等完整元素的《飞机大战》游戏。下面我们就来一步步拆解把这个想法变成屏幕上鲜活的代码。2. 核心设计思路与架构拆解在动手写代码之前理清思路至关重要。一个结构混乱的游戏代码会很快变成难以维护的“屎山”。我们的目标是构建一个清晰、易于扩展的架构。2.1 游戏核心循环一切动起来的引擎游戏的核心是一个无限循环通常称为“游戏循环”Game Loop。每一次循环我们称之为一“帧”。在每一帧里我们需要按顺序完成以下几件事处理输入检测玩家是否按下了键盘如方向键、空格键。更新游戏状态根据输入和游戏规则更新所有游戏对象的位置、状态例如根据左键按下将玩家飞机坐标向左移动根据时间让敌机向下移动检查子弹是否击中敌机。渲染画面将最新的游戏状态绘制到屏幕上清空上一帧的画面然后重新画出飞机、子弹、敌机、分数等。这个“处理-更新-渲染”的循环以极快的速度例如每秒60帧运行就形成了我们看到的连续动画。在控制台程序中我们可能用while(1)循环但在图形界面中我们需要将这个循环嵌入到Windows消息机制中或者使用定时器来驱动。使用EasyX我们可以很方便地在main函数里用一个while循环来实现这个核心逻辑。2.2 面向对象设计游戏对象的抽象我们将游戏中的所有实体都抽象为“对象”Object并用C的“类”Class来描述它们。这是本项目应用面向对象编程思想的关键。主要包含以下几类玩家飞机类 (CPlayer)代表玩家控制的飞机。属性包括坐标(x, y)、生命值(hp)、移动速度(speed)、拥有的子弹类型等。方法包括移动(move)、射击(shoot)、绘制自身(draw)、被击中(hit)等。子弹类 (CBullet)代表从玩家或敌机发射出的子弹。属性包括坐标、速度、威力、方向向上打敌机或向下打玩家等。方法包括移动(update)、绘制(draw)、检查是否飞出屏幕外(isOut)等。敌机类 (CEnemy)代表敌方单位。我们可以设计多种敌机比如普通敌机、快速敌机、Boss敌机它们可以继承自一个敌机基类。属性包括坐标、生命值、速度、类型、分数价值等。方法包括移动(update)、绘制(draw)、被击中(hit)、是否与玩家碰撞(checkCollisionWithPlayer)等。爆炸效果类 (CExplosion)当子弹击中敌机或玩家被击中时需要播放一个短暂的动画。这个类可以管理爆炸动画的帧序列和播放状态。游戏管理类 (CGame)这是一个总管类负责创建和管理上述所有对象的集合例如用vectorCBullet存储所有活跃的子弹管理游戏状态是否进行中、是否暂停、当前分数、关卡以及协调核心循环中的更新和渲染步骤。采用这种设计游戏主循环的代码会非常清晰CGame game; // 游戏总管 game.Init(); // 初始化加载图片创建初始对象 while (!game.IsGameOver()) { game.HandleInput(); // 处理键盘输入 game.Update(); // 更新所有对象状态移动、碰撞检测 game.Render(); // 绘制当前帧画面 Sleep(10); // 控制帧率每帧休眠10毫秒约100帧/秒 }2.3 关键算法碰撞检测碰撞检测是游戏中的核心算法之一用于判断两个物体是否发生了重叠碰撞。在《飞机大战》这种2D游戏中最常用的是“矩形碰撞检测”和“圆形碰撞检测”。矩形碰撞检测将每个游戏对象飞机、子弹用一个矩形边界框Bounding Box包围。判断两个矩形是否相交只需要比较它们的坐标即可。这种方法计算简单效率高虽然精度稍差对于非矩形的图形但对于我们的像素风游戏完全够用。bool CheckRectCollision(int x1, int y1, int w1, int h1, int x2, int y2, int w2, int h2) { // 判断矩形1(x1,y1,w1,h1)和矩形2(x2,y2,w2,h2)是否相交 return !(x1 w1 x2 || x2 w2 x1 || y1 h1 y2 || y2 h2 y1); }圆形碰撞检测将对象视为圆形判断两个圆心之间的距离是否小于半径之和。这种方法更适合圆形物体计算涉及开方效率稍低。在我们的游戏中对于飞机和子弹使用矩形碰撞检测是标准做法。在Update()阶段我们需要遍历所有玩家子弹和所有敌机进行碰撞检测同时也要遍历所有敌机子弹和玩家飞机。注意碰撞检测的遍历是性能敏感区域。如果有N颗子弹和M架敌机最朴素的实现需要O(N*M)次比较。当对象很多时需要考虑空间划分算法如四叉树来优化。但对于我们初学者项目几十个对象的规模直接遍历完全没问题。3. 开发环境搭建与工具准备工欲善其事必先利其器。一个顺手的开发环境能极大提升编码效率和调试体验。3.1 安装Visual Studio与EasyX图形库安装Visual Studio推荐使用Visual Studio 2022 Community版它完全免费且功能强大。在安装时务必勾选“使用C的桌面开发”工作负载。这将安装C编译器、调试器和基本的Windows开发库。下载与安装EasyX访问EasyX的官方网站下载最新版本的安装包。运行安装程序它会自动检测你系统上已安装的Visual Studio版本。关键一步在安装向导中为你使用的Visual Studio版本打上勾例如 VS 2022。安装程序会将必要的头文件(easyx.h等)和库文件复制到VS对应的目录下。验证安装打开VS新建一个空项目在源文件中输入#include graphics.h并编译如果不报错说明安装成功。graphics.h是EasyX为了兼容老语法提供的头文件内部包含了easyx.h。3.2 创建第一个EasyX图形窗口项目打开Visual Studio 2022按照以下步骤操作选择“创建新项目” - “空项目” - 输入项目名称如“PlaneWar”和位置。在“解决方案资源管理器”中右键点击“源文件”文件夹 - “添加” - “新建项”创建一个名为main.cpp的C文件。在main.cpp中输入以下测试代码#include graphics.h // 引用EasyX图形库 #include conio.h // 用于_getch()函数 int main() { initgraph(640, 480); // 初始化一个640x480的图形窗口 setbkcolor(WHITE); // 设置背景色为白色 cleardevice(); // 用背景色清空屏幕 setcolor(BLACK); // 设置当前绘图颜色为黑色 circle(320, 240, 50); // 在窗口中心画一个半径为50的圆 outtextxy(280, 235, _T(Hello EasyX)); // 在圆上方输出文字 _getch(); // 按任意键继续 closegraph(); // 关闭图形窗口 return 0; }按F5编译并运行。如果弹出一个白色窗口中间有一个黑色的圆和文字恭喜你环境配置成功实操心得很多新手在这里会遇到“无法打开源文件graphics.h”的错误。99%的原因是在安装EasyX时没有为当前VS版本打勾或者安装后没有重启VS。请卸载EasyX重新安装并确保勾选正确然后重启Visual Studio再试。3.3 资源准备图片与音效一个游戏光有几何图形不够我们需要更精美的素材。你需要准备一些图片资源例如player.png玩家飞机的图片。enemy1.png,enemy2.png不同种类敌机的图片。bullet.png子弹的图片。explosion1.png,explosion2.png...爆炸动画的序列帧图片。background.png游戏背景图可选。将这些图片文件放在你项目目录下的一个文件夹里比如res/。EasyX提供了loadimage()函数来加载常见格式如PNG, BMP, JPG的图片。对于音效EasyX本身不支持音频播放。我们可以使用Windows APIPlaySound()或者更强大的库如FMOD、irrKlang。为了简化第一个版本可以暂时不加音效专注于游戏逻辑和图形。4. 核心类实现详解现在我们进入具体的代码实现阶段。我们将按照之前的设计逐个实现核心类。4.1 玩家飞机类 (CPlayer) 的实现玩家飞机是游戏的核心受控单位。我们需要它能响应键盘操作在屏幕底部移动并发射子弹。// player.h #pragma once #include graphics.h #include “bullet.h” // 因为要发射子弹需要知道子弹类 class CPlayer { private: int m_x, m_y; // 飞机中心坐标 int m_width, m_height; // 飞机图片的宽高用于碰撞检测 int m_speed; // 移动速度像素/帧 int m_hp; // 生命值 IMAGE m_imgPlayer; // 飞机图片对象 std::vectorCBullet m_vecBullets; // 当前发射出的所有子弹 public: CPlayer(); void Init(int x, int y, int speed 5, int hp 3); void LoadImage(const TCHAR* path); // 加载图片 void Move(int dirX, int dirY); // 移动dirX/Y为-1,0,1 void Shoot(); // 发射子弹 void Update(); // 更新子弹状态 void Draw(); // 绘制飞机和子弹 void Hit(int damage); // 被击中 bool IsAlive() const { return m_hp 0; } int GetX() const { return m_x; } int GetY() const { return m_y; } int GetWidth() const { return m_width; } int GetHeight() const { return m_height; } std::vectorCBullet GetBullets() { return m_vecBullets; } // ... 其他方法如获取生命值等 };在Move方法中需要处理边界检测防止飞机移出屏幕// player.cpp void CPlayer::Move(int dirX, int dirY) { m_x dirX * m_speed; m_y dirY * m_speed; // 边界检查假设屏幕宽WIDTH高HEIGHT const int leftBound m_width / 2; const int rightBound WIDTH - m_width / 2; const int topBound m_height / 2; const int bottomBound HEIGHT - m_height / 2; if (m_x leftBound) m_x leftBound; if (m_x rightBound) m_x rightBound; if (m_y topBound) m_y topBound; if (m_y bottomBound) m_y bottomBound; }Shoot方法负责创建一颗新的子弹并将其加入到子弹列表中。子弹的初始位置通常是飞机头部m_x, m_y - m_height/2方向向上。Update方法需要遍历m_vecBullets调用每个子弹的Update方法并移除那些已经飞出屏幕的子弹isOut()返回true。这里就涉及到对vector的遍历和删除操作需要注意迭代器失效的问题。避坑技巧在遍历容器并删除元素时推荐使用“擦除-移除”惯用法Erase-Remove Idiom或者从后向前遍历。对于vectorCBullet一个简单安全的做法是for (auto it m_vecBullets.begin(); it ! m_vecBullets.end(); ) { it-Update(); if (it-IsOut()) { it m_vecBullets.erase(it); // erase返回下一个有效迭代器 } else { it; } }4.2 子弹类 (CBullet) 与敌机类 (CEnemy) 的实现子弹类的实现相对简单核心是位置更新和绘制。// bullet.h class CBullet { private: int m_x, m_y; int m_speedX, m_speedY; // 速度分量支持斜向飞行 int m_power; // 攻击力 bool m_isFromPlayer; // true为玩家子弹false为敌机子弹 IMAGE m_imgBullet; public: void Init(int x, int y, int speedX, int speedY, int power, bool fromPlayer); void Update(); void Draw(); bool IsOut() const; // 判断是否飞出屏幕 // 获取碰撞矩形 RECT GetCollisionRect() const; };敌机类则更为复杂因为它可能有不同的行为模式直线下落、正弦波移动、追逐玩家等。我们可以先实现一个基础敌机类然后通过继承创建变种。// enemy.h class CEnemy { protected: int m_x, m_y; int m_speed; int m_hp; int m_score; // 击落后获得的分数 IMAGE m_imgEnemy; enum State { ALIVE, EXPLODING, DEAD } m_state; // 状态机 int m_explosionFrame; // 爆炸动画当前帧 public: virtual void Init(int x, int y, int speed, int hp, int score); virtual void Update(); // 虚函数允许子类重写移动逻辑 virtual void Draw(); void Hit(int damage); bool IsDead() const { return m_state DEAD; } RECT GetCollisionRect() const; int GetScore() const { return m_score; } }; // 实现一个最简单的直线下落敌机 void CEnemy::Update() { if (m_state ALIVE) { m_y m_speed; // 简单向下移动 if (m_y HEIGHT) { // 飞出屏幕底部则标记为死亡 m_state DEAD; } } else if (m_state EXPLODING) { m_explosionFrame; if (m_explosionFrame 15) { // 假设爆炸动画有15帧 m_state DEAD; } } }4.3 游戏管理类 (CGame) 与主循环搭建游戏管理类是粘合剂它将所有模块组合在一起并驱动游戏循环。// game.h class CGame { private: CPlayer m_player; std::vectorstd::unique_ptrCEnemy m_vecEnemies; // 使用智能指针管理敌机 std::vectorCExplosion m_vecExplosions; int m_score; int m_level; bool m_isGameOver; IMAGE m_bgImg; // 背景图片 // 敌机生成相关 DWORD m_lastEnemyGenTime; // 上次生成敌机的时间 int m_enemyGenInterval; // 敌机生成间隔毫秒 public: CGame(); bool Init(); // 初始化游戏资源 void Run(); // 运行游戏主循环 void HandleInput(); void Update(); void Render(); void SpawnEnemy(); // 生成新敌机 void CheckCollisions(); // 碰撞检测 void GameOver(); // ... 其他方法 };主循环Run()是游戏的心脏void CGame::Run() { DWORD lastFrameTime GetTickCount(); // 记录上一帧时间用于控制帧率 while (!m_isGameOver) { DWORD currentTime GetTickCount(); DWORD deltaTime currentTime - lastFrameTime; // 计算帧间隔 // 处理输入 HandleInput(); // 更新游戏状态 Update(); // 渲染 Render(); // 控制帧率确保稳定的游戏体验 // 如果一帧处理太快小于16ms则休眠剩余时间目标约60FPS DWORD frameTime GetTickCount() - currentTime; if (frameTime 16) { Sleep(16 - frameTime); } lastFrameTime currentTime; } // 游戏结束显示分数等 GameOver(); }在Update()函数中我们需要按顺序调用m_player.Update()更新玩家子弹。遍历更新所有敌机。调用SpawnEnemy()根据时间间隔生成新敌机。调用CheckCollisions()进行所有碰撞检测。更新爆炸效果动画。CheckCollisions()是逻辑最密集的地方之一void CGame::CheckCollisions() { // 1. 玩家子弹 vs 敌机 auto playerBullets m_player.GetBullets(); for (auto bullet : playerBullets) { for (auto it m_vecEnemies.begin(); it ! m_vecEnemies.end(); it) { auto enemy *it; if (!enemy-IsDead() CheckRectCollision(bullet.GetCollisionRect(), enemy-GetCollisionRect())) { // 发生碰撞 enemy-Hit(bullet.GetPower()); bullet.MarkForRemoval(); // 标记子弹为待移除 if (enemy-IsDead()) { m_score enemy-GetScore(); // 创建爆炸效果 m_vecExplosions.emplace_back(enemy-GetX(), enemy-GetY()); } break; // 一颗子弹只能击中一个敌机 } } } // 移除被标记的子弹在Player.Update中或这里统一处理 // 2. 敌机 vs 玩家 for (auto enemy : m_vecEnemies) { if (!enemy-IsDead() CheckRectCollision(enemy-GetCollisionRect(), m_player.GetCollisionRect())) { m_player.Hit(1); // 玩家扣血 enemy-Hit(999); // 敌机撞毁 // ... 创建爆炸 if (!m_player.IsAlive()) { m_isGameOver true; } } } // 3. 敌机子弹 vs 玩家 (如果敌机会射击) // ... 逻辑类似 }5. 图形渲染与用户体验优化游戏逻辑跑通后我们需要让画面看起来更舒服玩起来更流畅。5.1 双缓冲与画面闪烁消除如果你直接在屏幕上绘图当画面复杂时会看到明显的闪烁。这是因为你在用户看到的过程中直接修改了屏幕像素。解决方案是双缓冲。原理在内存中创建一个和屏幕画布一样大小的“缓冲区图像”。所有绘图操作都先在这个缓冲区上进行。当一整帧画面完全绘制好后再一次性将这个缓冲区的内容“贴”到屏幕上。这个过程瞬间完成用户看不到中间绘制过程。EasyX实现EasyX默认就启用了双缓冲。你通过BeginBatchDraw()开始批量绘图所有的line,circle,putimage等操作都只在内存缓冲中进行最后调用FlushBatchDraw()一次性刷新到屏幕。或者更简单的是EasyX在cleardevice()和EndBatchDraw()之间会自动管理双缓冲。我们通常的绘图模式是cleardevice(); // 清空缓冲区 // ... 绘制背景、飞机、子弹、敌机、分数等 FlushBatchDraw(); // 将缓冲区内容刷新到屏幕并开启下一帧的缓冲 Sleep(10); // 短暂休眠控制帧率5.2 透明背景图片处理我们加载的飞机、子弹PNG图片通常带有透明背景。EasyX的putimage函数默认是不处理透明色的。我们需要使用putimage的重载版本并指定透明色比如纯黑色BLACK或者使用putimage_withalpha函数如果EasyX版本支持。一个更通用和强大的方法是使用IMAGEDATA和AlphaBlend函数。但为了简单起见我们可以在制作图片时将背景设为一种游戏中不可能用到的颜色如品红色RGB(255, 0, 255)然后在绘制时使用transparentimage函数指定透明色。// 假设 m_imgPlayer 是已加载的图片 void CPlayer::Draw() { // 将图片中品红色(RGB(255,0,255))的部分设为透明 transparentimage(NULL, m_x - m_width/2, m_y - m_height/2, m_imgPlayer, RGB(255, 0, 255)); }5.3 分数显示、生命值UI与游戏状态提示游戏需要反馈。我们需要在屏幕的固定位置通常是角落绘制文字信息。void CGame::RenderUI() { settextcolor(WHITE); // 设置文字颜色 settextstyle(20, 0, _T(“宋体”)); // 设置文字高度、宽度0为自适应、字体 TCHAR scoreStr[32]; _stprintf_s(scoreStr, _T(“得分: %d”), m_score); outtextxy(10, 10, scoreStr); TCHAR hpStr[32]; _stprintf_s(hpStr, _T(“生命: %d”), m_player.GetHp()); outtextxy(10, 40, hpStr); if (m_isGameOver) { settextstyle(40, 0, _T(“黑体”)); settextcolor(RED); outtextxy(WIDTH/2 - 100, HEIGHT/2 - 20, _T(“游戏结束”)); settextstyle(20, 0, _T(“宋体”)); _stprintf_s(scoreStr, _T(“最终得分: %d”), m_score); outtextxy(WIDTH/2 - 80, HEIGHT/2 30, scoreStr); } }6. 性能优化与高级功能拓展当基础版本运行流畅后你可以考虑以下优化和扩展让游戏更专业、更好玩。6.1 对象池优化避免频繁new/delete在游戏中子弹和敌机会被频繁创建和销毁。如果每一帧都使用new和delete或vector的频繁push_back和erase会导致内存碎片和性能下降。对象池Object Pool是一种经典的优化模式。思路在游戏初始化时预先创建一大批子弹对象和敌机对象并将它们置入“空闲池”。当需要发射子弹时从空闲池中取出一个对象初始化其状态并将其放入“活跃列表”。当子弹飞出屏幕或击中目标后不是删除它而是将其状态重置并放回“空闲池”。好处完全避免了运行时动态内存分配的开销运行速度更快内存使用更稳定。实现可以用两个vector或list分别管理空闲对象和活跃对象。也可以使用一个数组固定大小和状态标志位来实现。6.2 状态模式管理复杂的游戏流程当前我们可能用一堆bool标志如m_isPlaying,m_isPaused,m_isGameOver来控制游戏状态然后在主循环里用大量的if-else判断。当状态增多时如开始菜单、设置界面、关卡选择、游戏进行、暂停、结束代码会变得难以维护。状态模式将每个状态如“菜单状态”、“游戏状态”、“暂停状态”封装成一个独立的类它们都继承自一个共同的“状态”基类基类定义了Enter,Exit,Update,Render,HandleInput等虚函数。游戏管理器持有一个指向当前状态对象的指针。当需要切换状态时如按下开始键游戏管理器调用当前状态的Exit然后创建新的状态对象调用其Enter并切换指针。好处每个状态的逻辑被隔离代码清晰增加新状态非常容易。6.3 扩展游戏性道具系统与关卡设计为了让游戏更有趣可以加入以下元素道具系统敌机被击落后有概率掉落道具如加生命、火力升级、清屏炸弹。实现一个CProp类它像子弹一样下落。玩家飞机接触到道具后触发效果。这需要扩展CPlayer的状态如“火力升级”持续10秒。多种敌机与Boss战继承CEnemy创建更多子类。比如CSineEnemy左右正弦移动CHomingEnemy会缓慢追踪玩家CBossEnemy拥有多段生命值和独特的攻击模式如扇形子弹、召唤小怪。Boss的出现可以关联到分数或时间。关卡与难度递增在CGame类中维护一个关卡变量m_level。随着分数增加关卡提升。更高的关卡可以1) 缩短敌机生成间隔2) 提高敌机速度3) 增加新类型敌机的出现概率4) Boss在特定关卡出现。粒子系统让爆炸效果更炫酷。实现一个简单的粒子系统爆炸时生成数十个小的、颜色、速度、生命周期各异的粒子模拟火花和烟雾。这能极大提升视觉表现力。7. 调试技巧与常见问题排查开发过程中你一定会遇到各种Bug。掌握有效的调试方法至关重要。7.1 可视化调试绘制碰撞框与调试信息当碰撞检测不生效时最直观的方法是将碰撞框画出来。void CGame::RenderDebugInfo() { // 绘制玩家碰撞框 RECT rcPlayer m_player.GetCollisionRect(); rectangle(rcPlayer.left, rcPlayer.top, rcPlayer.right, rcPlayer.bottom); // 绘制所有敌机碰撞框 setlinecolor(YELLOW); for (auto enemy : m_vecEnemies) { if (!enemy-IsDead()) { RECT rcEnemy enemy-GetCollisionRect(); rectangle(rcEnemy.left, rcEnemy.top, rcEnemy.right, rcEnemy.bottom); } } // 绘制所有子弹碰撞框 setlinecolor(GREEN); for (auto bullet : m_player.GetBullets()) { RECT rcBullet bullet.GetCollisionRect(); rectangle(rcBullet.left, rcBullet.top, rcBullet.right, rcBullet.bottom); } }在开发阶段你可以在Render()函数的最后调用这个方法就能清晰地看到每个物体的边界快速定位是碰撞框计算错误还是检测逻辑有问题。7.2 常见编译与运行时错误“error LNK2019: 无法解析的外部符号...”这是最常见的链接错误。意味着你的.cpp文件实现了某个函数但在编译成.obj文件后链接器在其他的.obj文件里找不到这个函数的定义。检查函数声明在.h文件里和定义在.cpp文件里的签名是否完全一致包括参数类型、const修饰符。实现该函数的.cpp文件是否被添加到了项目中参与编译。如果你使用了第三方库如EasyX是否在项目属性中正确配置了附加库目录和附加依赖项。对于EasyX通常安装后自动配置好无需手动设置。游戏运行卡顿、闪烁严重卡顿检查你的Update和Render逻辑是否过于复杂。特别是嵌套循环的复杂度。使用对象池优化对象管理。确保Sleep或帧率控制逻辑正确避免循环空转占用100%CPU。闪烁确认你正确使用了双缓冲。确保绘图顺序是cleardevice()- 绘制所有内容 -FlushBatchDraw()。不要在FlushBatchDraw()之后还进行零星的绘图操作。图片加载失败显示为黑色方块检查图片路径是否正确。在VS中默认的“工作目录”是项目文件夹.vcxproj文件所在目录而不是.exe输出目录。最稳妥的方式是使用相对路径并将资源文件夹res/复制到.exe的同级目录下。或者使用绝对路径进行测试。检查图片格式。EasyX支持的格式可能因版本而异确保是BMP、PNG、JPG等常见格式。键盘输入响应不灵敏或“粘键”EasyX的GetAsyncKeyState或_kbhit_getch在循环中可能太快。确保你的主循环中有适当的延时如Sleep(10)。对于连续移动建议使用GetAsyncKeyState来检测按键是否被“按住”而不是单次按下。同时处理按键时考虑“按键消抖”和“按键重复”的体验。7.3 内存泄漏检查虽然对于小型项目程序结束操作系统会回收内存但养成好习惯很重要。确保所有new的对象都有对应的delete。使用std::unique_ptr或std::shared_ptr等智能指针可以自动管理内存是现代C推荐的做法。如果你在CGame中用vectorCEnemy*存储敌机记得在析构函数或移除敌机时delete它们。更好的做法是使用vectorstd::unique_ptrCEnemy。完成这个《飞机大战》项目你收获的远不止一个可以运行的游戏。你实践了C的核心语法和面向对象思想理解了游戏开发的基本架构和核心循环掌握了图形编程、资源管理、碰撞检测等实用技能。更重要的是你拥有了将复杂想法分解、设计并最终用代码实现的能力。接下来你可以以此为蓝本添加更多自己的想法更华丽的弹幕、更智能的敌人、联机对战功能或者用同样的思路去尝试制作《贪吃蛇》、《俄罗斯方块》、《打砖块》等其他经典游戏。编程的世界从此在你手中变得生动而具体。