ARTICLE DETAIL

资讯详情

深耕网站建设、视觉设计与SEO优化的一线实战洞察。

C++课程设计:基于Qt的信号槽与类继承实现植物大战僵尸

C++课程设计:基于Qt的信号槽与类继承实现植物大战僵尸 简介面向C课程设计与Qt初学者的完整期末项目方案以植物大战僵尸为选题演示了Graphics View框架下的游戏开发全流程。资源包含完整可编译源码与开发文档源码采用面向对象设计通过封装、继承与多态实现植物基类太阳花、豌豆射手、坚果等、僵尸基类普通僵尸、路障僵尸及地图、卡片槽等模块层次清晰适合复习Qt对象模型与碰撞检测等知识点。压缩包共111个文件以25个cpp与25个头文件为核心配以gif动图示意运行效果、png/jpg素材、pro工程文件、pdf开发文档及mp4演示视频等整体大小约39.43MB。随附的目录结构与注释便于快速定位各功能模块。目前已有3825人学习下载。通过本套代码读者既能获得可运行的课程设计成品也能学习如何拆分游戏实体类、管理场景与视图交互为后续拓展更多植物或僵尸类型提供可复用的框架参考。1. 一门 C 期末设计为什么选 Qt 做植物大战僵尸植物大战僵尸这个选题在 C 课程设计里出现频率极高几乎每个学校、每一届都有人做。原因很直接:它比图书管理系统、学生信息管理系统更有画面感但又不像 3D 引擎那样依赖复杂数学;它要求你同时处理对象管理、事件响应、界面刷新、资源加载这些恰好是 C 面向对象和 Qt 框架最擅长的部分。把这个项目做完等于把类继承、多态、信号槽、定时器、事件循环、智能指针全部过了一遍期末答辩时老师问什么你都有东西可讲。但「QT 开发的植物大战僵尸源代码」这个标题本身有误导性。拿到这种压缩包的人最常犯的错是直接打开 .pro 文件点运行结果要么缺少编译器套件要么 Qt 版本不匹配要么资源文件路径带中文导致素材加载失败。真正的难点从来不是写不写得出来而是能不能在你自己的电脑上把它跑起来、改得动、讲得清。这篇文按「搭建工程 → 设计类结构 → 实现核心玩法 → 调试验收」的顺序给你一套可以直接落地的方案。2. 环境与工程骨架:Qt 版本、编译器口径和第一个可运行窗口2.1 选 Qt 5.15.2 还是 Qt 6,先看老师的验收环境课程设计有个现实约束:不是你想用哪个版本就用哪个版本而是机房、老师电脑、你室友电脑上装了什么。Qt 6 把 QFontMetrics 宽度函数改了名、去掉了部分 QRegExp 接口代码在 Qt 5 上编译通过拿到 Qt 6 上报错的情况很常见。建议默认用 Qt 5.15.2它是在 Windows 上最稳的 LTS 版本Qt 5.15.2 下载安装后自带 Qt CreatorMinGW 和 MSVC 套件二选一即可。如果你的开发机是 Win11MSVC 2019 套件需要 Visual Studio 2019 或 2022 的 C 桌面开发组件否则会报cannot find -lxxx这类链接错误。装完 Qt 后在 Qt Creator 里新建项目,选择Qt Widgets Application,这一步不要选Qt Quick Application——后者走 QML 声明式语法虽然做动画更方便但课程设计要考察的是 C 代码量QML 写多了老师会认为你在躲 C。项目名称用拼音或英文比如PvZGame不要用植物大战僵尸做项目名Qt Creator 对中文路径和中文项目名的支持在部分版本上有编码问题。2.2 用 qmake 还是 CMake,取决于你要不要跨平台Qt Creator 新建项目时会问构建系统默认推荐 CMake但课程设计场景我建议选 qmake。理由很实在:qmake 的.pro文件三行就能描述一个完整项目老师拿到你的源码包用 Qt Creator 打开.pro文件直接构建不需要额外配置 CMakeLists而 CMake 虽然更现代但你要多写一份CMakeLists.txt里面find_package(Qt5 COMPONENTS Widgets)的写法一旦和安装路径对不上又是一轮排错。在「能跑起来」这个目标面前qmake 的简单就是最大优势。一个最简.pro文件长这样:QT core gui widgets TARGET PvZGame TEMPLATE app SOURCES main.cpp \ gamewidget.cpp HEADERS gamewidget.h RESOURCES res.qrc注意QT widgets这行。Qt 5 之后 Widgets 模块从gui里拆出来了不写这行QApplication和QWidget直接编译报错。RESOURCES res.qrc用来挂载图片素材植物大战僵尸的素材文件多全部塞进 qrc 资源系统里发布时只带一个 exe不会出现「图片路径找不到」的问题。2.3 用 QTimer 驱动游戏循环而不是 QThread植物大战僵尸不是回合制游戏僵尸每时每刻都在移动植物按固定间隔发射豌豆。初学者容易想到多线程:一个线程刷界面一个线程算逻辑。但在 Qt 里所有涉及 QWidget 和 QGraphicsScene 的操作必须发生在主线程子线程直接操作界面会触发QObject::setParent: Cannot set parent, new parent is in a different thread之类的运行时崩溃。正确的做法是用QTimer做 tick 驱动每帧更新一次游戏状态:// gamewidget.h #include QTimer class GameWidget : public QWidget { Q_OBJECT public: explicit GameWidget(QWidget *parent nullptr); private slots: void onTick(); private: QTimer m_timer; }; // gamewidget.cpp GameWidget::GameWidget(QWidget *parent) : QWidget(parent) { m_timer.setInterval(30); // 约 33 fps connect(m_timer, QTimer::timeout, this, GameWidget::onTick); m_timer.start(); } void GameWidget::onTick() { // 更新僵尸位置、检测碰撞、刷新画面 update(); }setInterval(30)的意思是每 30 毫秒触发一次timeout信号。这个数值不是越小越好QTimer的最小时钟精度由操作系统决定Windows 上约 15ms 左右你设成 1ms 也不会得到 1000fps。对于植物大战僵尸这种节奏偏慢的塔防游戏30ms 到 50ms 是合理区间既能保证动画平滑又不会让 CPU 空转。注意connect用的是新语法:信号和槽都取地址编译期就能检查参数类型比旧的SIGNAL()/SLOT()宏写法安全得多。3. 类设计先行:用 C 继承把植物、僵尸、子弹组装成游戏框架3.1 抽象基类:Entities 不该是一个万能的 God Class很多课程设计写着写着所有游戏对象都堆在GameWidget里:一个QListQPointF存僵尸位置一个QListint存植物类型一个QListQPointF存子弹位置更新时用switch(type)判断类型再做不同处理。这样写能跑但答辩时老师问「你的类设计是怎样的」会很尴尬,因为根本没有类设计。C 课程设计的核心考察点就是抽象和继承哪怕游戏功能再完整没有类分层也拿不到高分。合理的做法是设计四个类层级。最上层是一个抽象基类GameEntity它保存所有游戏对象共有的属性:坐标、生命值、所属行列。往下拆出Plant、Zombie、Projectile三个派生类分别承担种植、移动、攻击逻辑。再用一个GameScene类管理它们之间的交互。这样每个类只负责一件事调试时定位问题也快。// entity.h #pragma once #include QGraphicsPixmapItem class GameEntity : public QObject, public QGraphicsPixmapItem { Q_OBJECT public: explicit GameEntity(QGraphicsItem *parent nullptr); virtual void update() 0; // 每帧调用 int hp() const { return m_hp; } void setHp(int hp) { m_hp hp; } protected: int m_hp; }; // plant.h class Plant : public GameEntity { Q_OBJECT public: explicit Plant(QGraphicsItem *parent nullptr); void update() override; // 生成子弹、播放受击动画 int row() const { return m_row; } int col() const { return m_col; } private: int m_row; int m_col; int m_shootTimer; };这里的多重继承要解释一下。QObject提供信号槽能力QGraphicsPixmapItem提供在场景中的渲染和位置能力两者都是 Qt 定义的类不能互相替换所以GameEntity同时继承它们。有人会问:为什么不直接用QGraphicsObject,它就是 Qt 官方把两者合并好的类?因为QGraphicsObject不能直接设置图片像素图你仍然要内部维护一个QPixmap成员绕了一圈不如直接多重继承来得干净。注意多重继承时Q_OBJECT宏必须写在类里否则connect时信号槽无法识别。3.2 场景与窗口:谁负责碰撞谁负责画界面GameScene继承QGraphicsScene负责所有游戏对象的增删和碰撞检测;GameView继承QGraphicsView负责把场景渲染到窗口上。这个分层是 Qt 图形视图框架的标准用法QGraphicsView处理鼠标键盘事件、QGraphicsScene管理图元生命周期、QGraphicsItem表达单个对象。初学者容易把三者混在一起写最后GameWidget里既有QGraphicsScene成员又有QVectorPlant*自己管理图元逻辑就拧巴了。推荐的堆叠顺序是:最底层:草坪背景一张QGraphicsPixmapItem中层:植物、僵尸、子弹按zValue排序顶层:阳光计数、冷却时间等 UI 元素// gamescene.h class GameScene : public QGraphicsScene { Q_OBJECT public: explicit GameScene(QObject *parent nullptr); void addPlant(int row, int col, Plant *plant); void spawnZombie(int row); void clearBullets(); protected: void mousePressEvent(QGraphicsSceneMouseEvent *event) override; private: QVectorPlant* m_plants; QVectorZombie* m_zombies; QVectorProjectile* m_bullets; };m_plants和m_zombies用QVector存裸指针这里不建议用std::vectorstd::unique_ptrPlant。原因是QGraphicsScene在析构时会自动删除所有添加到场景里的QGraphicsItem你再用智能指针去管会出现 double free。这是 Qt 图形视图框架独有的生命周期规则:图元一旦addItem()进场景所有权就转移给了场景。你要做的只是在场景析构前把图元从场景中移除或者干脆不自己 delete。这个细节是答辩高频考点务必讲清楚。3.3 信号槽在游戏里的三个典型用途信号槽不只是按钮点击的回调在游戏里至少有三种常见用法。第一种是对象间解耦:植物发射子弹不需要植物直接调用GameScene::addBullet()而是发一个shootRequested(QPointF pos, int row)信号场景收到信号后再创建子弹对象。这样植物类不依赖场景类单独拿出来做单元测试也可以。第二种是 UI 刷新:阳光数量变化时植物类发sunChanged(int)信号界面上的QLabel收到信号后更新数字。如果不用信号槽你得在每帧的update()里去查询阳光数量再判断要不要刷新写法啰嗦且容易漏。第三种是游戏状态通知:僵尸到达房子时发gameOver()信号QMessageBox弹窗和背景音乐停止都挂在同一个信号上互不干扰。class GameScene : public QGraphicsScene { Q_OBJECT signals: void sunChanged(int value); void gameOver(); };信号槽的性能在课程设计级别完全不是问题。Qt 5 的信号槽是类型安全的直接连接同一线程内最终会退化为一次直接函数调用开销远小于你想象的动态派发。提示:写派生类时如果GameEntity里定义了带参数的构造函数派生类的构造函数必须显式调用它。4. 核心玩法回路:种植逻辑、僵尸移动、碰撞判定与刷新机制4.1 种植判定:阳光够不够、格子上有没有东西、冷却结束没植物大战僵尸的种植逻辑是三重条件叠加。第一重是资源判断:当前阳光数是否大于植物价格。第二重是位置判断:鼠标点击的位置是否落在 5×9 的草坪格子里该格子是否为空。第三重是冷却判断:这类植物是否还在冷却期。三重条件写在一起就是一段教科书级别的「卫语句」示例:// gamescene.cpp void GameScene::tryPlant(int row, int col, PlantType type) { // 卫语句:不满足条件直接 return if (row 0 || row 5 || col 0 || col 9) return; if (m_grid[row][col] ! nullptr) return; if (m_sunCount PlantConfig[type].cost) return; if (m_cooldown[type] 0) return; Plant *plant PlantFactory::create(type); plant-setPos(col * CELL_WIDTH, row * CELL_HEIGHT); addItem(plant); m_grid[row][col] plant; m_sunCount - PlantConfig[type].cost; m_cooldown[type] PlantConfig[type].cooldown; emit sunChanged(m_sunCount); }这段代码有两个设计点值得展开。PlantConfig是一个静态配置表用struct加数组实现保存每种植物的价格、血量、攻击间隔、冷却时间。把游戏数值从类代码里抽出来放在配置表里后续调平衡只改数据不改逻辑这是游戏开发里常见的>bool isHit(const Projectile *bullet, const Zombie *zombie) { if (bullet-row() ! zombie-row()) return false; // 不同行不打 QRectF bulletRect bullet-boundingRect().translated(bullet-pos()); QRectF zombieRect zombie-boundingRect().translated(zombie-pos()); return bulletRect.intersects(zombieRect); }translated(pos())这一行是核心。图元的boundingRect()返回的是相对坐标,也就是图元自己坐标系下的矩形;要拿到实际场景坐标必须把它平移到图元所在位置。漏掉translated是新手最常见的 bug,表现是子弹明明打中了僵尸却不掉血,单步调试发现bulletRect永远在原点附近。4.3 僵尸生成:按波次、按行、控制屏幕上的同时存在数僵尸不能无限生成否则游戏必输。课程设计要体现「可玩性」最简单的做法是分波次生成:每隔一段时间刷一波每一波数量递增行号随机:void GameScene::spawnWave() { m_waveCount; int count qMin(3 m_waveCount / 2, 10); // 每波数量封顶 10 for (int i 0; i count; i) { int row QRandomGenerator::global()-bounded(5); Zombie *zombie new Zombie(); zombie-setRow(row); zombie-setPos(SCENE_WIDTH i * 60, row * CELL_HEIGHT); addItem(zombie); m_zombies.append(zombie); } }QRandomGenerator::global()-bounded(5)是 Qt 5.10 之后推荐的随机数接口返回值在 0 到 4 之间。早期的qrand() % 5有两个问题:一是需要先qsrand()播种否则每次运行生成的序列相同二是取模运算会产生轻微的概率偏移。QRandomGenerator把这两件事都处理好了不需要额外播种。qMin在这里做数量封顶防止后期波次生成过多僵尸导致同屏对象数量爆炸,既影响性能又让游戏变得不可能赢。4.4 每帧刷新:遍历对象、删死对象、按 Z 值重绘上一章提到用QTimer驱动刷新那onTick()里到底做什么?按顺序来:先遍历植物执行生长和射击逻辑;再遍历僵尸执行移动和攻击逻辑;然后遍历子弹执行移动并检测碰撞;最后把hp 0的对象从场景中移除。顺序不能乱因为植物打出的子弹要在僵尸移动之前判定否则子弹和僵尸的位置关系会错一帧视觉上就是「打到了但不掉血」。删除对象时有个坑:不能在遍历容器的过程中直接delete然后从QVector里移除,迭代器会失效。常见做法是先把要删的图元指针收集到一个临时QList里遍历结束后再统一处理:// 先标记 QListQGraphicsItem* to_remove; for (Zombie *zombie : qAsConst(m_zombies)) { if (zombie-hp() 0) { to_remove.append(zombie); } } // 后删除 for (QGraphicsItem *item : to_remove) { removeItem(item); delete item; } // 同步容器 m_zombies.erase(std::remove_if(m_zombies.begin(), m_zombies.end(), [](Zombie *z) { return z-hp() 0; }), m_zombies.end());std::remove_if配合erase是 C 里从容器删除元素的惯用写法,remove_if把满足条件的元素移到容器末尾并返回新的逻辑结尾erase再把这部分真正删掉。用 Lambda 捕获hp() 0条件比写循环简练。你如果对 Lambda 不熟这里是一个现成的学习入口。提示:qAsConst用于遍历只读容器避免 Qt 的隐式共享implicit sharing在遍历过程中触发深拷贝。容器很小的时候影响不大但养成习惯没坏处。5. 让课程设计不翻车:内存问题、中文字体、素材路径和答辩预演5.1 排查 Qt 典型的「闪退无提示」陷阱植物大战僵尸类项目的崩溃大多是空指针和 double free,而不是算法错误。两个高频场景:一是Zombie死亡后自身调用deleteLater(),但此时场景还在下一帧遍历m_zombies列表通过一个已释放的指针访问对象,直接段错误。解决方法是统一由场景管理删除,而不是对象自己删自己。二是QPixmap加载失败时返回空图片,把空QPixmap传给setPixmap()不会报错,但绘制时会异常。养成加载后判断的习惯:QPixmap pixmap(:/images/zombie_normal.png); if (pixmap.isNull()) { qWarning() Failed to load zombie_normal.png; }qWarning()会输出到 Qt Creator 的「应用程序输出」窗口比printf多带文件行号。调试时把素材路径写错的问题瞬间就能发现。我一般会建议在main()里加一行qSetMessagePattern([%{file}:%{line}] %{message}),让日志带源码位置,定位问题快很多。5.2 中文字体与乱码:MSVC 下源码文件的编码战争在 Windows 上用 MSVC 编译时源码文件里的中文字符串比如植物名称、提示文本经常变成乱码。根因是 MSVC 默认按本地代码页GBK解析源文件而 Qt Creator 默认把文件存成 UTF-8。中文 向日葵 在 UTF-8 里的字节序列被 MSVC 按 GBK 解码后就是另一回事了。解决方式是在.pro文件里加编译选项:msvc { QMAKE_CXXFLAGS /utf-8 }对于无法改.pro的场合,也可以在源文件顶部加#pragma execution_character_set(utf-8)但 VS2015 之后这个 pragma 已经弃用统一用/utf-8编译选项更干净。记住规则:Qt 5 默认所有字符串按 UTF-8 处理C 源码也用 UTF-8 保存两边的编码对齐就没有乱码问题。如果你的代码写的是英文界面,这一步可以跳过,植物大战僵尸的植物名翻译成英文反而更通用,比如Sunflower、Peashooter,避免答辩演示时出乱码事故。5.3 交付打包不裸奔:发布 exe 时带对 Qt 的 DLL课程设计要交到老师电脑上跑往往不是直接交源码.zip,而是交一个打包好的运行目录。最稳的做法是用 Qt Creator 左侧的Release模式构建,然后在命令行运行windeployqt.exe:cd /d D:\PvZGame\build-PvZGame-Desktop_Qt_5_15_2_MinGW_64_bit-Release\release D:\Qt\5.15.2\mingw81_64\bin\windeployqt.exe PvZGame.exewindeployqt会自动扫描 exe 依赖的 Qt 模块把Qt5Core.dll、Qt5Gui.dll、Qt5Widgets.dll以及 platforms 插件目录一并复制到 exe 旁边。如果你的代码里用了音效还会带上Qt5Multimedia.dll。拷贝完把整个目录压成 zip 提交即可。注意运行目录里不能有Qt5Cored.dll——带d后缀的是 Debug 版 DLL,Release 版 exe 加载它会报配置错误;反之 Debug 版 exe 找不到Qt5Cored.dll直接闪退。5.4 答辩现场别踩的三个演示坑越简单的地方越容易翻车。第一个坑是双击 exe 没反应——Windows 把标准输出重定向到了空设备qWarning()的日志看不到。在 Qt Creator 里直接运行没错,双击 exe 就没了,先在命令行里启动 exe 看有没有报错输出。第二个坑是字体缩放。答辩教室的显示器可能设了 125% 或 150% 缩放Qt 5 默认启用高 DPI 缩放,界面会模糊或布局错位。提前在main()里关掉缩放:int main(int argc, char *argv[]) { QApplication::setAttribute(Qt::AA_EnableHighDpiScaling, false); QApplication app(argc, argv); // ... }Qt::AA_EnableHighDpiScaling设置为 false,界面强制按 100% 缩放渲染字体大小统一,观感清晰。代价是在高分屏上略微发虚,但课程设计演示场景稳定优先。第三个坑是演示时鼠标点太快,连续种了十棵向日葵,阳光瞬间清空,场面尴尬。这不算 bug,但答辩问「你是怎么做经济系统平衡的」时,如果你答不上来,可以从「每株植物定价参考其产出效率」这个角度解释:向日葵产出阳光的累计收益应该大于其种植成本,否则这个植物没有使用价值。哪怕数值不精细,有这个思考就会让老师觉得你做了游戏性设计。把这四件事做扎实——编码统一、内存交由场景管理、发布目录完整、演示路径提前验证——课程设计从「写出来」到「稳过」的距离就填补上了。最后交代码之前,删掉build-开头的目录和*.pro.user文件,只保留源码、资源.qrc、图片素材和项目描述文档,压缩包命名带上学号姓名,路径不要有中文,这份源码就是一份可以直接给下一届当范式参考的作品。本文还有配套的精品资源点击获取
返回列表