简介这是一份基于Qt Graphics View框架开发的植物大战僵尸游戏完整课程设计源码包面向C程序设计课程的期末项目适合需要完成同类游戏开发或学习Qt图形编程的高校学生。项目采用面向对象设计通过封装、继承与多态实现植物、僵尸、商店等核心类体系并配有开发文档与演示视频便于理解整体架构与运行效果。资源包共111个文件涵盖25个cpp源文件、25个头文件以及21个静态PNG素材另有pro工程文件、PDF开发说明和演示视频等压缩包约39.43MB目录结构清晰方便直接打开工程进行二次开发与学习。目前已有3825人学习下载。从中可获取一份结构完整、可直接运行的Qt游戏项目学习Graphics View框架的使用、自定义图形项的交互设计以及面向对象思想在游戏开发中的落地方式也可作为课程设计答辩时的功能演示与代码讲解基础。1. 从“期末课程设计”到可运行的Qt植物大战僵尸差在哪拿到这个标题第一反应可能是把它解压、打开.pro、点运行。实际动手你才会发现一个Qt植物大战僵尸课程设计能不能顺利交差取决于三件事类结构是否清晰、资源图片放在哪里、以及目标机器上有没有对应的Qt运行库。不要相信一个“源代码.zip”解压后就能双击运行按Qt 5.15.2这个稳定基座来审视这个工程应该可以拆成场景、实体、调度三个层次而不是把所有东西塞进MainWindow里。接下来我从一个常见课设项目的角度把植物的种植、僵尸生成、子弹碰撞、编译发布和答辩展示串一遍让新手能跟着重构也让有经验的读者看到边界和值得改进的地方。2. 植物大战僵尸的面向对象设计用QGraphicsItem承载多态2.1 QGraphicsView还是QWidget先按工期做决定植物大战僵尸的渲染层有两条路。一条是重写QWidget的paintEvent把所有植物、僵尸画在一张画布上再用一个2D数组记录格子数据另一条是使用QGraphicsScene搭配QGraphicsView把每个角色封装成独立的item。如果是两个月以上的大作业手绘paintEvent能让你拿高分因为你能讲清楚整个绘制流水线如果只剩两三周我会建议直接用QGraphicsView。原因很简单场景坐标转换、item之间的相交测试、鼠标点击命中测试都是现成的你把精力放在对象关系上更划算。两种方案的取舍可以列成一张对比表方案代码量碰撞检测动画扩展课程设计答辩亮点QWidget手动绘制多自己写矩形相交需要管理dirty区域展示底层渲染能力QGraphicsScene/View少Qt内置intersects每item独立更新展示面向对象与架构能力我见过不少课设源码表面上用了QGraphicsScene但游戏逻辑还是写在view的鼠标事件中植物和僵尸的类只有数据成员没有方法。这在答辩时是减分项。所以真正要做的是把逻辑下沉到实体类中让view只负责接收用户输入和驱动场景。2.2 实体基类同时继承QObject和QGraphicsItem一个成熟的框架会定义抽象基类GameEntity它继承QObject和QGraphicsItem。注意QGraphicsItem在前QObject在后因为Qt的元对象系统要求QObject在继承列表中顺序靠后否则编译会出现undefined reference to vtable。这个基类中paint和boundingRect是纯虚函数这保证每个子类必须定义自己的外观和碰撞范围。// gameentity.h #include QObject #include QGraphicsItem class GameEntity : public QGraphicsItem, public QObject { Q_OBJECT public: explicit GameEntity(const QPointF initialPos) : QGraphicsItem(), QObject(), currentTick(0) { setPos(initialPos); } QRectF boundingRect() const override 0; void paint(QPainter *painter, const QStyleOptionGraphicsItem *option, QWidget *widget) override 0; virtual void updateByTick(int tick) { currentTick tick; update(); // 触发场景重绘 } protected: int currentTick; int hp; int row; };代码说明updateByTick是每帧的业务逻辑入口QGraphicsItem::update()会通知scene在下一轮重绘这个item不需要手动调用repaint。hp和row放在基类是为了后续碰撞检测和伤害计算统一。使用时你还要在子类构造函数中调用setFlag(QGraphicsItem::ItemIsSelectable)或类似标志如果不用鼠标选取这步可以省略。2.3 场景内的对象管理与网格索引场景中会有植物、僵尸、子弹三种实体它们有各自的行为和生命周期。比较稳妥的管理方式是在GameScene里维护三类容器QListPlant* plants、QListZombie* zombies、QListBullet* bullets同时用一个QHashint, Plant* gridPlantMap记录格子与植物的映射。之所以不让QGraphicsScene的items()直接承担查找是因为items()返回QGraphicsItem的通用列表每次都要qgraphicsitem_cast代码既啰嗦又慢。const int GRID_ROWS 5; const int GRID_COLS 9; QHashint, Plant* m_gridPlantMap; int cellIndex(int row, int col) { return row * GRID_COLS col; }当放置植物时先检查该index是否存在plant存在就拒绝放置。这样的好处是判断“某格能不能种”的时间复杂度为O(1)。在游戏中后期场景同时存在上百个item用QGraphicsScene::itemAt逐个遍历是不可靠的尤其是点选和网格种植判定这两种逻辑不该混在一起。2.4 动画帧如何驱动按tick而不是按timeQt动画框架QPropertyAnimation可以处理按钮和简单的数值动画但僵尸行走、植物摇摆这种循环帧动画用QPropertyAnimation要写很多setKeyValueAt维护成本高。我更喜欢在updateByTick里用计数器决定帧序号。假设僵尸有walk1.png、walk2.png、walk3.png、walk4.png四帧每150ms换一帧而主timer是20ms那么帧间隔换算成7.5个tick实际取8意味着160ms一帧。void WalkingZombie::updateByTick(int tick) { GameEntity::updateByTick(tick); int frame (tick / 8) % 4; setPixmap(m_frames[frame]); // 如果用QGraphicsPixmapItem方式 }参数说明m_frames在构造函数中从QPixmap缓存不要每次paint都从磁盘加载。tick是全局递增计数器不是每帧差值所以帧切换与帧率无关。这样做还有一个优点当你暂停游戏时只要停止timer所有动画都会同步冻结。你用QTime::elapsed()则还得另外处理暂停逻辑。3. 核心玩法落地的Qt实现太阳、种植、生成与碰撞3.1 用QTimer做固定步长主循环游戏中最稳定的结构是一个20毫秒的主定时器它在scene层触发onTick然后由onTick通知所有实体。不要在每个植物类里单独起QTimer那样当你有20个向日葵时就有20个timer窗口切换、暂停时还要逐个处理很容易出错。GameScene::GameScene(QObject *parent) : QGraphicsScene(parent) { m_timer new QTimer(this); connect(m_timer, QTimer::timeout, this, GameScene::onTick); m_timer-start(20); } void GameScene::onTick() { m_elapsedTick; for (Plant *p : m_plants) p-updateByTick(m_elapsedTick); for (Zombie *z : m_zombies) z-updateByTick(m_elapsedTick); for (Bullet *b : m_bullets) b-updateByTick(m_elapsedTick); tryGenerateSun(); handleBulletCollisions(); removeDeadEntities(); checkGameOver(); }这里的设计重点是updateByTick只负责推进自身状态碰撞和生成由scene统一管理。这样实体之间不互相持有指针降低耦合。参数说明20毫秒意味着每秒50次tick向日葵生产阳光间隔如果设为600tick刚好12秒比较接近原版节奏。如果你希望帧率更高可以改成10ms但QTimer在Windows上的精度大约15ms设10ms反而可能浪费CPU。3.2 种植植物的坐标换算与检查用户点击scene坐标后先转成行列。我以每格宽80、高100为例。view要开启setMouseTracking(false)也行只要在mousePressEvent处理点击即可。void GameScene::mousePressEvent(QGraphicsSceneMouseEvent *event) { QPointF pos event-scenePos(); int col static_castint(pos.x()) / CELL_WIDTH; int row static_castint(pos.y()) / CELL_HEIGHT; if (row 0 row GRID_ROWS col 0 col GRID_COLS !m_gridPlantMap.contains(row * GRID_COLS col)) { if (m_plantCost m_sunPoints) { m_sunPoints - m_plantCost; Plant *plant createPlantAt(row, col); m_gridPlantMap.insert(row * GRID_COLS col, plant); addItem(plant); } } }这段代码已经附带阳光扣除检查。要注意createPlantAt返回的plant要和网格索引一起保存否则后续在植物被吃掉后你无法从grid中移除。移除时应该在removeDeadEntities里做“查格子id-删条目”的联动而不是直接delete item。3.3 子弹与僵尸的碰撞检测先同行过滤再做矩形相交原版植物大战僵尸的碰撞其实发生在同一行子弹和僵尸之间。如果对所有子弹和所有僵尸做双重循环时间复杂度是O(B*Z)。简化后的代码先做行号过滤仍然是最可读的方案void GameScene::handleBulletCollisions() { for (Bullet *b : m_bullets) { QRectF bulletRect b-boundingRect().translated(b-pos()); for (Zombie *z : m_zombies) { if (z-row() ! b-row()) continue; QRectF zombieRect z-boundingRect().translated(z-pos()); if (bulletRect.intersects(zombieRect)) { z-setHp(z-hp() - b-damage()); b-setRemovedFlag(); break; } } } }参数说明boundingRect返回的是局部坐标系矩形必须translated(z-pos())变成场景坐标如果直接用全局位置设置Entity pos那么z-pos()是场景坐标。这里有个细节僵尸的boundingRect建议只取图片实际占用的窄条不要覆盖到旁边草地否则玩家会看到子弹悬空也能命中。你可以用调试模式绘制边界来判断。3.4 僵尸生成的调度表与难度曲线生成僵尸我倾向使用事件表而不是纯随机。原因之一是期末考试演示时你希望前30秒稳定出现若干波僵尸让玩家有时间种植物如果靠rand很可能开局一下刷出五只直接把演示搞砸。事件表可以这样设计时间秒僵尸类型数量10普通僵尸125普通僵尸240锥桶僵尸160路障僵尸1在程序里用queuepairdouble, int m_waveQueue键是游戏秒值是类型。onTick中double currentTime m_elapsedTick * 0.02; while (!m_waveQueue.empty() currentTime m_waveQueue.front().first) { spawnZombie(m_waveQueue.front().second); m_waveQueue.pop(); }这样既保证可预测性也保留了少量随机僵尸出现的y坐标和具体行号可以随机只要不落在已有植物的格子上即可。难度曲线可以很容易地通过修改这张表调节不用改逻辑代码。4. 把源代码编译成可执行文件Qt版本、编译器与常见报错4.1 从.pro文件开始而不是直接双击拿到.zip解压后的第一件事是看.pro和main.cpp。如果你的机器上同时装了学校要求的Qt 5.14和另一个Qt 5.15那么打开项目前最好确认qmake版本。在终端执行qmake -v输出里会带上5.15.2这样的版本号。如果命令行找不到qmake就得打开Qt安装目录下的“Qt 5.15.2 (MinGW 8.1.0)”命令行环境。一份最简.pro是这样QT core gui widgets CONFIG c11 TARGET PlantsVsZombies TEMPLATE app SOURCES main.cpp \ gameScene.cpp \ plant.cpp \ zombie.cpp \ bullet.cpp HEADERS gameScene.h \ plant.h \ zombie.h \ bullet.h RESOURCES game.qrc代码说明CONFIG c11不是必须的因为Qt 5.15默认支持C17但如果你用了constexpr建议显式写c17而不是c11。QT widgets必须放在四行内QGraphicsScene和QGraphicsView都来自Qt Widgets模块。4.2 使用qmake和make构建在Windows上如果你使用MinGW编译器命令行如下mkdir build cd build qmake ..\PlantsVsZombies.pro -spec win32-g mingw32-make -j4如果用MSVC则是nmake这里的关键是不要在Windows上用make命令因为MinGW的make叫做mingw32-makeMSVC环境用nmake。你可以通过qmake -spec参数观察具体差异。许多同学看到“Cant find file for -lgl”就直接跑去装OpenGL其实在MinGW下只要添加CONFIG console或LIBS -lopengl32。4.3 常见编译报错对症表错误信息真正的原因解决动作Qt serialport: unknown module安装Qt时没勾选SerialPort重装勾选或删除.pro中该模块incompatible Qt libraryPATH中存在多个Qt版本用绝对路径调用qmake和编译器QGraphicsScene not found.pro未加widgets模块QT widgetsfatal error: QSoundEffect: No such fileQt Multimedia模块未安装QT multimediacannot open output file .exe程序还在运行杀掉任务管理器中的旧进程注意如果你只是做植物大战僵尸完全不需要serialport、multimedia等等。凡是.pro里有的模块都必须是当前Qt环境实际安装过的。我见过最典型的错误是网上下载的源代码包含了串口模块但在常规Qt SDK中默认不安装于是整个项目无法编译。此时直接注释掉那行通常不会有副作用。4.4 发布到没有Qt环境的电脑windeployqt答辩演示不一定在开发机所以发布打包非常关键。构建出release版本后cd release windeployqt PlantsVsZombies.exe --release工具会根据exe的依赖自动复制。不过它不会复制qrc里png这些图片因为qrc编译进了二进制。真正需要额外关心的只有两种文件Qt平台插件和mingw32.dll。如果你用的是MinGW编译器还要把libgcc_s_dw2-1.dll、libstdc-6.dll、libwinpthread-1.dll一起拷贝否则目标机器提示缺少dll。一个推荐的做法是在空目录中执行windeployqt然后把需要的dll和qwindows.dll挪到exe旁边。不要直接COPY整个Qt安装目录那会有几百兆垃圾文件。5. 调试、参数调优与运行期性能让植物大战僵尸有“手感”5.1 用qDebug和场景统计定位逻辑错误Qt程序调试的第一工具是qDebug它可以输出到应用程序输出面板。在onTick开头这样写void GameScene::onTick() { if (m_debugMode) { qDebug() tick m_elapsedTick plants m_plants.size() zombies m_zombies.size() bullets m_bullets.size(); } }参数说明m_debugMode可以在命令行加--debug参数来控制默认false。这样你不必删日志只有演示时打开。使用统计信息能看到如果僵尸数量一直不增加可能是waveQueue中时间没有到如果子弹数量持续增长说明撞击后没有正确remove。5.2 调整植物花费和僵尸速度一张参数表讲清楚平衡课程设计打分者很看重可玩性。可玩性来自数值不是代码。把关键数值抽出到config.h表格呈现参数原版参考值推荐课设值说明阳光初始值50150让玩家开局能立刻种向日葵向日葵花费5050保持简单向日葵产阳光间隔12秒10秒加快节奏避免演示等待豌豆射手攻击力2025提高通关效率普通僵尸速度0.2px/tick0.25px/tick40%加速接近现实手速你会发现直接套原版数值反而拖沓课堂演示时3分钟一轮比较合适。调参后一定要跑一遍完整流程记下击破了几个僵尸、丢了几朵植物把测试结果写进课设报告这比代码本身更能体现工程意识。5.3 当僵尸数量增多时的碰撞优化如果课上测试时超过80个僵尸每帧的交集运算开始占用可观CPU帧率下降。优化方向有两个一是利用QGraphicsItem的boundingRectQt自带会做空间索引用起来。二是按行分桶把僵尸按row放入QVectorZombie* m_zombiesByRow[5]碰撞检测时子弹只需要查询对应行桶而不是全量遍历。第三种更简单的优化是把僵尸碰撞矩形缩窄减少误命中同时让CPU忙于矩形相交的几率低一些。在Qt中可以利用QGraphicsScene::items(const QRectF rect, Qt::IntersectsShape)它返回该矩形区域内的所有item。这里的坑是items不会区分类型所以还是要用qgraphicsitem_cast过滤僵尸。如果追求速度还是用自己的按行容器更好——代码更直白QVectorZombie* rowZombies m_zombiesByRow[bullet-row()]; for (Zombie *z : rowZombies) { if (z-sceneBoundingRect().intersects(bullet-sceneBoundingRect())) { // apply damage break; } }这样每次碰撞只需遍历同一行的僵尸数量通常不超过10个性能几乎没有任何压力。6. 课程设计答辩的演示顺序与代码讲解技巧6.1 演示流程先跑通再谈代码现场演示不要从冷启动开始。进门前先把程序运行到某个稳定状态存一个存档点如果课程设计支持读档这是最从容的方式如果没有存档功能提前在主菜单放一个“快速开始”按钮。演示时按“种植向日葵→阳光收集→种植豌豆→僵尸出现→战斗结束”的顺序走时间控制在2分钟以内。每一段最好都有可见的变量变化比如阳光数、僵尸剩余血量这些都是老师能直观看到的东西。6.2 讲清楚三个“技术关键词”答辩时老师不一定有时间看完全部源码但会对这样几个关键词有印象多态、事件循环、碰撞检测。你可以打开GameEntity的类图先说sprite、hp、updateByTick是公有的行为再点开Peashooter指出它重写paint所以每个实体自己决定画成什么。接着用箭头在onTick和updateByTick之间绕一圈说明主循环由QTimer驱动。碰撞部分打开handleBulletCollisions说明你做了同行过滤。全程不要念代码而是像讲架构一样配合鼠标勾选关键行老师会认为你确实理解了工程结构。6.3 两个给课程设计加分的实用小功能如果还有余力我给两个低成本功能。第一个是用QSettings保存最高分与关卡进度适当展示ini文件内容第二个是用QShortcut设置键盘快捷键比如空格键暂停数字键1/2/3快速种植不同植物。这两个功能实现起来都在30行以内但它们能证明你具备真实Qt开发习惯而不仅仅是会用控件。最后的演示重点一定是稳定性和手感宁可少一个功能也不要出现演示到一半僵尸卡住这种翻车现场。本文还有配套的精品资源点击获取
