简介:这是一份基于Qt与C++框架编写的简易植物大战僵尸游戏课程设计源码,面向计算机相关专业的在校学生、教师及企业员工,可直接用于课程设计、毕业设计或项目初期演示,也适合入门者学习Qt游戏开发。压缩包共37个文件,包含17个cpp源文件、16个h头文件,以及pro工程配置、qrc资源文件和README说明,整体仅32KB,结构精简、模块划分清晰。该工程已测试运行成功,实现了主界面、阳光生成、植物种植与射击、僵尸刷新等核心玩法;同时覆盖地图绘制、植物冷却、僵尸状态等关键模块,阅读源码有助于理解事件处理、多线程与对象交互等Qt机制。项目内附说明文档梳理了目录结构,便于按模块学习;目前已有535人学习下载,可在现有代码基础上自由修改扩展,满足课设、毕设等不同场景需求。
1. 课程设计选型:为什么是Qt和C++而不是Java或Python
如果你打开这份“基于Qt和C++框架编写的简易植物大战僵尸游戏源码.zip”,大概率是在为课程设计发愁,手里已经积压了好几版写了一半又被推翻的代码。这个标题看起来平平无奇,但背后其实是很多计算机专业学生在选型时必须面对的一道坎:用Python做游戏demo确实快,答辩时却容易被追问“性能怎么保证”;用Java做窗口程序也不是不行,可游戏里的精灵动画和碰撞响应总感觉隔着一层。Qt和C++的组合正好卡在“代码量可控”和“技术含量看着够”之间,是课程设计里最常见的可靠方案。
这个包里的游戏,不是拿Unity或Godot那种现成游戏引擎拖出来的,而是在Qt的QGraphicsView框架上从零搭的一套对象模型。它把植物、僵尸、子弹、草坪、太阳全都当作场景里有独立行为的对象,用定时器驱动逻辑,用信号槽传递事件。这套做法最大的价值是,你做完这个课程设计之后,对“框架”这个词的理解会比只会调API的同学深得多——你知道框架在哪些地方给你搭好了舞台,哪些地方需要你自己塞逻辑。
2. 拆开源码包后的第一件事:工程结构、编译链路与资源目录
2.1 先看.pro文件:Qt工程的心脏
拿到zip解压后,不要急着双击.pro文件,先打开文件管理器看一眼目录结构。一个规范的Qt Widgets或Qt Quick工程,顶层一定有一个以.pro结尾的工程文件,它告诉qmake这个项目该怎么组织。以常见的qmake工程为例,.pro文件里通常只有十几行配置,但它决定了你按下编译键之后会发生什么。
QT += core gui greaterThan(QT_MAJOR_VERSION, 4): QT += widgets TARGET = PlantsVsZombies TEMPLATE = app DEFINES += QT_DEPRECATED_WARNINGS SOURCES += \ main.cpp \ GameScene.cpp \ Plant.cpp \ Zombie.cpp \ Bullet.cpp \ Sun.cpp HEADERS += \ GameScene.h \ Plant.h \ Zombie.h \ Bullet.h \ Sun.h FORMS += \ MainWindow.ui RESOURCES += \ resources.qrc这里有几个值得逐行说清楚的细节。QT += core gui是基础模块,widgets是图形界面控件模块,游戏场景依赖它;TARGET是生成的可执行文件名字;SOURCES和HEADERS里每一项都必须真实存在于磁盘上,否者qmake阶段就会报错。最容易被忽略的是RESOURCES,它指向的resources.qrc负责把图片、音频等文件打包进二进制可执行文件里,后面打包发布时很多诡异问题都出在这一行。
我遇到过好几次这样的情况:解压之后直接打开.pro文件,Qt Creator提示“找不到文件列表中的某个.cpp”,检查之后发现是zip解压工具把文件名里的下划线改掉了,或者是路径中有中文导致qmake读取文件列表时编码不一致。建议先确认目录里所有源文件都存在、命名和.pro里一致,再谈编译。
2.2 用Qt Creator把源码导入工程:可能翻车的地方
如果你用的IDE是Qt Creator,导入方式有两种:直接打开.pro文件,或新建空项目后再把源码文件手动拖进去。前者是正路,因为qmake会帮你把完整的编译链路理顺;后者很容易出现头文件路径找不到的问题。
打开.pro文件后,先做一次“构建”而不是直接“运行”。原因是编译错误和运行错误有本质区别:编译错误说明你的环境有问题,运行错误才是代码逻辑问题。课程设计里大量时间其实花在环境调试上,尤其是第一次在一台电脑上打开源码包时。
# 在项目根目录执行 qmake 和 make,可以绕过 IDE 直接验证工程可用性 mkdir build cd build qmake ../PlantsVsZombies.pro make -j4如果上面这三步能顺利跑完,说明工程本身没问题,IDE那边出问题基本是配置问题。我在实验室帮同学排查过一个案例:Qt Creator里显示“无法运行”,但命令行构建却能通过,最后发现是他的构建套件(Kit)里编译器路径指向了32位版本,而项目用了64位库。请记住:IDE失败不代表工程失败,先用命令行缩小排查范围。
2.3 resources.qrc:图片凭什么能找到
植物大战僵尸游戏的核心玩法不复杂,但美术资源多。植物卡片要图片、僵尸要动画帧、草坪要格子背景、子弹和爆炸效果也要图。这些资源如果按普通文件路径读取,一旦可执行文件换了目录,路径就失效;如果把资源丢进qrc文件里,Qt会在编译期把它们转换成C++字节数组,烧进二进制里,无论你把可执行文件拷到哪儿都不会丢资源。
<RCC> <qresource prefix="/"> <file>images/sunflower.png</file> <file>images/peashooter.png</file> <file>images/wallnut.png</file> <file>images/zombie_1.png</file> <file>images/zombie_2.png</file> <file>images/pea.png</file> <file>images/sun.png</file> </qresource> </RCC>加载方式也跟普通图片不同,要写成QPixmap(":/images/sunflower.png"),注意冒号前缀。新手最容易踩的坑是复制别人代码后只改了路径没改前缀,导致程序运行起来画面上一片空白。记住一个规律:qrc里的路径,永远以:/开头,冒号是必须的,它告诉Qt去已编译的资源段里找文件,而不是去磁盘找。
3. 把“植物大战僵尸”拆成游戏对象:场景、基类与事件循环设计
3.1 用QGraphicsView架构当棋盘,而不是用QWidget硬画
做这种精灵类游戏,选择QGraphicsView框架几乎是Qt方案里的标准答案。它有一套完整的“场景-视图-图元”三层模型:QGraphicsScene管理所有游戏对象的位置和碰撞关系,QGraphicsView负责把场景渲染到窗口上,QGraphicsItem是场景中一个个可交互的图元。
我见过一些课程设计用QLabel移动或者直接在QWidget上paintEvent画全部画面,那种写法在植物大战僵尸这种对象交互密集的游戏里,很快就会失控——每个僵尸都要自己管理坐标、刷新和碰撞,代码量翻倍且帧率不稳。QGraphicsView的优势在于场景内部维护了一个空间索引,碰撞检测不需要你遍历所有对象,缩放和平移也是内建的。
// GameScene.h #ifndef GAMESCENE_H #define GAMESCENE_H #include <QGraphicsScene> #include <QTimer> #include "Plant.h" #include "Zombie.h" class GameScene : public QGraphicsScene { Q_OBJECT public: explicit GameScene(QObject *parent = nullptr); void startGame(); void stopGame(); private slots: void updateGame(); // 游戏主循环,由定时器触发 void spawnZombie(); // 每隔一段时间生成一只僵尸 private: QTimer *gameTimer; QTimer *zombieSpawnTimer; QList<Plant*> plantList; QList<Zombie*> zombieList; }; #endif // GAMESCENE_H这段头文件看起来短,但它把游戏循环的思路定下来了:两个定时器分别负责游戏刷新和僵尸生成,所有对象存放在QList里便于遍历。updateGame()是主循环,每帧要检查碰撞、更新子弹位置、判断输赢。用QTimer做游戏循环的定时器很常见,但要注意QTimer的精度问题,默认精度是中等精度,对60帧游戏来说够用,但如果你需要精确到毫秒级的帧间隔,则要显式设置setTimerType(Qt::PreciseTimer)。
3.2 Plant与Zombie的基类设计:继承自QGraphicsPixmapItem还是QObject?
这是Qt游戏开发里一个很经典的设计抉择。QGraphicsPixmapItem负责显示图片,但它不是QObject的子类,这意味着它不能直接使用信号槽、定时器这些Qt核心机制;而QObject本身不能作为图元放入场景。常见的做法是用多继承,同时继承QObject和QGraphicsPixmapItem,让植物和僵尸既能进场景显示,又能用信号槽通信。
// Plant.h #ifndef PLANT_H #define PLANT_H #include <QObject> #include <QGraphicsPixmapItem> #include <QPixmap> #include <QTimer> class Plant : public QObject, public QGraphicsPixmapItem { Q_OBJECT public: enum PlantType { Sunflower, // 向日葵:生产阳光 Peashooter, // 豌豆射手:射击僵尸 Wallnut // 坚果墙:阻挡僵尸 }; explicit Plant(PlantType type, QGraphicsItem *parent = nullptr); PlantType getType() const; int getCost() const; // 种植消耗的阳光 int getHealth() const; void takeDamage(int damage); protected: PlantType m_type; int m_health; int m_cost; }; #endif // PLANT_H这个类的设计核心是“数据驱动行为”。PlantType枚举区分三种植物,每种植物在构造函数里初始化自己的血量、价格和图片,行为差异则交给场景去判断。注意这里用了Q_OBJECT宏,它让这个类拥有信号槽能力。多继承时有一个坑:QObject的构造函数只能接受一个父对象指针,而QGraphicsPixmapItem的构造函数接受的是QGraphicsItem父指针,两者不能互相传递,所以通常两个父参数都传nullptr或只传parent给图元部分,否则会编译报错。
僵尸类也类似,只是多了移动速度、攻击力等字段。这个抽象层次的意义在于:如果你未来想加一个“樱桃炸弹”植物,只需要新增一个枚举值,在构造函数里设置它的图片、价格、生命值,然后在场景的种植逻辑里加一行判断,而不需要改整个架构。
3.3 子弹与碰撞检测:itemCollision和手动坐标检测怎么选
这是整个游戏中后期最容易出问题的部分,也直接决定游戏的手感。QGraphicsScene提供了一个collidingItems()方法,可以返回跟某个图元重叠的所有图元。这个方法内部走的是Qt的场景空间索引,效率不低,但要注意它是以图元外接矩形相交为判断依据的,图片如果有透明区域就会出现“明明没碰到却被判定为碰撞”的情况。
// GameScene.cpp 中的碰撞检测片段 void GameScene::updateGame() { // 先遍历所有子弹,检查是否打中僵尸 for (Bullet *bullet : qAsConst(m_bullets)) { if (bullet == nullptr) continue; // 方法1:使用Qt内置的碰撞检测,取场景中与子弹相交的所有图元 QList<QGraphicsItem *> hitItems = bullet->collidingItems(); for (QGraphicsItem *item : hitItems) { // 用dynamic_cast判断图元是否为僵尸 Zombie *zombie = dynamic_cast<Zombie *>(item); if (zombie && zombie->isAlive()) { zombie->takeDamage(bullet->getDamage()); bullet->setActive(false); break; // 一颗子弹只打中一个僵尸 } } } // 方法2:手动坐标判断,适合需要精确判定圆心的场景 for (Bullet *bullet : qAsConst(m_bullets)) { QPointF bulletPos = bullet->pos(); for (Zombie *zombie : qAsConst(m_zombies)) { QPointF zombiePos = zombie->pos(); qreal dx = bulletPos.x() - zombiePos.x(); qreal dy = bulletPos.y() - zombiePos.y(); qreal dist = std::sqrt(dx * dx + dy * dy); if (dist < 25.0) { // 半径为25像素的圆形碰撞区域 zombie->takeDamage(bullet->getDamage()); bullet->setActive(false); break; } } } }两份代码都跑得通,差别在适用场景。collidingItems()适合子弹小、移动快、碰撞要求不高的情况;手动坐标检测适合对碰撞精度有要求或想自定义碰撞盒的情况,比如僵尸的“头”才算受击区域。我的个人习惯是:课程设计用collidingItems()足够,但答辩时一定要能说出它的局限——它基于外接矩形,不能区分透明像素。
这里还要提一个容易被忽略的性能问题:collidingItems()在场景中图元数量少时没问题,但如果一屏有几十个僵尸加上几十颗豌豆,每帧遍历所有子弹再遍历碰撞列表,计算量会快速上升。优化手段通常是空间分块或者只检查同一行的图元,但对课程设计而言,先把功能跑通更重要,优化可以放到答辩后的扩展里。
4. 核心玩法落地:太阳、种植、攻击与游戏循环
4.1 太阳生成与点击拾取:定时器驱动还是随机事件?
植物大战僵尸的核心资源是阳光,游戏里阳光有两种来源:自然掉落和向日葵产出。自然掉落的太阳由主定时器触发,每隔几秒随机在指定区域出现;向日葵产出的太阳位置固定在植物旁边。这里的实现关键点是用一个QTimer控制生成节奏,然后每个太阳对象自己负责“出现在场景里,等待被点击”。
点击拾取在Qt里可以走事件过滤或重写mousePressEvent。比较直观的做法是给Sun类重写mousePressEvent,但要注意场景本身也有鼠标事件,两者之间有一个事件分发顺序的问题。实战中我建议在场景层处理所有鼠标事件,然后通过itemAt()判断点到了谁:
// GameScene.cpp 鼠标点击拾取阳光 void GameScene::mousePressEvent(QGraphicsSceneMouseEvent *event) { QGraphicsScene::mousePressEvent(event); // 用荫景坐标找到鼠标点击位置的那个图元 QGraphicsItem *item = itemAt(event->scenePos(), QTransform()); Sun *sun = dynamic_cast<Sun *>(item); if (sun) { // 每朵阳光固定加50点 m_sunEnergy += 50; emit sunEnergyChanged(m_sunEnergy); // 发信号告诉UI更新 sun->collectSun(); // 触发收集动画后删除 } }信号sunEnergyChanged可以连接到界面上的QLabel更新函数,这样游戏逻辑和UI显示是解耦的。很多课程设计容易忽略这个解耦,直接在游戏逻辑代码里写ui->label->setText(...),短时间看着省事,等你要加暂停、加音效、加新植物时,就会发现逻辑和视图缠成一团乱麻。
4.2 植物冷却机制:用QTimer还是用时间戳?
种植植物需要冷却时间,这是游戏平衡性的关键,也是课程设计里代码质量的分水岭。初级写法是种植成功后启动一个定时器,冷却结束再恢复可种状态;但如果你同时支持多个种植槽位,每个槽位一套QTimer就会很臃肿。
更简洁的常见做法是记录每个槽位的“上次种植时间”,每次点击卡片时用QDateTime::currentMSecsSinceEpoch()计算时间差,大于冷却阈值才允许种植。这种方式没有定时器开销,天然支持多个槽位,而且暂停游戏时也不容易出Bug。
// CardWidget.cpp 冷却控制片段 void CardWidget::tryPlant() { qint64 now = QDateTime::currentMSecsSinceEpoch(); qint64 elapsed = now - m_lastPlantTime; if (elapsed >= m_cooldownMs && m_gameScene->getSunEnergy() >= m_cost) { // 扣阳光,通知场景进入“种植模式” m_gameScene->setSunEnergy(m_gameScene->getSunEnergy() - m_cost); m_gameScene->setPlantingMode(getPlantType()); m_lastPlantTime = now; update(); } else { // 冷却中或阳光不足,给出视觉反馈 QApplication::beep(); } }注意这里的setPlantingMode是一个状态位,告诉场景“玩家目前选中的是什么植物”,接下来玩家点击草坪格子时,场景就创建对应类型的植物对象。这种“卡片只管冷却和消费,场景只管创建实例”的分工,比把种植逻辑全部堆在卡片控件里要清晰得多。
4.3 游戏状态机:待机、运行、暂停和结束怎么转换
课程设计里最常见的一个失误是:游戏过程没有明确状态,代码里用一堆bool变量互相纠缠。植物大战僵尸这个游戏虽然简单,但至少有四个状态:开始界面待机、游戏进行中、暂停、结束(胜利或失败)。不同状态下,定时器要开关、鼠标事件要拦截、界面控件要切换。
// GameScene.cpp 状态切换逻辑 void GameScene::setGameState(GameState newState) { if (m_state == newState) return; m_state = newState; switch (m_state) { case Running: gameTimer->start(16); // 约60帧/秒 zombieSpawnTimer->start(20000); // 每20秒一波僵尸 break; case Paused: case GameOver: case Victory: gameTimer->stop(); zombieSpawnTimer->stop(); break; case Ready: // 待机状态:场景里只有草坪背景 clearAllObjects(); break; } }用枚举替代一堆bool是这里最实用的建议。枚举值让代码的自解释性大大提高,答辩时你直接说“我用了一个四状态的状态机管理游戏进程”,比说“我用了好几个bool变量切换界面”有说服力得多。还有一个细节:暂停时不仅要停定时器,还要把当前场景里的动画暂停,QGraphicsItem没有全局暂停机制,我一般用一个成员变量标记游戏状态,在每帧updateGame()开头检查,非Running状态直接跳过逻辑更新。
4.4 攻击逻辑:双定时器方案让豌豆射手稳定工作
豌豆射手的攻击频率是有讲究的。你可以在Plant类里给豌豆射手挂一个定时器,每1.4秒发射一颗子弹;也可以只在场景全局更新里统一处理所有植物的射击行为。两种方案我都试过,结论是:统一在场景层处理更利于答辩和排错。
想想看:全局统一处理时,“某行是否有僵尸”的判断只需要写一次,所有豌豆射手共用同一个扫描结果;如果每个豌豆射手自己挂定时器处理,你就是在一帧里对同一个僵尸执行了几十次行检查,做的是无用功。在场景里用一个简单函数判断“这一行有没有僵尸”,比每棵植物自己感知世界要可靠得多。
// GameScene.cpp 豌豆射手统一开火逻辑 void GameScene::updateShooters() { for (Plant *plant : qAsConst(m_plants)) { if (plant->getType() != Plant::Peashooter) continue; if (!plant->isAlive()) continue; // 只检查豌豆射手所在行是否有活着的僵尸 int row = plant->getRow(); bool hasZombieInRow = false; for (Zombie *zombie : qAsConst(m_zombies)) { if (zombie->getRow() == row && zombie->isAlive()) { hasZombieInRow = true; break; } } if (hasZombieInRow) { // 发射豌豆,豌豆出生在植物前方 Bullet *bullet = new Bullet(plant->getShootPos()); bullet->setRow(row); m_bullets.append(bullet); addItem(bullet); } } }这里每帧都要遍历所有植物,是一种“按需计算”的思路。如果未来植物种类变多,可以为每行维护一个僵尸列表来优化。课程设计里别过度设计,但要在答辩时能说出这个方案的局限——你认为应该怎么做会更好,老师通常对“能指出自己代码不足并给出改进方向”的学生印象更好。
5. 课程设计最容易翻车的7个点:编译、信号槽与打包避坑
5.1 信号槽连接失败,槽函数不执行
现象:界面上的阳光数字不动,点种植卡片没反应,但代码里确实connect了。
原因分三类:一是连接时用了自定义类型,但信号和槽的参数类型不一致,Qt运行时才报“无法连接”的警告;二是类没有声明Q_OBJECT宏,导致信号槽机制无法工作;三是接收对象在连接后被销毁,连接自动断开。
解决:先看Qt Creator的“应用程序输出”面板,有没有QObject::connect相关警告。确认自定义类型在使用前先qRegisterMetaType注册;确认槽函数被声明为private slots:而不仅仅是普通成员函数;确认接收对象生命周期跟场景一致。我的习惯是:所有跨对象的通信,发射方和接收方都必须是堆上new出来的对象,并且父对象设置正确。
5.2 图片资源在你电脑上正常,别人电脑上消失
现象:可执行文件拷到别的电脑运行,界面出现但植物和僵尸都不显示,只显示白色方块或空白。
原因:代码里用了相对路径加载图片(比如QPixmap("images/pea.png")),而可执行文件的运行目录变了,相对路径就失效了。资源没有走qrc打包进可执行文件。
解决:把图片统一放进.qrc资源文件,代码里统一用QPixmap(":/images/pea.png")这种带冒号前缀的路径。这是一个一劳永逸的改法,改完之后编译出来的exe自带所有图片,拷到任何Windows电脑上都不会丢图。
5.3 打包发布后提示缺少Qt运行环境
现象:Debug模式下程序正常,把exe单独拷到别的机器运行,弹窗提示“无法定位程序输入点”或直接报“缺少Qt5Core.dll”。
原因:Debug版的exe依赖Qt的调试库,而调试库通常只有开发机上有;且Qt不是静态编译,默认是动态链接,运行必须带上一堆DLL。
解决:发布时千万别用Debug版的exe,要用Release版。然后在Release目录下运行Qt自带的部署工具,Windows上是windeployqt:
cd build-release windeployqt PlantsVsZombies.exe这个工具会自动把需要的Qt DLL、平台插件(platforms目录)和编译器等依赖全部拷到exe所在目录。拷完后再打包zip,到别的电脑上双击exe通常就能跑起来。
5.4 中文字符串乱码
现象:界面上植物名称显示成乱码“鏂囩珷”,或者代码里的中文注释导致编译警告。
原因:Qt 5系列默认源文件编码是UTF-8,但Windows控制台和部分编译器环境用本地编码GBK解析,导致中文字符串在运行时变成乱码。如果使用tr()包裹中文字符串,还需要翻译文件机制,没安装翻译文件时反而会显示空。
解决:项目代码文件统一保存为UTF-8编码;代码里显示中文的地方直接写中文,不要用tr()。如果你用了MSVC编译器,可以在.cpp文件顶部加#pragma execution_character_set("utf-8")强制编译器按UTF-8处理字符串。这个坑在答辩演示时最尴尬,界面上一堆乱码会让老师瞬间觉得代码质量不行。
5.5 运行崩溃:野指针和悬垂图元
现象:启动游戏后几秒钟程序直接崩溃退出的,多见于删除僵尸或子弹时。
原因:QGraphicsScene::removeItem()会从场景移除图元,但如果此时还有另一个对象持有这个图元的指针,后续再访问它就会触发无效内存访问。
解决:删除对象前先切断所有引用。用deleteLater()而不是直接delete,让Qt在事件循环结束时安全回收。注意removeItem之后不要立即delete,给deleteLater一个机会处理挂起的事件。
5.6 zip压缩包本身的问题:伪加密和文件名乱码
现象:从网上下载的课程设计源码zip包,解压时提示需要密码,但正文里根本没提密码;或者解压之后文件全是_开头的乱码文件名。
原因:国内不少资源站为了防止无授权转载,会给zip加伪加密头,文件实际没加密,但解压工具会误判。文件名乱码则是压缩工具用了非标准编码。
解决:用7-Zip打开,如果提示加密但能预览目录结构,可以尝试用7z x 文件名.zip -p直接解压(空密码);如果文件名乱码,用Bandizip换用系统编码重新解压。这不是代码问题,但每年都有人卡在这一步,白折腾一晚上。
5.7 调试时一切正常,一全屏就错位
现象:窗口正常大小下游戏流畅,点最大化按钮后草坪格子和植物位置对不上,鼠标点击位置错位。
原因:你的坐标计算里写了硬编码的固定值,比如setFixedSize(900, 600),或者背景图尺寸是固定的,窗口拉伸后视图缩放导致坐标映射偏移。
解决:给QGraphicsView设置setSceneRect()固定场景大小,然后把视图的缩放模式设为fitInView并忽略窗口尺寸变化;或者重写resizeEvent,在窗口尺寸变化时重新计算场景到视图的映射。我建议课程设计直接用setFixedSize锁定窗口大小,把精力集中到玩法上,等答辩完了再考虑自适应窗口。
6. 答辩与改造:给僵尸加更聪明的逻辑与更优的碰撞检测
到这里,源码包里的项目多半已经能在你电脑上跑起来,剩下的事是如何把它变成一份能拿高分、能讲出东西的课程设计。很多同学忽略了一个事实:老师看代码的时间远少于看演示的时间,你咬断三分钟怎么讲解,比多写两百行代码更有价值。
6.1 答辩演示脚本:先讲架构,再讲难点,最后打一局
我带的学弟学妹答辩前,我给他们定的演示顺序是固定的。开场先花30秒讲整体架构:“这是一个基于QGraphicsView框架的游戏,场景管理所有对象,植物、僵尸、子弹都继承自图元基类,游戏循环由定时器驱动。”这句话的作用是把老师的注意力拉到你最熟悉的领域。中间用1分钟讲一个具体难点——我建议讲碰撞检测或者状态机——并主动说出它的局限和你的改进打算。最后再打一局游戏,证明它真的能玩。
6.2 三个性价比超高的功能扩展
如果时间和精力允许,给游戏加三个低成本、高演示效果的功能,排名按性价比从高到低:
第一个是僵尸的移动方式从“匀速直线”改成“按行推进”,每移动一段距离就检查一次前方是否有坚果墙,有则停下来啃咬。这个改动的代码量不大,只需要在Zombie::update()里加一个碰撞检测分支,但演示效果会立刻像样——僵尸不再是闭眼直走的木偶。
// Zombie.cpp 扩展:遇到坚果墙就停止并啃咬 void Zombie::update() { // 检查前方是否有植物阻挡 QList<QGraphicsItem *> blockingItems = collidingItems(); for (QGraphicsItem *item : blockingItems) { Plant *plant = dynamic_cast<Plant *>(item); if (plant && plant->isAlive()) { setState(Zombie::Eating); // 切换到啃食状态动画 plant->takeDamage(m_biteDamage); return; // 本帧不再前进 } } setState(Zombie::Walking); setPos(x() - m_speed, y()); }第二个是加入音效,用QSoundEffect播放种植和射击音效。这部分代码量极小,但演示时很有感染力,显得你做了“完整的游戏”而不是“一个会动的demo”。
第三个是用QPropertyAnimation给太阳的收集过程加平滑动画,让阳光被点击后飞到右上角的能量槽里。这个效果会让老师看到你对Qt动画框架的掌握,属于“进阶展示型功能”。
6.3 我的最后一个建议:留下一个“以后可以怎么改”的注释块
说起来不像是代码建议,但这是我在做了无数个课程设计之后最想分享的习惯。在你交源码之前的最后一晚,打开最关键的两个文件——通常是GameScene.cpp和Plant.cpp——在文件末尾留一个注释块,写三到五条“TODO”,例如:
/* * TODO(后续扩展方向) * 1. 碰撞检测改为自定义圆形碰撞盒,解决透明像素误判问题 * 2. 引入QPropertyAnimation实现植物被僵尸啃咬时的抖动动画 * 3. 把波次和僵尸血量改为可配置JSON,不用改代码就能调难度 * 4. 用QStateMachine重构游戏状态切换,消除状态分支中的重复代码 */这个注释不会影响编译,不会影响运行,但它能让你在答辩时说出那句最有用的话:“这是我当前实现的版本,但我知道它有这些不足,如果给我更多时间,我会优先从这几个方向优化。”这句话传达的信息是——你有代码能力,有自知之明,也有规划意识——而这恰恰是课程设计想要考察的。
希望这次Qt和C++的破关之旅还算顺利,希望这份笔记帮你在交源码前少踩几个坑。做课程设计就是这样,代码写完只是第一步,真正值钱的是你知道它为什么能跑,以及它在哪些情况下会跑不起来。这两者,它都给你了。
本文还有配套的精品资源,点击获取