简介:这是一份面向高校学生的C++程序设计期末课程设计参考源码,以经典塔防游戏植物大战僵尸为题材,基于QT框架与C++语言实现,适合正在学习面向对象编程、GUI开发或准备课程设计的学习者参考借鉴。资源包共31个文件,约20KB,以cpp与h源文件为主体,另有qrc资源文件、ui界面文件、pro工程文件及ts翻译文件,覆盖游戏实体、植物、僵尸、子弹、卡片、场景、动画与按钮等模块,结构清晰便于按功能阅读。目前已有3083人学习下载,热度较高。通过研读源码,读者可以了解QT中QGraphicsView与QGraphicsScene构建2D游戏画面的方式、事件驱动模型与多线程处理思路,以及C++继承多态在游戏对象复用中的实际运用,同时涉及文件I/O、资源管理与调试测试等环节,对提升软件工程实践能力与完成同类课程设计具有直接参考价值。
1. 从一份 QT 植物大战僵尸源码说起:C++ 课程设计到底该做成什么样
很多人搜「基于QT开发的植物大战僵尸游戏源代码,C++程序设计期末课程设计」,心里想的其实是同一件事:期末要交一个能跑起来、能演示、代码量看着像回事的 C++ 项目,最好还能顺手把 QT 学会。植物大战僵尸这个题材被反复选中,不是因为它简单,而是因为它天然覆盖了 C++ 课程设计要考察的几乎所有点——类与继承、容器与迭代器、定时器与事件循环、图形绘制、资源管理、状态机。你把它做出来,等于把一学期的知识点串了一遍。
但真正动手的人很快会发现,网上流传的源码大多有两个问题:一是编译不过,二是看不懂。编译不过多半是 QT 版本和模块对不上,比如qt unknown module in qt:serialport这类报错,或者fatal: cannot mix incompatible qt library这种库混用问题;看不懂则是因为很多代码把逻辑全塞进一个mainwindow.cpp,没有分层。这篇笔记就按一线做法,把这份课程设计从环境、架构、核心玩法到避坑完整拆一遍,让你能自己复现,而不是复制粘贴一堆跑不起来的代码。
2. 环境与工程骨架:QT 5.15.2 + qmake 的最小可编译结构
2.1 为什么课程设计优先选 QT 5.15.2 而不是 QT 6
选版本这件事,课程设计里比想象中重要。QT 6 把QMediaPlayer、QSound等模块做了较大调整,很多老教程里的音频播放代码在 QT 6 下直接编译失败;而 QT 5.15.2 是 5.x 系列最后一个长期维护版本,QGraphicsView、QTimer、QPainter这些课程设计高频用到的类接口稳定,网上能查到的资料也最多。热搜里qt 5.15.2下载安装长期靠前,说明踩过版本坑的人不少。
安装时只勾选Qt 5.15.2下的MinGW 64-bit和Qt Creator,不要勾源码和 Debugging Tools,能省下好几个 G。装完在 Qt Creator 里工具 → 选项 → Kits确认编译器是 MinGW,否则后面会出现找不到g++的情况。如果你用 VS Code 写代码,vscode配置c/c++环境那套c_cpp_properties.json要额外把 QT 的 include 路径加进去,否则#include <QApplication>会标红,但能编译——这是编辑器索引问题,不是代码问题。
2.2 用 qmake 建工程:.pro 文件怎么写才不翻车
课程设计不建议上 CMake,qmake 的.pro文件更直观,老师也更容易看懂。一个植物大战僵尸工程的最小.pro长这样:
QT += core gui widgets # 如果要做音效,加 multimedia;但 QT5 的 multimedia 在部分机器上要装解码器 QT += multimedia TARGET = PlantsVsZombies TEMPLATE = app CONFIG += c++11 SOURCES += \ main.cpp \ mainwindow.cpp \ gameScene.cpp \ plant.cpp \ zombie.cpp \ bullet.cpp \ sun.cpp HEADERS += \ mainwindow.h \ gameScene.h \ plant.h \ zombie.h \ bullet.h \ sun.h RESOURCES += res.qrcQT += widgets是必须的,只写gui会缺QApplication。CONFIG += c++11保证能用auto、范围 for 和nullptr,课程设计里用这些能让代码短不少。RESOURCES += res.qrc是把图片、音效打包进可执行文件的关键,.qrc里用相对路径引用images/peashooter.png这类资源,运行时用:/images/peashooter.png访问。很多人图片加载不出来,就是.qrc没加进.pro或者路径写成了磁盘绝对路径。
提示:
.qrc文件修改后必须重新执行 qmake(Qt Creator 里是「构建 → 执行 qmake」),否则新加的资源不会生效,这个坑几乎每个人都踩过一次。
2.3 工程目录分层:别把两千行塞进 mainwindow
课程设计代码量通常在 2000 到 4000 行,如果全堆在mainwindow.cpp,答辩时老师翻两页就不想看了。我一般按职责分四层:main.cpp只负责QApplication和显示主窗口;mainwindow管菜单、关卡切换、暂停;gameScene继承QGraphicsScene,管游戏主循环和碰撞检测;plant、zombie、bullet、sun各自继承QGraphicsItem,管自己的绘制和状态。这样每个文件两三百行,逻辑清晰,也方便你单独调试某一种僵尸的行为。
3. 核心玩法实现:从阳光、种植到僵尸波的完整链路
3.1 用 QGraphicsScene 搭游戏主循环:定时器怎么设才不卡
QT 做 2D 游戏,主流做法是QGraphicsView + QGraphicsScene + QGraphicsItem,而不是自己在paintEvent里画所有东西。QGraphicsScene自带空间索引和碰撞检测,省掉大量手写代码。游戏主循环用QTimer驱动,常见做法是 30 到 60 帧:
// gameScene.cpp GameScene::GameScene(QObject *parent) : QGraphicsScene(parent) { // 16ms 约等于 60 帧,课程设计用 33ms(30 帧)也够 m_timer = new QTimer(this); connect(m_timer, &QTimer::timeout, this, &GameScene::updateGame); m_timer->start(33); // 阳光掉落定时器,每 5 秒掉一个 m_sunTimer = new QTimer(this); connect(m_sunTimer, &QTimer::timeout, this, &GameScene::spawnSun); m_sunTimer->start(5000); } void GameScene::updateGame() { // 遍历所有僵尸,推进移动 for (Zombie *z : m_zombies) { z->advance(); // 内部按 speed 移动 x } // 遍历子弹,检测与僵尸碰撞 for (Bullet *b : m_bullets) { QList<QGraphicsItem *> hits = b->collidingItems(); for (QGraphicsItem *item : hits) { Zombie *z = dynamic_cast<Zombie *>(item); if (z) { z->takeDamage(b->damage()); removeItem(b); m_bullets.removeOne(b); delete b; break; } } } // 清理死亡对象,避免野指针 cleanupDead(); }m_timer->start(33)里的 33 是毫秒,决定游戏刷新频率。设太小(比如 5)会吃满 CPU,设太大(比如 100)僵尸会一跳一跳。collidingItems()返回与子弹相交的所有 item,用dynamic_cast判断是不是僵尸,这是 QT 里做碰撞的惯用法。注意删除 item 时要从容器里同步移除,否则下一帧遍历会访问已释放内存,这是课程设计里最常见的崩溃原因。
3.2 植物与僵尸的类设计:继承 QGraphicsItem 的四个必写函数
每种植物和僵尸都继承QGraphicsItem,必须实现两个纯虚函数boundingRect()和paint(),通常还会重写advance()和shape():
// zombie.h class Zombie : public QGraphicsItem { public: Zombie(int hp, qreal speed, QGraphicsItem *parent = nullptr); QRectF boundingRect() const override; void paint(QPainter *painter, const QStyleOptionGraphicsItem *option, QWidget *widget) override; void advance(int phase) override; // 每帧移动 void takeDamage(int dmg) { m_hp -= dmg; if (m_hp <= 0) m_dead = true; } bool isDead() const { return m_dead; } private: int m_hp; qreal m_speed; bool m_dead = false; QPixmap m_pixmap; }; // zombie.cpp QRectF Zombie::boundingRect() const { return QRectF(0, 0, 80, 100); // 必须和图片尺寸一致 } void Zombie::paint(QPainter *painter, const QStyleOptionGraphicsItem *, QWidget *) { painter->drawPixmap(0, 0, m_pixmap); } void Zombie::advance(int phase) { if (phase == 0) return; // phase 0 是准备阶段,不移动 if (m_dead) return; setX(x() - m_speed); // 僵尸向左走 }boundingRect()返回的矩形必须覆盖paint()画出来的所有像素,否则碰撞检测会漏判,表现为子弹穿过僵尸不掉血。advance(int phase)里phase == 0时不能动,这是 QT 的约定,很多新手在这里写移动逻辑导致每帧移动两次。m_speed建议设成 0.3 到 0.8 像素每帧,配合 33ms 定时器,僵尸横穿屏幕大概十几秒,节奏接近原版。
3.3 阳光经济与种植判定:鼠标事件和网格坐标换算
种植是玩家交互的核心。GameScene重写mousePressEvent,把鼠标坐标换算成网格行列,再判断该格是否已有植物、阳光是否够:
void GameScene::mousePressEvent(QGraphicsSceneMouseEvent *event) { QPointF pos = event->scenePos(); int col = int(pos.x()) / GRID_W; // GRID_W = 80 int row = int(pos.y()) / GRID_H; // GRID_H = 100 if (col < 0 || col >= 9 || row < 0 || row >= 5) return; if (m_selectedPlant == PlantType::Peashooter) { if (m_sun < 100) return; // 阳光不够 if (m_grid[row][col] != nullptr) return; // 已有植物 Peashooter *p = new Peashooter(); p->setPos(col * GRID_W, row * GRID_H); addItem(p); m_grid[row][col] = p; m_sun -= 100; emit sunChanged(m_sun); // 通知 UI 更新 } }scenePos()拿到的是场景坐标,除以格子宽高得到行列。m_grid是个Plant*[5][9]二维数组,记录每格植物,既用于防重叠,也用于僵尸啃食时查找目标。emit sunChanged是 QT 信号槽,让主窗口顶部的阳光数字实时刷新。这里有个细节:int(pos.x())对负数会向零取整,所以要先判断边界再算行列,否则点到场景外会数组越界。
4. 避坑与排查:课程设计里最容易翻车的五件事
4.1 现象:编译报unknown module in QT: serialport
原因:.pro里写了QT += serialport,但安装 QT 时没勾 Qt Serial Port 模块,或者这个模块本来就不需要。植物大战僵尸用不到串口。
解决:直接从.pro删掉这一行,重新执行 qmake。如果确实需要串口,回到 Qt Maintenance Tool 勾选对应模块再装。热搜里qt unknown module in qt:serialport出现频率很高,本质都是模块没装或写错。
4.2 现象:运行时报could not find the Qt platform plugin "linuxfb"
原因:在 Windows 上跑了一个为 Linux 编译的程序,或者环境变量QT_QPA_PLATFORM被设成了linuxfb。这个插件是 Linux 帧缓冲用的,Windows 上不存在。
解决:删掉或改掉QT_QPA_PLATFORM环境变量,Windows 下默认应该是windows。如果是在 Qt Creator 里跑,检查「项目 → 运行 → 环境」里有没有手动加过这个变量。
4.3 现象:僵尸死亡后程序随机崩溃
原因:僵尸血量归零后delete了对象,但m_zombies容器里还留着这个指针,下一帧advance()访问已释放内存。
解决:删除对象时同步从容器移除,或者用QList<QPointer<Zombie>>让指针自动置空。我一般写一个cleanupDead(),每帧末尾统一遍历容器,把isDead()为真的对象removeItem、delete、removeOne,顺序不能反。
4.4 现象:图片显示成白块或程序找不到资源
原因:.qrc没加进.pro,或者代码里用了磁盘路径images/xx.png而不是资源路径:/images/xx.png。
解决:确认.pro里有RESOURCES += res.qrc,代码里统一用冒号开头的资源路径。改完.qrc一定要重新执行 qmake,只点「构建」不会重新编译资源。
4.5 现象:子弹穿过僵尸不掉血
原因:boundingRect()返回的矩形比实际图片小,或者子弹移动速度太快,一帧跨过了僵尸的碰撞盒。
解决:把boundingRect()调成和pixmap尺寸一致,必要时四周各加 2 像素余量。子弹速度超过 15 像素每帧时,改成射线检测或者把子弹拆成多段移动,避免「隧穿」。
5. 进阶技巧:让课程设计从及格变成加分项
5.1 用状态机管僵尸行为,而不是一堆 if
基础版僵尸只会走和啃,加分版可以加「被冰冻减速」「血量低于一半掉胳膊」这些状态。与其在advance()里堆if (m_frozen) ... else if (m_lowHp) ...,不如用一个枚举加状态切换:
enum class ZombieState { Walking, Eating, Frozen, Dying }; void Zombie::advance(int phase) { if (phase == 0 || m_state == ZombieState::Dying) return; switch (m_state) { case ZombieState::Walking: setX(x() - m_speed); if (hasPlantAhead()) m_state = ZombieState::Eating; break; case ZombieState::Eating: m_eatTimer += 33; if (m_eatTimer >= 1000) { m_eatTimer = 0; eatPlant(); } if (!hasPlantAhead()) m_state = ZombieState::Walking; break; case ZombieState::Frozen: m_frozenTimer -= 33; if (m_frozenTimer <= 0) m_state = ZombieState::Walking; break; default: break; } }状态机的好处是每种行为独立,加新状态不用改老逻辑。答辩时老师问「僵尸怎么实现不同行为」,你能画出状态迁移图,比说「一堆 if」高一个档次。
5.2 用 QSoundEffect 加音效,注意 QT5 的坑
音效是课程设计里性价比最高的加分项,几行代码就能让演示效果翻倍。QT5 用QSoundEffect播短音效:
// 在 GameScene 构造里 m_shootSound = new QSoundEffect(this); m_shootSound->setSource(QUrl("qrc:/sounds/shoot.wav")); m_shootSound->setVolume(0.5); // 发射子弹时 m_shootSound->play();注意QSoundEffect只支持 wav 格式,mp3 要用QMediaPlayer,但后者在部分 Windows 机器上要装解码器,课程设计现场演示容易翻车。我一般统一转成 wav,采样率 44100、16 位、单声道,文件小又稳。.pro里记得加QT += multimedia。
5.3 验证清单:交作业前自己跑一遍
| 检查项 | 预期结果 | 不通过时看哪里 |
|---|---|---|
| 全新解压后编译 | 无 error,warning 少于 5 条 | .pro模块、qmake 是否执行 |
| 图片音效显示 | 无白块、无静音 | .qrc路径、资源是否重新编译 |
| 连续玩 5 分钟 | 不崩溃、内存不持续涨 | 死亡对象是否清理、定时器是否泄漏 |
| 僵尸吃植物 | 植物消失、僵尸继续走 | m_grid是否同步清空 |
| 阳光不足时种植 | 无反应、不扣阳光 | mousePressEvent里的判断顺序 |
这张表我每次交课程设计前都会跑一遍,尤其是「连续玩 5 分钟」这条,能揪出大部分内存问题。很多同学演示时前两分钟好好的,第三分钟突然崩,就是对象没清理干净。
5.4 一个我自己的习惯
我做完任何 QT 课程设计,都会把mainwindow.cpp的行数控制在 300 行以内,超了就说明有逻辑该挪到gameScene或单独的类里。这个习惯是被答辩逼出来的——老师翻代码时,文件越短、职责越单一,越容易给高分。另外,git commit至少分五次:环境搭好一次、场景能显示一次、种植能交互一次、僵尸能走一次、音效加完一次。这样万一改崩了,git reset就能回到上一个能跑的状态,比通宵重写强得多。希望帮到你。
本文还有配套的精品资源,点击获取