
简介面向C程序设计课程期末设计的Qt植物大战僵尸游戏源码包基于Graphics View框架构建场景与视图利用面向对象的封装、继承、多态将植物和僵尸分别抽象为基类并延伸出太阳花、豌豆射手、坚果、普通僵尸、路障僵尸等具体角色商店、地图、卡片等板块独立成类覆盖阳光收集、植物种植、子弹发射、僵尸行进与碰撞判定等完整游戏机制适合课程设计答辩和游戏编程学习。整个压缩包共111个文件含25个cpp与25个h源码、21个png与28个gif界面素材、qt工程文件及开发文档pdf另附演示视频mp4和音效wav大小约39.43MB。目前已有3828人学习下载。对照开发文档可梳理场景、视图、图形项的关系从主窗口到每个类的槽函数逐层分析快速掌握Qt事件循环与碰撞检测的实现也可作为增加新植物、新僵尸或关卡系统的二次开发框架完整源代码加开发文档能让期末设计直接落地无论用于报告撰写、答辩演示还是后续扩展资料链条都很完整。1. C程序设计期末课程设计用QT重写一版植物大战僵尸C期末课程设计拿植物大战僵尸当题目用QT重写一版听着像玩具项目真做起来牵涉的东西一点都不少。这份源码包的核心是 Qt 的 Graphics View 框架配合封装、继承、多态把植物、僵尸、卡片槽抽象成三条继承线太阳花、豌豆射手、坚果、土豆雷、樱桃炸弹都在 Plant 派生线上各自实现难度正好卡在期末课程设计要「有完整功能、有类设计、能演示」的验收点上。它不是那种把图片贴来贴去的小 demo而是把一局游戏拆成了可扩展的类结构场景管理、网格种植、碰撞判定、卡片冷却、僵尸啃食全都有对应的模块。适合两类人一类是要交 C 课程设计、想把游戏做成能跑能演示项目的同学另一类是学完 Qt 基础、想看 Graphics View 怎么支撑真实游戏逻辑的开发者。下面我按架构、类设计、编译运行、调参验证的顺序把它拆开。2. 架构选型Graphics View框架和C三大特性是怎么在代码里落地的2.1 Scene / View / Item 三层模型为什么课程设计不选QWidget绘图很多课程设计做小游戏会重写paintEvent在 QWidget 上直接画所有元素。这种写法做俄罗斯方块没问题一旦场景里同时有阳光、僵尸、子弹、卡片对象数量上到几十个重绘区域和点击命中就会变成一团乱麻。这套源码改用 Graphics View 框架核心思路是三层分离QGraphicsScene管理场景里的所有 ItemQGraphicsView负责把场景渲染到界面上QGraphicsItem是每一个可以被选中、移动、刷新、碰撞检测的独立对象。mainwindow.cpp里做的事就是把 View 挂到主窗口中央构造 Scene再把植物、僵尸这些 Item 放进 Scene。三层各自的坐标系统也值得捋一遍Item 有局部坐标Scene 是全局坐标View 是屏幕坐标。鼠标点击植物的时候先拿到的是 View 坐标要映射到 Scene 坐标再由 Map 类换算成种植网格。换算结果记录在二维网格里保证一个格子只能种一棵植物这套逻辑在map.cpp里是独立的没有和绘制代码混在一起后期改棋盘行列数也只需要动一个文件。画质相关的配置一般在 View 上做常见做法是开抗锯齿和设置视口更新模式。setRenderHints(QPainter::Antialiasing)开了之后豌豆和僵尸的圆角边缘会平滑很多期末答辩投到屏幕上观感提升明显setViewportUpdateMode控制画面刷新粒度对象多的时候用整帧更新会卡改用局部更新能明显缓解。这些参数都在 mainwindow 的初始化代码里想调直接改那一行。2.2 封装、继承、多态Plant、Zombie、Other三条继承线的分工摘要里写得很清楚自定义的三个类都继承自QGraphicsItem分别是植物基类 Plant、僵尸基类 Zombie、其他基类 Other。Plant 的派生类包括太阳花 SunFlower、豌豆射手 Peashooter、坚果 Wallnut再算上文件列表里的potatomine.cpp和cherrybomb.cpp植物至少五条分支Zombie 的派生类有普通僵尸 BasicZombie、路障僵尸 ConeZombie 这类Other 下面挂着商店 Shop卡片槽、地图 Map、卡片 Card另外button.cpp、shovel.cpp也都归在这条线附近。这套派生结构把游戏对象分成了独立分支每个派生类只实现自己差异化的部分。多态的作用在帧循环里体现得最直接Scene 里的 Item 都是基类指针调用update()、advance()时自动进入各个派生类的实现不用在循环里写一堆if (type 1)判断。QGraphicsItem本身要求子类必须实现boundingRect()和paint()这两个纯虚函数植物画成什么样、僵尸画成什么样全部由各自的 paint 决定。源文件对应类 / 模块职责mainwindow.cppMainWindow装配 Scene、View、定时器、主循环card.cppCard卡片绘制、冷却状态、点击响应shop.cppShop卡片槽管理、阳光消耗map.cppMap地块网格、坐标换算、格子占用zombie.cppZombie 基类移动、啃食、受击通用逻辑basiczombie.cppBasicZombie普通僵尸的外观和参数potatomine.cppPotatoMine土豆雷的武装、爆炸逻辑cherrybomb.cppCherryBomb樱桃炸弹的延时引爆button.cppButton开始、暂停等控制按钮shovel.cppShovel铲除已种植的植物这个表基本就是源码包的目录地图下载后先对照这个表把文件过一遍读代码的效率会高很多。2.3 游戏循环QTimer驱动和信号槽的配合方式整套游戏没有引入物理引擎游戏推进靠定时器驱动这是 Qt 小游戏最常见的做法。MainWindow 里建一个QTimer把timeout信号连到 Scene 的advance()方法上每帧推进所有 Item。太阳花产阳光、豌豆射子弹、僵尸移动都挂在这同一个帧循环里而不是每个对象单独开线程——游戏对象数量不大单线程主循环完全够用而且能避免大量线程同步问题。卡片这块走的是信号槽玩家点击卡片后卡片发出选种信号Shop 把当前状态切到「种植模式」然后再接收鼠标点击事件完成种植。这个交互链如果直接写在 mousePressEvent 里后期加铲子、加樱桃炸弹都会改到想哭拆成信号槽之后卡片、商店、地块三者解耦新增一种植物只需要注册新的签名和对应的派生类。定时器还有一个容易忽略的细节冷却计时和阳光产出不能用同一个小间隔硬写否则调游戏节奏时要改好几个地方。常见的做法是把冷却计数放到 Card 内部阳光产出放到太阳花自己的 tick 里这样数值调整限定在局部类中不会牵连全局。3. 核心类拆解Plant、Zombie、Shop三线设计以及网格种植和碰撞判断3.1 Plant一条线构造函数传参、冷却与攻击判定的实现Plant 这条线是所有派生类里数量最多的封装的好处在这里体现得很明显。基类把公共状态都收进来血量、阳光消耗、冷却时间、受击后的扣除逻辑。下面的骨架代码展示了这类结构长什么样class Plant : public QGraphicsItem { public: Plant(int hp, int cost, int cooldown, QGraphicsItem *parent nullptr) : QGraphicsItem(parent), m_hp(hp), m_cost(cost), m_cooldown(cooldown) {} int cost() const { return m_cost; } bool isCooling() const { return m_cooldown 0; } int hp() const { return m_hp; } void takeDamage(int damage) { m_hp - damage; if (m_hp 0) { setVisible(false); } } void tick() { if (m_cooldown 0) { --m_cooldown; } } QRectF boundingRect() const override { // 需要与实际绘制尺寸匹配不要随意放大 return QRectF(0, 0, 60, 80); } protected: int m_hp; int m_cost; int m_cooldown; };这里有两个细节值得注意。第一takeDamage里血量扣到零后只做了setVisible(false)没有立刻从 Scene 里删除这是为了避免在帧循环中间删除对象导致迭代器失效真正的清理放到下一帧或deleteLater()是 Qt 游戏里一个比较务实的小技巧。第二boundingRect()返回的矩形必须和绘制内容一致如果画的是 60×80 的植物这里返回值写小了点击命中区域就会缩水出现「鼠标点中了植物却选不中」的玄学问题。豌豆射手、土豆雷、樱桃炸弹的差异主要在行为上构造函数负责把参数传进来paint()负责画各自的外观。土豆雷比较特殊它有一个「未武装」状态种下去的前几秒不能引爆要等计时结束切换到武装状态之后僵尸踩到才爆炸。实现上就是增加一个布尔状态或枚举在advance()里轮询。樱桃炸弹则是种植后延时引爆可以用一次性计时器触发爆炸动画再把爆炸范围内的僵尸都扣一遍血。这些分支都靠多态接口收敛到基类指针上卡片槽不用关心具体是谁。3.2 Zombie一条线移动、啃食、死亡状态切换僵尸的行为比植物更线性但状态切换也要认真处理。普通僵尸和路障僵尸的差别主要体现在参数上路障僵尸血量更高、移动速度略慢、外观多一个路障。僵尸的核心逻辑是三种状态行走、啃食、死亡。行走时每帧向左移动固定距离走到植物面前切换成啃食对植物持续造成伤害植物血量归零后僵尸回到行走状态继续前进。下面是一段骨架代码enum class ZombieState { Walking, Eating, Dying }; void BasicZombie::advance(int phase) { if (!phase) { return; // phase 0 时只做位置预更新避免一帧跑两次 } switch (m_state) { case ZombieState::Walking: if (findPlantInFront()) { m_state ZombieState::Eating; m_targetPlant findPlantInFront(); } else { setX(x() - m_speed); } break; case ZombieState::Eating: if (!m_targetPlant || m_targetPlant-hp() 0) { m_state ZombieState::Walking; m_targetPlant nullptr; } else { m_targetPlant-takeDamage(m_dps); } break; case ZombieState::Dying: // 播放死亡动画后调 deleteLater() break; } }advance(int phase)的 phase 参数是 Qt 的机制一帧内会调用两次第一次 phase 为 0 做逻辑更新第二次 phase 为 1 做绘制更新逻辑判断加个if (!phase) return;可以避免状态被处理两遍。僵尸找目标的方式不一定要全场景遍历常见的做法是只检查当前行前方固定距离内有没有植物这个查找逻辑放在你自己的findPlantInFront()里可以用 Map 的网格数据直接判断「这一行前方格子是否被占用」效率比collidingItems更高。路障僵尸的血量拆分是一个可选技巧把路障看作额外一层护甲优先扣路障血量路障被打掉后再扣本体配合paint()里判断当前阶段把路障画出来视觉反馈会明显很多。3.3 Shop、Map、Card和Shovel种植交互的数据层与表现层分离种植交互是这版游戏里最容易写出 bug 的部分。完整流程分四步点击卡片、扣阳光、跟鼠标移动、点击地块落子。shop.cpp负责第一步的扣费和卡片冷却card.cpp画卡片的形状和状态map.cpp负责最后一步的网格换算。网格换算的核心在于把自由坐标吸附到格子中心公式大致是这样int col qBound(0, (scenePos.x() - gridOriginX) / gridCellWidth, gridCols - 1); int row qBound(0, (scenePos.y() - gridOriginY) / gridCellHeight, gridRows - 1); if (gridOccupied[row][col]) { return; // 格子已被占用不允许重复种植 }gridOriginX是棋盘左上角的起始坐标gridCellWidth是每格宽度。qBound的作用是把越界坐标限制在合法范围内防止玩家点到棋盘外把索引算成负数导致数组越界崩溃。铲子的实现有两种思路。一种是点击铲子后进入铲除模式再点击地块上已有的植物另一种是直接检测鼠标点击位置是否有植物。文件列表里有独立的shovel.cpp说明它作为工具类被单独抽出来了。和种植流程相比铲除逻辑要注意顺序先把植物从网格数组中移除再调 Scene 的removeItem最后deleteLater顺序反了容易留下悬空指针。地图这块还有个小坑阳光值归零或不足时卡片应该置灰且不可点击。判断点放在 Shop 的点击入口里不要在 Card 内部各自检查否则新增一种植物就要改一次卡片逻辑。4. 运行与避坑Qt环境配置、编译报错和运行时崩溃的排查记录4.1 环境对齐Qt版本、编译器、qmake三件套拿到源码包先别急着双击 .pro先确认本机 Qt 环境。期末课程设计里最常见的翻车是把代码拷到另一台机器上Qt 版本从 5.x 换成 6.x库路径和模块名称对不上编译报一堆错。建议先用 Qt 5.15 LTS 或 6.x 里你熟悉的一个版本关键是工程文件和宿主环境要一致。.qpro 文件本身不复杂核心就是声明模块和列出源文件QT core gui widgets greaterThan(QT_MAJOR_VERSION, 4): QT widgets TARGET PlantsVsZombies TEMPLATE app # 把解压目录里实际存在的 .cpp / .h 追加到下面两组变量里 SOURCES main.cpp \ mainwindow.cpp card.cpp shop.cpp map.cpp \ zombie.cpp basiczombie.cpp potatomine.cpp \ cherrybomb.cpp button.cpp shovel.cpp HEADERS mainwindow.h card.h shop.h map.h \ zombie.h basiczombie.h potatomine.h \ cherrybomb.h button.h shovel.h RESOURCES res.qrcgreaterThan(QT_MAJOR_VERSION, 4)这行是在 Qt5 及以上自动追加 widgets 模块老工程从 Qt4 迁移时很关键删掉会导致 Qt5 下找不到QWidget头文件。RESOURCES对应资源文件如果源码包里有图片素材需要确认res.qrc里的路径实际存在否则运行时会找不到图片植物和僵尸全是空白方块。构建方式有两种习惯命令行的用qmake mingw32-makeWindows 下 MSVC 则用nmake想省事就直接用 Qt Creator 打开 .pro 文件编译。如果你平时习惯在 VS Code 里写 C/C调试 Qt 项目还是建议切回 Qt Creator至少它能自动识别 .pro 里的源文件列表和 Qt 的 include 路径少踩一堆环境配置的坑。4.2 编译期报错fatal: cannot mix incompatible Qt library现象编译过程正常链接阶段报错错误信息类似fatal: cannot mix incompatible Qt library (version 0x50601) with this library代码本身没有任何语法问题。原因这句话不是代码错误是库冲突。工程或系统中的某个库编译时用的是 Qt 5.6.1 的头文件而链接时却混入了其他版本的 Qt 库版本不匹配直接拒绝链接。常见场景是电脑上装了多个 Qt 版本PATH 环境变量或 qmake 指向不一致。解决先qmake -v看当前 qmake 指向哪个版本然后彻底清理编译缓存把 build 目录和.qmake.stash、Makefile全删掉重新 qmake。如果还报错检查 PATH 里是否混入了其他 Qt 版本的 bin 路径把不需要的版本从 PATH 里拿掉只保留当前项目用的那个。4.3 运行时报错qt.qpa.plugin Could not find the Qt platform plugin linuxfb现象在 Linux 或嵌入式板卡上运行编译好的可执行文件终端输出qt.qpa.plugin: could not find the qt platform plugin linuxfb程序直接退出窗口根本出不来。原因Qt 在 Linux 下靠平台插件和显示服务器通信linuxfb是嵌入式平台插件。桌面 Linux 上如果没安装 xcb 插件或者环境变量QT_QPA_PLATFORM被误设成了linuxfbQt 找不到对应插件就罢工。解决桌面环境一般不需要 linuxfb。执行export QT_QPA_PLATFORMxcb或直接取消这个环境变量再运行。如果提示缺少 xcb装对应发行版的 Qt xcb 插件包。只有当真在跑 framebuffer 设备时才用 linuxfb而且要先确认插件二进制实际存在于 Qt 的plugins/platforms目录里。4.4 运行崩溃access violation 0xC0000005 与 QGraphicsItem 野指针现象游戏运行中突然崩溃调试器停在QGraphicsScene::removeItem附近错误代码0xC0000005也就是访问违例——读写了已释放的内存。原因游戏对象在帧循环里被删除时其他地方还持有指向它的指针。典型场景是僵尸啃死了植物植物把自己 delete 了下一帧僵尸还要通过m_targetPlant访问这个对象的hp()访问到的已经是悬空指针。解决删除对象前必须先scene-removeItem(item)再deleteLater()让 Qt 在事件循环安全点统一释放。持有目标的类在目标失效时要把指针置空。我的习惯是在基类里加一个isInScene()或isAlive()判断访问前先检查别指望指针自己变安全。4.5 碰撞误判土豆雷该炸没炸樱桃炸弹炸空现象土豆雷埋在格子里僵尸明明踩上去了它却迟迟不爆炸樱桃炸弹放下来几秒后爆炸范围边缘的僵尸却完好无损。原因碰撞判定用的是boundingRect()的矩形区域矩形比实际物体大或小都会有偏差。土豆雷的问题是触发半径设得过小或者僵尸移动步长太大一帧内直接从触发区上方穿过去了帧循环根本没检测到相交。樱桃炸弹的问题则往往出在爆炸范围计算上如果用矩形判定圆形爆炸区域四个角的僵尸会被误判为在范围内。解决碰撞筛选用shape()或自定义圆形区域不要宽泛地用boundingRect()做精确判定。僵尸的移动要做步进检测保存上一帧位置和当前位置两个位置之间的线段再和触发区做相交判断就不会漏掉高速移动的物体。爆炸范围用「中心点距离」判断半径内算命中比矩形判定准得多。5. 进阶调优平衡参数、状态机重构和一局试玩的验证闭环前几章把这套源码的结构和坑都过了一遍剩下的问题是怎么把一局游戏调得真正能玩而不是能跑就行。期末答辩时评委看的就是手感——阳光产出节奏、僵尸进攻密度、植物强度是否匹配代码结构反而不是最直观的。植物和僵尸的数值最后都会落到构造函数那几个参数上。下面是一组参考值你可以按自己的感觉改向日葵产阳光间隔约 12 秒、每次 50 阳光豌豆射手单发伤害 20、攻速 1.5 秒普通僵尸血量 200、速度约 0.4 格/秒路障僵尸血量 400、速度略慢。这些数值没有标准答案调整的核心原则是「前期让玩家有余力种豌豆中期感受到压力后期靠樱桃炸弹翻盘」。如果觉得阳光永远够用就调高植物价格或缩短僵尸出兵间隔如果觉得一波都撑不住就把甲板第 1 行设置为安全区直接在代码里加一个出生保护判定。一个很实用的重构招数是把散落的僵尸行为判断收敛成状态机前面 3.2 节的ZombieState枚举就是雏形。当分支越来越多时switch比一堆if else清晰得多每个 case 只做一件事状态迁移路径明确出 bug 时看日志就能定位。我通常会在每个状态迁移处打一行 qDebug 输出比如Walking-Eating跑一局下来能完整复盘僵尸的行为轨迹比断点调试高效。验证和测试也不要全靠肉眼。C 的随机数如果不固定种子每一局的僵尸波次都不一样很难评估一次改动到底有没有效果。在测试阶段用固定随机种子初始化std::mt19937改动前后用同一种子各跑一遍才能确定差异来自数值调整而不是运气。帧率验证则用QElapsedTimer包住advance()循环超过 33 毫秒说明当前帧有性能瓶颈再考虑优化绘制或减少 Item 数量。这里说一个我自己的翻车经历权当后车之鉴有一版我把向日葵的产阳光间隔从 12 秒改成 2 秒来测卡手问题结果满场阳光把场景里的 Item 堆到上千帧率从 40 帧掉到 12 帧最终直接卡死。从那以后我每次调数值都会在开发文档里先记一笔旧值和新值改完只跑两分钟验证就拉回真实节奏绝不让测试参数污染正常对局。这套源码的完整工程和开发文档都在同一个包里下载后先按第 4 章的流程把环境对齐再对照第 3 章的类图读代码一次跑起来的概率很高。希望帮到你。本文还有配套的精品资源点击获取