简介:一份面向计算机相关专业在校生与初学者的Qt/C++课程设计源码,实现简易版植物大战僵尸游戏。项目覆盖主界面渲染、植物种植、子弹发射、僵尸生成与移动、阳光收集等核心玩法,界面与逻辑分包组织,结构便于理解;可用作课程设计、毕业设计或项目初期演示,也适合在此基础上按需扩展功能。压缩包共37个文件,以17个cpp源文件和16个h头文件为主,内含pro工程配置、qrc资源与README说明;植物、豌豆、僵尸、阳光、冷却等模块按类拆分,线程控制与音效处理也单独放置,整体仅32KB,轻量且便于快速阅读调试,目录结构清晰。目前已有536人学习下载,代码已测试运行通过,功能可用;解压后可直接编译运行,完整工程便于课程设计答辩演示,也适合二次开发扩展。
1. 课程设计基于Qt和C++框架编写的简易植物大战僵尸游戏源码.zip,值得花一个周末吃透
看到这个标题还愿意点进来的,多半是正在为C++课程设计发愁的人:命令行通讯录、图书管理系统写得再好,答辩时也讲不出十分钟。这个项目把“植物大战僵尸”的核心循环——阳光产出、植物种植、僵尸生成、子弹碰撞——全部收进一个Qt Widgets工程里,用类结构去映射玩法,用信号与槽去接事件,代码量不大但五脏俱全。适合大二大三相承课设,也适合Qt初学者把官方demo换成这套更完整的源码去捋清对象生命周期和绘图流程。如果你只想“跑起来截图交差”,这方向反而绕;但如果你想在答辩时让老师看到你对框架、封装、游戏循环的理解,这一套源码值得反复看三遍。
2. 为什么是Qt不是别家:选型理由拆解与工程内部分层
2.1 Qt Widgets还是Qt QML:课程设计最不会被答辩追问的选型理由
植物大战僵尸这类2D塔防,摆在面前的第一条路就是Qt里的两套UI体系:Qt Widgets和Qt QML。网上不少人推荐Qt QML,理由是动画顺滑、界面代码短,甚至有人把它和Qt MVVM框架扯到一起写大架构。我的观点很直接:课程设计用Qt Widgets,别上QML。QML的Canvas和Behavior做僵尸摇摆确实快,但答辩老师一问“C++侧怎么跟QML通信”,你就得解释Q_PROPERTY、contextProperty、信号槽桥接,复杂度翻一倍。Qt Widgets把逻辑用C++类写完,回调逻辑全在一处,调试时一个断点能看完全部状态。
还有一个现实原因:大部分课程设计环境是Qt 5.15.X加一个MSVC或者MinGW套件,Qt Widgets的兼容性最稳。QML如果碰到显卡驱动问题,花一下午查环境都有可能。用QWidget做游戏视图,重写paintEvent自己画植物格子、僵尸血条、子弹移动,每一帧的状态都在掌控中,这种“黑匣子暴露在你面前”的感觉,恰恰是答辩时最有底气的资本。
2.2 源码里的“框架”二字指什么:八类模块各司其职
标题里最容易被人忽略的词是“框架”。它不是指某个现成的应用框架,而是这套源码自带的工程骨架。拿到手你基本会看到这样的分工:
| 模块 | 职责 | 对应游戏机制 |
|---|---|---|
| main.cpp | 启动入口,创建游戏窗口 | 程序入口 |
| GameController | 维护游戏状态、计分、胜负 | 游戏规则 |
| GameWidget | 绘图画布、鼠标事件、定时器 | 画面与交互 |
| Plant基类 | 植物属性、冷却、发射逻辑 | 豌豆射手/向日葵 |
| Zombie基类 | 血量、移动、啃食逻辑 | 普通僵尸/路障僵尸 |
| Bullet类 | 子弹属性、伤害、方向 | 豌豆子弹 |
| SunObject类 | 阳光的产生、点击收集 | 经济系统 |
| resources.qrc | 打包图片和音频 | 贴图与音效 |
这个分层思路比游戏本身更重要:玩法逻辑在Controller,渲染交互在Widget,实体对象是普通C++类。答辩时你只要说一句“我把每个可独立测试的对象拆成一个类”,老师就能看出你不是直接复制粘贴的。很多初学者会犯的错是把所有代码堆进一个QMainWindow里,最后updateGame函数写两百行,排错时根本无从下手。按这套模块拆,至少改僵尸速度、加新品种植物时,不需要动绘图代码。
2.3 用信号与槽管理游戏事件,而不是全局变量和if嵌套
Qt框架里最值得写进答辩讲稿的,就是信号与槽。很多课设项目一到“僵尸死亡后加一分”“植物被啃完界面要刷新”就开始用全局变量传状态,代码越写越像蜘蛛网。Qt的做法是事件发生方只管发信号,关心这件事的类自己决定要不要响应。
// 子弹击中僵尸,由Controller发出信号 class GameController : public QObject { Q_OBJECT public: void onBulletHitZombie(Bullet* bullet, Zombie* zombie) { zombie->takeDamage(bullet->damage()); if (zombie->hp() <= 0) { zombie->setDead(true); emit zombieKilled(zombie->rewardSun()); } } signals: void zombieKilled(int sunValue); void gameOverByZombie(); void waveChanged(int currentWave); };关键点在于emit只负责“通知”,GameWidget可以在构造时把僵尸死亡信号连到自己的刷新槽函数上,GameController完全不需要知道界面长什么样。这样如果以后你想把界面换成绩单显示或日志输出,只需要新增一个连接。信号与槽机制本身有轻微性能损耗,但一个简单游戏一帧也就几十个对象,损耗可以忽略,换来的是结构清晰度。答辩现场如果有老师问“你这里为什么不用回调函数”,你就可以回答:回调在跨线程和对象销毁时容易变成悬垂指针,信号与槽在连接断开上更安全。
3. 从空工程到能跑的最小demo:手把手代码落地
3.1 最小类继承设计:从QWidget到游戏画布只有三步
拿到源码先别急着全盘看懂,我建议按“画得出→动得起来→打得死人”三步还原。第一步只做一个能画草地的QWidget子类。Qt里所有窗口都可以当作画布,关键是重写paintEvent,用QPainter绘制背景和格子线。这里有个很容易绕弯的点:不要把每个植物都设计成QLabel放上去,那会让你后面处理碰撞检测时到处找geometry。正确做法是自定义数据结构存在内存里,paintEvent里统一绘制。
// GameWidget.h #ifndef GAMEWIDGET_H #define GAMEWIDGET_H #include <QWidget> #include <QList> #include "Plant.h" #include "Zombie.h" #include "Bullet.h" class GameController; class GameWidget : public QWidget { Q_OBJECT public: explicit GameWidget(QWidget* parent = nullptr); void setController(GameController* controller); protected: void paintEvent(QPaintEvent* event) override; void mousePressEvent(QMouseEvent* event) override; void timerEvent(QTimerEvent* event) override; private: void updateGameFrame(); void drawGrass(QPainter& painter); void drawPlants(QPainter& painter); void drawZombies(QPainter& painter); void drawBullets(QPainter& painter); QList<Plant*> m_plants; QList<Zombie*> m_zombies; QList<Bullet*> m_bullets; GameController* m_controller; int m_gameTimerId = -1; }; #endif构造时启动定时器,常用的做法是用QTimer配合事件循环,也可以用原生timerEvent。用timerEvent少include一个头文件,但QTimer更灵活,可以在游戏暂停时stop。我这里倾向QTimer,因为它能挂在任意对象身上,lambda槽写起来方便。最终选择哪种取决于你源码里原本怎么组织,不需要两边都写。
3.2 游戏主循环:QTimer驱动帧更新与对象状态推进
游戏循环是这个项目的发动机。植物大战僵尸不像贪吃蛇那样等玩家按键才动,它需要每隔几十毫秒推进一次僵尸位置、子弹飞行、阳光倒计时。Qt里没有专门的GameLoop类,你要自己用QTimer模拟。每秒30帧就是33毫秒回调一次,帧率太低僵尸一卡一卡,帧率太高CPU占用翻倍。我这个项目里会把m_tickMs定义成33,留一个常量方便改。
// 构造函数中启动游戏循环 GameWidget::GameWidget(QWidget* parent) : QWidget(parent), m_controller(nullptr) { setFixedSize(GRID_COLS * CELL_W, GRID_ROWS * CELL_H + TOOLBAR_H); QTimer* timer = new QTimer(this); connect(timer, &QTimer::timeout, this, &GameWidget::updateGameFrame); timer->start(m_tickMs); // m_tickMs = 33,约30fps } void GameWidget::updateGameFrame() { // 1. 自然生成阳光,按累计时间触发 m_sunAccumulator += m_tickMs; if (m_sunAccumulator >= m_sunIntervalMs) { spawnSun(); m_sunAccumulator -= m_sunIntervalMs; } // 2. 植物按射速发射子弹,每棵植物有独立冷却 for (Plant* plant : std::as_const(m_plants)) { if (plant->isReadyToShoot()) { Bullet* bullet = new Bullet(plant->bulletSpawnPos(), plant->bulletDamage()); m_bullets.append(bullet); plant->resetShootCooldown(); } } // 3. 子弹向右侧移动,僵尸向左侧移动 for (Bullet* bullet : std::as_const(m_bullets)) { bullet->setX(bullet->x() + m_bulletSpeed * m_tickMs / 1000.0); } for (Zombie* zombie : std::as_const(m_zombies)) { zombie->setX(zombie->x() - m_zombieSpeed * m_tickMs / 1000.0); } // 4. 统一处理碰撞 handleCollisions(); // 5. 清理死亡对象 cleanDeadObjects(); // 触发重绘,paintEvent会重新绘制全部对象 update(); }这里有两个值得答辩细讲的参数:一个是m_bulletSpeed与m_tickMs的关系,子弹每帧位移如果直接用固定像素,切换帧率时速度会变化;我习惯把速度定义成“每秒移动像素数”,再乘上tick时间换算,这样改帧率不影响手感。另一个是m_sunAccumulator,它用累加器模式而不是计数器递减,避免定时器累积误差。真实计时器可能在系统繁忙时偶尔延迟几十毫秒,用累加器能自动消化这些误差,比单纯数帧数稳定得多。
3.3 从鼠标点击到种植:网格坐标换算与合法性判断
植物大战僵尸的种植交互,本质是“窗口像素坐标”到“逻辑网格坐标”的换算。鼠标点击在屏幕上是(x, y),而格子是行列号,中间隔着一层单元格宽度和高度的除法。新手最容易翻车的是忘记减去顶部工具栏的高度,导致点击第一排却种到第二排。
void GameWidget::mousePressEvent(QMouseEvent* event) { if (m_gameOver) return; int x = event->pos().x(); int y = event->pos().y() - TOOLBAR_H; // 工具栏偏移 // 点中顶部工具栏或超出边界则忽略 if (y < 0 || x < 0 || x >= GRID_COLS * CELL_W || y >= GRID_ROWS * CELL_H) { return; } int col = x / CELL_W; int row = y / CELL_H; // 点击范围贴近格子的算格子,否则可能误触 if (m_selectedPlantType == PlantType::None) { // 未选中植物,可能是在采阳光 emit clickForSun(QPoint(col * CELL_W, row * CELL_H)); return; } if (m_grid[row][col]) { // 已有植物,提示不可种植 return; } emit plantSelectedToPlace(m_selectedPlantType, row, col); m_selectedPlantType = PlantType::None; }这套代码的关键是“选中植物后再点击格子”。实际项目里通常有一个工具栏区域显示卡片,卡片被点击后记录m_selectedPlantType,然后等待鼠标点击地图。要注意的是QMouseEvent的坐标是相对当前控件的,如果你的游戏窗口嵌在更大的界面里,先event->pos()再用mapTo如果没有特殊布局就够用了。阳光折叠的点击收集同样走这个流程,只是不需要网格判定,直接用QRectF::contains判断点击位置是否落在阳光贴图区域内。
3.4 碰撞检测与子弹生命周期:谁负责删对象
早期demo里最容易出Bug的就是对象清理:僵尸被子弹打死,但子弹还在飞行;植物被啃光,僵尸还站在原地继续啃空气。这里我要先说一句血泪经验:不要在遍历QList的循环里直接delete元素,迭代器会失效,下一秒就崩给你看。统一做法是把死亡对象标记出来,等遍历结束再统一清理。
void GameWidget::handleCollisions() { // 子弹与僵尸的碰撞 for (Bullet* bullet : std::as_const(m_bullets)) { if (bullet->isDead()) continue; QRectF bulletRect = bullet->collisionRect(); for (Zombie* zombie : std::as_const(m_zombies)) { if (zombie->isDead()) continue; if (bulletRect.intersects(zombie->collisionRect())) { zombie->takeDamage(bullet->damage()); bullet->setDead(true); // 一颗子弹只命中一个僵尸 if (zombie->hp() <= 0) { zombie->setDead(true); } break; } } } // 僵尸与植物的碰撞:僵尸停下啃咬 for (Zombie* zombie : std::as_const(m_zombies)) { if (zombie->isDead()) continue; for (Plant* plant : std::as_const(m_plants)) { if (plant->isDead()) continue; if (zombie->collisionRect().intersects(plant->collisionRect())) { zombie->setEating(true); // 停止移动 plant->takeDamage(zombie->damagePerBite()); if (plant->hp() <= 0) { plant->setDead(true); zombie->setEating(false); } break; } } } }cleanDeadObjects函数里只要遍历三张表,把m_plants[m_plants.size() - 1]等尾元素与死亡对象交换后pop_back,避免删中间元素导致大量移动。如果对象是由new创建的,别忘了delete,但注意父对象链:如果一个对象设置了QObject父对象,delete会由父析构统一执行,如果你又手动delete,就可能双重释放。我的经验是实体类不继承QObject,让GameWidget统一持有并统一删除,这样生命周期最直观。
4. 课程设计最常翻车的六个坑:现象、原因、解决
4.1 依赖路径报错::-1: error: dependent '..\..\..\..\..\..\qt\5.15.2\msvc2019_64\include\qtwidgets' does not exist
现象:用Qt Creator打开源码后,构建输出面板第一行就出现上面这种以:-1: error: dependent开头的红色错误,项目完全无法编译。
原因:这个报错不是代码问题,是Qt Creator的构建配置里套件与项目文件包含了错误的依赖路径。常见诱因包括压缩包解压后路径含中文或空格、别人提交的.pro文件里写死了绝对路径、或者本机装的Qt版本和源码作者用的完全不同。MSVC2019_64套件只认对应编译版本的Qt库。
解决:先看左侧项目模式里的Kits列表,确认当前用的编译器是MSVC还是MinGW,然后打开.pro文件,检查里面有没有INCLUDEPATH或DEPENDPATH指向了Qt安装目录。正常Qt工程不需要手动添加Qt自身include路径,那些路径由套件自动注入。如果.pro里出现了类似D:/Qt/5.15.2/...的写死路径,删掉后重新qmake。最后,把整个项目放到纯英文目录下再试一次,中文路径在MSVC套件下经常让依赖解析失效。
4.2 图片和音效全部空白:qrc资源系统加载失败
现象:程序编译通过、窗口弹出来了,但草坪是黑的,植物贴图不显示,点击也没有音效。
原因:资源没有被打进可执行文件。Qt的资源分两种路径:一种是硬盘绝对路径,一种是qrc资源路径:/images/peashooter.png。课程设计被网上素材坑过的人很多,直接把图片路径写在代码里,程序一换目录就找不到文件;还有的人qrc文件里路径前缀写错,比如实际是:/res/image,代码里写:/image。
解决:右键项目添加Qt Resource File,把存放贴图的目录加进去。代码里统一用冒号开头访问,不要用相对路径。另外注意qrc文件的编码,如果图片文件名是中文,在某些编译器下会乱码,建议所有素材改成英文命名。
4.3 游戏运行十几秒后随机崩溃:对象被提前delete
现象:可以正常种下几棵植物,僵尸一出来,子弹一碰到僵尸,程序退出并报Segmentation fault。
原因:典型生命周期管理失误。比如子弹的碰撞检测里调用了delete bullet,但同一轮循环后面又访问bullet;或者僵尸被清掉后,GameController里还保留着指向僵尸的指针,下一次刷新时又用它。Qt智能指针确实用起来省心,但在普通C++容器里混用裸指针和智能指针反而更复杂。
解决:所有从容器移除的对象统一走markDead标记,循环结束后在cleanDeadObjects里统一delete并removeOne。如果对象本身是QObject子类,可以考虑deleteLater(),它会等事件循环回到安全点再释放,避免“正在处理信号时对象被销毁”。
4.4 报错“Qt版本与编译器不匹配”:找不到MSVC2019_64工具链
现象:代码看起来没问题,但构建时报cannot find -lQt5Widgetsd或unknown option -Wl这样的链接错误。
原因:Qt安装时选择的编译器版本跟当前套件不匹配。比如机器上装了支持MSVC2019_64的Qt 5.15.2,但编译器组件没完全安装;或者反过来,MinGW套件尝试链接MSVC编译的Qt库。
解决:如果坚持用MSVC套件,打开Qt安装目录维护工具,勾选对应版本的MSVC2019_64组件。如果只是课设只要能跑,最快是切换到MinGW套件,把.pro里的QT += widgets确认无误后直接重新构建。两种编译器生成的库文件格式不同,混用必炸,这个没有后悔药,看清套件再动手。
4.5 游戏速度忽快忽慢:QTimer误差没有补偿
现象:植物射速一开始正常,运行两分钟后明显变快,或者画面一顿一顿。
原因:QTimer的触发间隔不是绝对精确的,尤其在后台有垃圾回收或系统负载时,实际触发间隔会比设定值大。如果你用“每次timeout就让冷却时间减1”这种计数方式,积累误差会越滚越大。我见过把m_tickMs设成10毫秒的,系统一卡,定时器积压,timeout连续触发多次,植物瞬间射出满天子弹。
解决:用QElapsedTimer记录真实流逝时间,每次game frame计算elapsedTime = m_elapsedTimer.restart(),然后所有速度都按elapsedTime换算。代码里就把m_tickMs当成最大步长,实际移动距离是速度乘以真实毫秒数。游戏想要稳定,宁可画面掉帧也不能让逻辑时间凭空跳跃。
4.6 高分屏下植物位置点击不准:devicePixelRatio问题
现象:在2K或4K屏幕上运行,植物被种下去的位置跟鼠标点的地方差出一截,但在普通1080p下正常。
原因:Qt在支持高DPI缩放的平台上,逻辑坐标和物理像素坐标有比例关系。如果窗口没有设置setAttribute(Qt::AA_EnableHighDpiScaling),或者代码里用了event->globalPos()再做局部坐标转换,就会拿物理坐标去套逻辑网格。
解决:在main.cpp的QApplication创建之前设置QApplication::setAttribute(Qt::AA_EnableHighDpiScaling);和setAttribute(Qt::AA_UseHighDpiPixmaps);,然后事件处理一律用event->position()而不是event->pos()。Qt 5.15里建议统一用event->position().toPoint(),Qt 6里pos()已经是浮点了。这个坑不遇到时觉得是玄学,遇到后一查DPI心里就清楚了。
5. 参数设计:植物平衡、难度曲线和存档验证
5.1 五类必调参数:从数值上让“简易版”也有手感
很多课程设计源码能玩但不好玩,就是因为在直接堆功能时忘记调数值。植物大战僵尸的底层平衡藏在五个参数组里:生产节奏、攻击节奏、僵尸强度、空间容错、时间压力。你可以在一开始全用常量,但在答辩演示前一定要把这些集中到一个配置区域。
| 参数组 | 典型默认值 | 作用 | 调整逻辑 |
|---|---|---|---|
| 阳光生成间隔 | 5000~7000ms | 决定经济节奏 | 缩短到3000ms游戏偏简单,拉长到9000ms需要更多向日葵 |
| 植物射速 | 每1.5s一发 | 决定防线强度 | 豌豆升到0.8s会使僵尸很难推进 |
| 僵尸移动速度 | 15~30px/s | 决定关卡压力 | 波动超过50px/s会感觉瞬移,不真实 |
| 僵尸血量 | 80~200 | 决定子弹命中次数 | 普通僵尸3颗子弹死,路障僵尸8颗子弹死,这个比例比较合理 |
| 关卡波次间隔 | 10~20s | 决定是否手忙脚乱 | 间隔越短越紧张,适合做教学关和地狱关 |
把这些值定义成类的static constexpr成员或配置文件里读入,答辩时现场改一个参数演示游戏变化,效果远比对着代码念要好。“我能看清每个数值怎么影响手感”这句话,是老师最愿意听到的。
5.2 难度曲线:用代码而不是硬编码表
简易版常见的简陋做法是每一关硬编码一波僵尸列表,代码写起来累,演示时又没法动态变化。更好的方式是定义一个“波次生成规则”,用当前波次号驱动生成间隔、僵尸血量、数量上限。
// GameController.cpp void GameController::startNextWave() { m_wave++; // 波次越高,僵尸生成越快,但设一个下限防止变成高压无解 int spawnIntervalMs = qMax(1500, 7000 - m_wave * 300); if (m_wave % 5 == 0) { // 每五波出现一次强化,血量和数量一起涨 m_zombieHp = qMin(500, 80 + m_wave * 15); } emit waveChanged(m_wave); }用qMax/qMin把数值钳制在一个合理范围内,防止后期失控。做曲线时也别追求数学上精确的指数函数,课程设计最怕公式太复杂以至于自己解释不清。你可以在答辩时说:“这个曲线是线性的,但每5波给一个断点,让玩家感受到阶段性挑战。”一句话就能解释完,老师也满意。
5.3 存档验证:最高波次和阳光数用QSettings落地
存档功能是很多“简易版”源码故意不做的,你做了就是加分项。用QSettings存键值对比手写JSON简单得多,而且跨平台路径由系统管理,不需要自己创建配置文件。
// 保存进度 QSettings settings("MyUniversity", "PvZCourse"); settings.setValue("maxWave", m_controller->currentWave()); settings.setValue("totalSun", m_controller->totalSunCollected()); settings.sync(); // 读取进度 int maxWave = settings.value("maxWave", 1).toInt(); int totalSun = settings.value("totalSun", 0).toInt();QSettings的构造参数第一个是组织名,第二个是应用名,具体到Windows就是注册表或ini文件,Linux下是隐藏配置文件,你完全不用关心存储路径。Qt 5.15和Qt 6在这套API上没有破坏性变化,源码拿来直接用。演示的时候先打一局,关掉重开,最高波次还在,这就是实打实的功能点。
5.4 进入Qt界面的输入细节:别再傻等paintEvent过度绘制
最后一个常被忽视的调优点不是功能而是输入体验。有些源码里植物射击检测每隔一帧就遍历全图查找目标,这在小规模没问题,但如果后续加植物种类,同一行同时存在5棵植物和3个僵尸,每帧重复计算就会卡。最简单的优化是每个植物在种植时把行号绑定,射击时只遍历同行的僵尸,这个字段在Plant类构造时就填好。
// Plant构造时记录行号,避免全图僵尸列表遍历 Plant::Plant(int row, int col, PlantType type) : m_row(row), m_col(col), m_type(type), m_shootCooldownMs(0), m_hp(100) {} bool Plant::isSameRow(const Zombie* zombie) const { return zombie->row() == m_row; }同行的判断还能降低误伤逻辑复杂度,因为PVZ规则是同一行的子弹只打同一行的僵尸。界面上再补充一个当前选中的植物高亮框,用drawRect画个边框即可,这层细节会让整个游戏看起来比“源码”更像“产品”。
6. 从“跑起来”到“能答辩”:三个验证技巧和一个加分习惯
源码能跑只是开始,课设评分看的是“你究竟懂多少”。第一个验证习惯是给自己做一次“最小化复盘”:把游戏每一步的输出来到控制台,比如僵尸生成时打印行号,子弹命中时打印伤害。不要只依赖肉眼观察,因为有些Bug是概率性的,肉眼很难复现。调试时按F10单步跟一遍handleCollisions,你会发现对象状态在每一帧都合乎预期。
第二个验证思路是格式化设计:限制游戏区域固定大小,用setFixedSize代替resize,窗口不可拉伸能避开大量坐标适配问题。同时把GRID_COLS、GRID_ROWS这些常量写在类头部注释里,别人一眼能看到五列九行、单元格80x100这种边界参数。答辩现场老师会用鼠标拉窗口、快速点击、连按空格等虐待式操作,用固定的棋盘布局能稳定扛住。
第三个技巧是利用Qt Creator自带的性能分析器,不用额外配置。运行程序后切到Profiler模式,观察QWidget::paintEvent的耗时。如果单帧绘制超过5毫秒,通常意味着你把太多无用绘图塞进了paintEvent,比如每一帧重新创建QPainter对象、加载QPixmap等。把贴图在构造时预先load到成员变量里,paintEvent只做drawPixmap,性能立刻上来。
我自己的习惯是给每个类写一个debugDescription方法,返回当前状态字符串,比如植物类型、血量、冷却剩余。这个小工具在答辩演示时按下快捷键就能在控制台看到整张游戏地图的快照。它不占界面空间,但能证明你对整个数据结构的掌握程度。植物大战僵尸这种游戏,最难的不是画图,而是状态同步:植物血量和僵尸位置每帧都在变,你能把状态用文本打出来,碰到任何问题都有底。
至于加分习惯,就是养成“先看对象生命周期,再改逻辑”的排查顺序。每次游戏崩了,先在cleanDeadObjects里加断点,看谁在释放。Qt的项目里90%的崩溃都是重复delete和用悬垂指针,剩下10%才是逻辑错误。把这份源码当作一个活的调试练习,跑通它、改慢它、改快它,再去加一个新植物。真到了答辩那天,你会发现自己不是在背代码,而是在讲一个自己设计过的系统。希望帮到你。
本文还有配套的精品资源,点击获取