
简介一份基于Qt Graphics View框架的植物大战僵尸游戏完整源代码专为C程序设计期末课程设计与Qt游戏开发入门者打造可用于课程设计选题、项目参考与二次开发。资源以面向对象方法组织源码借助封装、继承和多态设计植物基类Plant、僵尸基类Zombie及商店、地图、卡片等交互对象植物侧覆盖太阳花、豌豆射手、坚果僵尸侧包含普通僵尸和路障僵尸层次清晰便于按需扩展。压缩包共111个文件大小约39.43MB主要包含25个cpp与25个h源文件、1份Pro工程文件、1份PDF开发文档并配有多张gif动图、png贴图、音频和演示视频可边看文档边对照运行效果。目前已有3828人学习下载适合需要完整课程设计源码、学习QGraphics View用法或快速搭建Qt小游戏项目的读者。 拿到这包源码很多人第一件事就是解压、打开 Qt Creator、点运行看到向日葵开始摇晃豌豆射手打碎了第一只僵尸就觉得“哦游戏能跑了任务完成”。但期末答辩大概率不会问“跑起来没有”而是会问游戏循环为什么不用 while(1)僵尸是怎么刷出来的植物被吃掉了内存谁来释放这个 C程序设计期末课程设计QT开发的植物大战僵尸游戏源代码本质不是一个游戏而是把 C 对象生命周期、多态、Qt 的信号槽、定时器、事件循环全部串起来的一份活教材。把十几份文件从“黑匣子”读到能改、能加植物、能答上老师追问才算真正拿到了它的价值。这篇就按实际跑通常走的路径把编译环境、核心代码、参数调整和典型报错一次讲透。适合正在赶课程设计、以及想用 Qt 写小游戏练手的人。2. 把源码跑起来Qt 5.15.2 安装、Kit 选择和三个必查配置2.1 安装 Qt 时组件怎么勾MinGW 还是 MSVC拿到这份源码先看工程文件。绝大部分课程设计给的是.pro工程qmake 工程少数给CMakeLists.txt。无论哪种第一关都是本机要有能编译 Qt 的编译器。课程设计场景下我一般直接选 MinGW 而不是 MSVC。理由很实际MinGW 不需要先装 Visual Studio下载体积小编译出的 exe 用 windeployqt 打包后拷到答辩机器就能跑MSVC 虽然调试器更强但经常因为电脑上 VS 版本不对、Windows SDK 缺失导致第一天就卡在环境上。这份源码如果用的是 Qt 5.15.2对应的编译器一般是 MinGW 8.1.0 64-bit两者必须匹配。Qt 5.15.2 下载安装时有个容易找不到的坑在线安装器默认不显示 5.15 这种旧版本。打开安装器后在 Select Components 界面左上角 Filter 输入框里手动敲5.15.2归档组件才会露出来。勾选时找到 Qt 5.15.2 节点下的MinGW 8.1.0 64-bit再往下翻到 Developer and Designer Tools勾选同一版本的MinGW 8.1.0 64-bit。不要图省事把整个 Qt 5.15.2 下所有组件全勾上几十 GB 装完工程实际只用得到 Qt Widgets 和 Qt GUI 两个模块。提示源码解压路径不要带中文和空格不要解压到“D:\植物大战僵尸\”。qmake 生成的 Makefile 对非 ASCII 路径的处理一直不算友好推荐解压到 D:\QtProjects\PVZ 这种纯英文路径。2.2 用 Qt Creator 打开 .pro 并完成首次构建打开 Qt Creator选择 Open Project定位到解压目录里的.pro文件。Qt Creator 会弹出一个 Kit 选择界面选带Qt 5.15.2 MinGW 64-bit字样的 Kit。Shadow Build 默认勾选意思是在源码目录之外单独生成一个 build 目录好处是不污染源码、想删就删。构建目录一般长这样build-PVZ-Desktop_Qt_5_15_2_MinGW_64_bit-Release。点左下角构建或按 CtrlB第一次编译会比较慢。观察下方的编译输出面板如果出现g: error: No such file or directory多半是路径问题如果出现cannot find -lQt5Widgets多半是 Kit 选错或者 Qt 没装全。这里要特意提一下 Debug 和 Release 的选择。Qt Creator 左下角有个构建套件选择器点开会看到 Debug、Release 两个选项。课程设计阶段很多同学一直用 Debug 写代码这没问题但答辩演示一定要切到 Release 再构建一次。Release 版跑得快更重要的是它会提前暴露“运行环境缺依赖”的问题省得答辩现场双击 exe 黑屏。2.3 qmake 和 CMake 两种构建方式命令对照不是所有人都喜欢在 IDE 里点按钮。如果你拿到的是.pro工程想在命令行自己编先打开 Qt 自带的命令行环境开始菜单里找到 “Qt 5.15.2 (MinGW 8.1.0 64-bit)”那个终端然后进入工程目录执行mkdir build-release cd build-release qmake ../PVZ.pro -spec win32-g CONFIGrelease mingw32-make -j4这里的-spec win32-g告诉 qmake 当前编译器是 MinGW如果换成 MSVC 环境则要写成-spec win32-msvc编译命令也相应变成nmake。CONFIGrelease指定生成 Release 版-j4表示用 4 个线程并行编译老机器改成-j2更稳。如果源码包里只有 CMakeLists.txt则用 CMake 流程cmake -B build -G MinGW Makefiles -DCMAKE_PREFIX_PATHC:/Qt/5.15.2/mingw81_64 cmake --build build --config ReleaseCMAKE_PREFIX_PATH必须指向 Qt 的安装目录CMake 会从这个路径下的lib/cmake里找 Qt5 的包描述文件。两套方式的核心差异对课程设计来说就是:.pro 是 Qt 原生、写法简单适合单人或小工程CMake 是跨编译器的事实标准适合以后想往工业级 Qt 项目走的同学。对比项qmakeCMake入口文件.proCMakeLists.txt生成中间目录Shadow build 目录build/ 目录编译器切换参数-spec win32-g / win32-msvc-G MinGW Makefiles / -G Visual Studio 17 2022课程设计适合度高模板固定中以后转正要用2.4 跑通前必查的三处配置第一处qrc 资源路径。如果编译通过、窗口能开但所有植物和僵尸都不显示大概率是资源引用错了。打开.qrc文件正常内容长这样RCC qresource prefix/ fileimages/sunflower.png/file fileimages/pea.png/file fileimages/zombie.png/file /qresource /RCCqrc 里的路径是相对 qrc 文件所在目录的禁止写盘符或绝对路径。代码里引用时必须写成:/images/sunflower.png这种 Qt 专用路径格式冒号加斜杠是前缀标记不是文件系统路径。如果之前把图片路径写成了C:/Users/...Qt 编译时不会报错运行时代码加载返回空表现出来就是“界面全白但程序不崩”。第二处窗口尺寸与高清屏缩放。如果主窗口在 4K 屏上只显示一小块检查 main.cpp 里有没有加QApplication::setAttribute(Qt::AA_EnableHighDpiScaling)。Qt 5.15 在部分高分屏上默认不启用缩放不加这句话界面字号和控件比例会非常小。第三处Kit 位数一致性。64 位系统装 64 位 Qt就选 64 位 Kit。有些同学的机器是 64 位但手里只有 32 位的 Qt 安装包构建时链接器报一堆无法解析的外部符号根源就是编译器和 Qt 库位数不一致。注意如果你之前习惯用 VSCode 配好 C/C 环境后直接写代码这里不建议拿 VSCode 编 Qt。Qt 工程需要 qmake/moc 预处理生成大量中间代码VSCode 里配起来成本不低课程设计阶段老老实实 Qt Creator省下的时间用来读逻辑。3. 从入口到第一株植物main.cpp、游戏循环和信号槽拆解3.1 main.cpp 就三件事初始化、显示、进事件循环把 main.cpp 完整展开一般不超过三十行。常见写法是#include QApplication #include mainwindow.h int main(int argc, char *argv[]) { QApplication::setAttribute(Qt::AA_EnableHighDpiScaling); QApplication a(argc, argv); MainWindow w; w.setWindowTitle(QStringLiteral(植物大战僵尸 · 期末课程设计)); w.show(); return a.exec(); }这段逻辑的核心不是 show而是最后一行a.exec()。程序执行到这里之后不会返回Qt 的主事件循环接管了控制权鼠标点击、键盘输入、定时器到期、窗口重绘请求全部由事件循环分发给对应对象。可以理解成程序“活”在了这一行里直到用户关窗。参数上要注意两点一是Qt::AA_EnableHighDpiScaling要在 QApplication 构造之前设置放到后面就无效二是中文字符串用QStringLiteral()包一层。这个宏在编译期把 UTF-8 字面量转成 QString能规避 MSVC 源码编码不一致导致的一批乱码问题。3.2 游戏循环不是 while(1)QTimer 驱动加 paintEvent 重绘很多新手写游戏循环第一反应是while (true) { updateGame(); Sleep(30); }在 Qt 里这么写窗口直接假死。因为 while 循环占住了当前线程事件循环没有机会处理鼠标移动和重绘消息界面卡成一团。Qt 下做游戏循环的标准做法是让定时器驱动逻辑、让 paintEvent 驱动绘制。核心骨架如下GameWidget::GameWidget(QWidget *parent) : QWidget(parent) { setFixedSize(1200, 700); m_timer new QTimer(this); connect(m_timer, QTimer::timeout, this, GameWidget::onTick); m_timer-start(33); // 33ms 约等于 30 FPS } void GameWidget::onTick() { updateSunshine(); // 阳光计时与产出 updateZombies(); // 僵尸走格子 updateBullets(); // 子弹位移与碰撞 update(); // 通知Qt触发一次重绘 } void GameWidget::paintEvent(QPaintEvent *) { QPainter painter(this); drawBackground(painter); // 画草坪 drawPlants(painter); // 画植物 drawZombies(painter); // 画僵尸 drawSunshine(painter); // 画阳光值 }逻辑是QTimer 每 33ms 触发一次 onTickonTick 里只做状态推进——植物阳光计时加一、僵尸往前走一步、子弹坐标加一个步长。真正把画面画出来的是 paintEvent它通过 update() 被间接调用。逻辑与绘制分离这是 Qt 游戏里最值得记住的一条约定。参数上33ms 约合 30 帧每秒对课程设计足够。屏幕一般是 60Hz把定时器改成 16ms 也不会更流畅反而会放大 CPU 开销。如果换到别人机器上发现速度不对不要只调定时器要看 onTick 里运动的步长是否按固定时间片写死。QTimer 是低精度定时器Windows 下实际精度大约 15ms不要用它做逐帧精准计时。暂停游戏也简单暂停按钮触发m_timer-stop()继续就m_timer-start(33)。不需要维护一个 while 条件这就体现了事件循环驱动的好处。3.3 点“向日葵”卡片到场景里种下一株植物信号槽怎么接从点击按钮到植物出现在草坪上中间隔着两段逻辑。第一段是按钮的 clicked 信号第二段是场景的鼠标点击事件。代码常见写法是// MainWindow 构造函数里 connect(ui-btnSunflower, QPushButton::clicked, this, MainWindow::onSelectSunflower); void MainWindow::onSelectSunflower() { m_gameWidget-setSelectedPlant(PlantType::Sunflower); m_gameWidget-setCursor(Qt::CrossCursor); } // GameWidget 里 void GameWidget::mousePressEvent(QMouseEvent *event) { if (m_selectedPlant ! PlantType::None) { QPoint gridPos toGridPos(event-pos()); if (canPlantAt(gridPos)) { addPlant(m_selectedPlant, gridPos); } } }按钮的 clicked 信号被 connect 到 onSelectSunflower 槽但槽函数里并没有真的去种植物只把“当前要种什么”记录成一个状态同时把鼠标改成十字光标表示进入种植模式。真正的创建动作发生在 mousePressEvent 里也就是用户鼠标点下去的那一刻。toGridPos是坐标到格子的换算常见实现是QPoint GameWidget::toGridPos(const QPoint pos) { return QPoint(pos.x() / GRID_W * GRID_W, pos.y() / GRID_H * GRID_H); }GRID_W 和 GRID_H 是每个草坪格子的宽高常见值是 80x100。除以格子宽高再乘回去相当于把像素坐标向下取整对齐到格子左上角。这样植物永远种在格子里不会卡在格子线上。Qt 里确实有 QTest::mouseClick 这种模拟鼠标点击事件的工具但那是给自动化测试用的游戏里响应鼠标要重写 mousePressEvent走这条路才对。4. 僵尸生成与子弹碰撞随机数、碰撞判定和多态的设计4.1 僵尸生成节奏用 QRandomGenerator 替代 rand()植物大战僵尸最重要的手感来源是僵尸刷新的不确定性。很多早期源码用rand() % 5000 3000来决定间隔但有三个老问题不调用 srand 的话每次运行序列一模一样低位取模分布不均匀僵尸容易一窝蜂出现rand 是全局状态和多线程逻辑混在一起易出隐性 bug。C 课程设计阶段直接用 Qt 封装好的随机数接口最省心void GameWidget::scheduleNextZombie() { int msec QRandomGenerator::global()-bounded(3000, 7000); QTimer::singleShot(msec, this, GameWidget::spawnZombie); } void GameWidget::spawnZombie() { addZombieAtRandomLane(); scheduleNextZombie(); // 递归安排下一只保持波次连续 }QRandomGenerator::global()返回进程级全局随机数生成器线程安全且不需要手动管理种子。bounded(3000, 7000)返回 [3000, 7000) 范围内的随机毫秒数。这样僵尸平均每 5 秒来一只但有时 3 秒、有时 7 秒节奏有起伏更接近原版游戏的手感。要模拟“越到后面越密”的波次可以在 scheduleNextZombie 里根据关卡数动态缩小上限比如bounded(3000, 7000 - m_wave * 300)。QTimer::singleShot这里特别强调要传this作为接收者。带了接收者的 singleShot在 GameWidget 被销毁时 Qt 会自动取消尚未触发的定时器避免析构后定时器回调访问悬空指针。如果不写this单发给裸函数对象销毁后回调概率性崩溃是典型的偶发闪退来源。4.2 子弹命中僵尸两套方案都给出判定写法这套源码里子弹打僵尸物理过程拆开就是两步子弹每帧向前移动一段距离然后检测它有没有碰到僵尸。实现方案取决于它用的是 QGraphicsScene 还是纯 QWidget 绘制。如果项目用了 QGraphicsScene每个僵尸是 QGraphicsItem 子类判定直接走框架接口QListQGraphicsItem * hitList bullet-collidingItems(); for (QGraphicsItem *item : hitList) { ZombieItem *zombie qgraphicsitem_castZombieItem *(item); if (zombie !zombie-isDead()) { zombie-takeDamage(bullet-damage()); bullet-deleteLater(); break; } }collidingItems()返回和当前图元碰撞的所有图元框架内部按各自 shape 做相交测试。qgraphicsitem_castZombieItem *是 Qt 提供的图元类型安全转换前提是 ZombieItem 类里声明了enum { Type UserType 1 }并重写type()方法否则转换始终返回 nullptr。很多同学这里卡一下午代码写对了但 cast 出来是空就是忘了加 Type 声明。另一个常见问题是碰撞检测结果不更新。图元的 collidingItems 只在场景更新时重新计算所以子弹每次 move 之后要调一下scene()-update()或scene()-advance()否则子弹明明穿过了僵尸碰撞结果还停留在上一帧。如果项目是 QWidget paintEvent 手绘那就要自己写矩形相交判定bool bulletHitZombie(const Bullet b, const Zombie z) { return b.x z.x z.w b.x BULLET_W z.x b.y z.y z.h b.y BULLET_H z.y; }两个矩形相交四条不等式全成立才为真。为了减少比较次数可以把草坪按行拆成 5 个数组子弹只和自己所在行的僵尸比。这个方案代码直观、无框架开销但扩展性差植物多了、僵尸种类多了以后坐标计算和维护会越来越啰嗦。答辩时若能主动说一句“我清楚这两种方案的取舍”比背概念更能加分。4.3 植物基类和三种子类加新植物只改两处这包源码里植物通常用多态组织。基类定义所有植物共有的能力子类只改自己独有的行为class Plant { public: virtual ~Plant() default; virtual int cost() const { return 50; } virtual int hp() const { return 100; } virtual void onTick(GameWidget *game, const QPointF pos) { Q_UNUSED(game); Q_UNUSED(pos); } virtual void draw(QPainter p, const QPointF pos) 0; bool isDead() const { return m_hp 0; } void takeDamage(int damage) { m_hp - damage; } private: int m_hp 100; };向日葵和豌豆射手各自继承它class Sunflower : public Plant { public: int cost() const override { return 50; } // onTick 里每10秒触发一次产阳光 }; class Peashooter : public Plant { public: int cost() const override { return 100; } // onTick 里检查所在行前方有没有僵尸有则发射子弹 };将来加一种新植物只需要做两件事写一个继承 Plant 的新类重写自己关心的虚函数再在工厂函数里加一条分支。其他所有逻辑天然可用不需要到处加 switch 判断植物类型。工厂分支常见写法Plant *PlantFactory::create(PlantType type) { if (type PlantType::Sunflower) return new Sunflower(); if (type PlantType::Peashooter) return new Peashooter(); if (type PlantType::WallNut) return new WallNut(); qWarning() unknown plant type type; return nullptr; }注意工厂返回裸指针GameWidget 里负责持有并最终释放。漏掉分支时不要返回 nullptr 后继续往下走否则后面的绘制代码对空指针取地址闪退点离真正原因十万八千里很难排查。加一句 qWarning 日志输出的习惯能帮你省下至少一小时调试时间。5. 答辩前必看的避坑清单编译链接、编码、假死和闪退5.1 编译链接报 fatal: cannot mix incompatible Qt library现象编译过程正常链接阶段弹出形如fatal: cannot mix incompatible Qt library (version ex50601) with this library的错误后面跟一长串 .lib 或 .a 文件路径。原因工程里有两个版本的 Qt 混用了。最常见是电脑上装了多个 Qt系统 PATH 环境变量先指向了旧版本或者 Qt Creator 里 Kit 绑定的 qmake 来自 5.15.2但编译器 PATH 里先找到了 5.6.1 的工具。解决在 Qt Creator 里打开 工具 - 选项 - Kits查看当前 Kit 绑定的 Qt 版本路径是否指向预期的安装目录比如C:\Qt\5.15.2\mingw81_64\bin\qmake.exe。确认后删掉旧的 build 目录重新执行 qmake 再编译。命令行下可以执行qmake -v验证当前终端里的 qmake 实际来自哪个版本。5.2 双击 exe 起不来qt.qpa.plugin 平台插件找不到现象在 Qt Creator 里运行好好的但进到 build 目录直接双击PVZ.exe弹窗或黑屏输出This application failed to start because no Qt platform plugin could be initialized具体到 Windows 上常见是could not find the qt platform plugin windowsLinux 嵌入式板子上则常见qt.qpa.plugin: could not find the qt platform plugin linuxfb。原因Qt 程序启动时要去 exe 同级目录下找platforms文件夹里的 qwindows.dll。Qt Creator 之所以能跑是因为它自己把 Qt 的 plugins 目录加进了环境变量脱离 Creator 后exe 找不到这套运行库。解决用 Qt 自带部署工具。先切换到 Release 构建产物目录再执行cd /d D:\QtProjects\build-PVZ-Desktop_Qt_5_15_2_MinGW_64_bit-Release\release windeployqt PVZ.exewindeployqt 会自动扫描 exe 依赖的 Qt 模块把 Qt5Core.dll、Qt5Gui.dll、Qt5Widgets.dll 以及platforms、imageformats等插件目录全部复制过来。拷贝完后整个 release 文件夹可以整体拷到没有装 Qt 的机器上运行。这是答辩现场最常用的血泪经验。提示Linux 下同样问题先检查QT_QPA_PLATFORM环境变量如果是 linuxfb改回 xcb 或 eglfs 再试。5.3 按钮中文乱码源文件编码统一成 UTF-8现象按钮文字“开始游戏”“阳光值”显示成乱码或者一排问号。在 MinGW 下正常换了 MSVC 就乱。原因MinGW 默认按 UTF-8 读源码而 MSVC 默认按系统代码页中文系统是 GBK解释源文件。源码文件是 UTF-8 保存的MSVC 用 GBK 去读字符串字面量自然错位。解决MSVC 下在.pro文件里追加一行msvc: QMAKE_CXXFLAGS /utf-8然后把所有.cpp、.h文件用编辑器统一另存为 UTF-8 编码。代码里写中文字符串时统一用QStringLiteral()或tr()包起来不要直接拿const char*赋给 QString。这一套操作下来换编译器也不容易翻车。5.4 点开始游戏后窗口假死现象点“开始游戏”窗口还能显示但拖不动、按钮没反应过几秒弹“无响应”。原因把游戏循环写成了槽函数里的 whilevoid MainWindow::onStartClicked() { while (m_gameRunning) { updateGame(); QThread::msleep(30); } }槽函数不返回Qt 事件循环收不到鼠标和重绘事件界面就被这个循环堵死了。不需要开多线程解决QTimer 本身就是答案。改成connect(ui-btnStart, QPushButton::clicked, this, [this] { m_gameRunning true; m_timer-start(33); });槽函数立刻返回定时器到点后由事件循环调用 onTick。整个窗口始终保持响应状态。5.5 僵尸啃掉植物后闪退或者切关卡崩溃现象植物血量归零、僵尸啃掉它的一瞬间程序崩偶尔是切换到下一关时崩。原因在遍历植物列表的过程中直接把对象 delete 掉了。之前提到的方案里GameWidget 持有植物的裸指针遍历一边访问一边删除迭代器失效后就崩了。如果用的是 QGraphicsItem直接在定时器回调里 delete item风险更高因为 paintEvent 还在绘制它。解决延迟删除。QGraphicsItem 方案里用deleteLater()让对象在当前事件处理完后、回到事件循环时再释放。QWidget 普通指针方案则先标记再统一回收void GameWidget::collectDeadPlants() { for (auto it m_plants.begin(); it ! m_plants.end(); ) { if ((*it)-isDead()) { delete *it; it m_plants.erase(it); } else { it; } } }调用时机放在 onTick 末尾即所有逻辑更新做完、没有代码还持有着列表迭代器之后再执行。注意it m_plants.erase(it)的写法erase 会返回下一个合法迭代器而不是先自增再 erase这是两种容易踩坑的写法。切换关卡前记得先m_timer-stop()再断开新旧场景之间残留的信号槽连接否则上一局的定时器还会驱动新场景的对象造成一次双重更新和潜在崩溃。6. 给课程设计加分Qt Designer 改布局、存读档与 Release 打包6.1 用 Qt Designer 拖布局替代手写 setGeometry源码里的界面如果是纯代码setGeometry写死的改起来非常痛苦。把 MainWindow 拿到 Qt Designer 里改左侧放一排 QPushButton 作为植物卡片栏设置 fixedWidth 和 icon右侧放 GameWidget。关键一步是右键 GameWidget 占位区域选择“提升为”类名填GameWidget头文件填gamewidget.h。提升之后设计器里就能直接看到自定义控件布局调整所见即所得生成的代码量也少得多。6.2 存读档、音效和可移植运行课程设计展示的三个实用加分项。存档用 Qt 自带的 QSettings不需要设计文件格式QSettings settings(MyCourse, PVZ); settings.setValue(sunshine, m_sunshine); int sunshine settings.value(sunshine, 50).toInt();第一参数是组织名第二参数是应用名落盘位置在 Windows 注册表或 ini 文件课程设计演示足够。音效用 QSoundEffect只吃 wav 格式资源先加进 qrcQSoundEffect shootSound; shootSound.setSource(QUrl(qrc:/sounds/shoot.wav)); shootSound.setVolume(0.5f); shootSound.play();wav 启动快、无解码开销比 mp3 更稳。可移植运行就是把 Release 版 exe 用 windeployqt 打包整个文件夹拷起来插到答辩机器上就能跑。最后说一件我自己的事。当年我做课程设计答辩现场用的是 Debug 版老师电脑上没有 Qt 环境双击起不来原地装了十分钟 Qt讲得再细也没心情听了。从那以后凡是可能要演示的程序我都先做一次 Release 加 windeployqt再在别的机器上完整跑一遍。这包源代码认真读一遍你拿到的不是一个会动的游戏界面而是一整套能说清楚的 Qt 对象管理思路答辩时老师问什么都有底。希望帮到你。本文还有配套的精品资源点击获取