简介:基于C++与Qt开发的飞机大战小游戏完整工程,面向计算机相关专业学生、初学Qt的开发者及需要课程设计或毕业设计参考的读者。项目包含完整可运行的源码,涵盖地图、英雄机、敌机、子弹、炸弹等核心模块,代码结构清晰,便于在此基础上二次开发,实现道具、计分等扩展功能。包内共58个文件,压缩包约33.2MB,其中7个cpp与7个头文件构成主程序,31张png和5张jpg为飞机、背景等游戏素材,2个wav提供音效,另有Qt工程文件、资源文件及图标文件,下载后可直接用Qt Creator打开AirplaneWar.pro运行。目前已有352人学习下载,适合用来理解Qt图形视图框架、事件循环、碰撞检测等知识点,也可作为实训项目、课设模仿的完整蓝本。项目代码经测试可正常运行,开发环境配置与资源引用均已完成,省去繁琐的环境搭建时间。
1. 收到一个飞机大战源码包:先确认这包值不值得打开
你大概率是在某个资源站或课程附件里看到「基于c++ QT开发的飞机大战小游戏.zip」这个名字的。它看起来像一个完整可运行的项目,解压后通常有一堆.cpp、.h、.pro文件,运气好还带一张游戏截图。这个标题背后是一个典型的 Qt 入门级游戏项目:用 C++ 写逻辑、用 Qt Widgets 或 Qt Quick 画界面、用事件循环驱动飞机移动和子弹发射。它适合两类人——刚学完 C++ 语法想找个「能跑起来的东西」练手的人,以及要做课程设计但不想从零造轮子的学生。先说一个反直觉的判断:这种小游戏最值钱的不是画飞机或移动子弹,而是 Qt 的对象树、信号槽和事件循环这三个机制。你把这三点吃透,飞机大战只是顺便做出来的东西。
2. Qt 5.15.2 + MSVC2019 环境:为什么这套组合是小游戏的及格线
2.1 从压缩包到工程目录:认识 .pro 文件与构建体系
Windows 下拿到这个 zip,第一步不是双击.exe,而是先看工程文件。一个标准的 Qt Widgets 项目,核心文件是.pro,它告诉 qmake 这个项目包含哪些源文件、链接哪些模块。很多人直接把整个目录拖进 Qt Creator 点运行,结果报错连篇,根源都是没先读.pro。
QT += core gui greaterThan(QT_MAJOR_VERSION, 4): QT += widgets TARGET = airplane_battle TEMPLATE = app SOURCES += main.cpp \ mainwindow.cpp \ gamewidget.cpp \ playerplane.cpp \ enemyplane.cpp \ bullet.cpp HEADERS += mainwindow.h \ gamewidget.h \ playerplane.h \ enemyplane.h \ bullet.h RESOURCES += res.qrcTARGET是生成的可执行文件名,TEMPLATE = app表示这是一个应用程序而不是库。QT += widgets在 Qt 5 里必须写,少了它QWidget和QGraphicsScene相关的头文件会直接报“没有那个文件”。RESOURCES指向资源文件,图片和音效都打包进二进制,部署时不用另外带素材。
我见过最多的问题是:项目是从 Qt 6 教程里改的,结果QT += widgets这行被删了,或者TARGET里带中文名,Windows 下 MSVC 编译直接崩。拿到源码包,第一步就是把.pro里每一行和实际目录对一遍,源文件缺了哪个补哪个,路径别用相对路径穿越(比如../../../../这种),一旦移动目录就全线崩溃。
2.2 把 .pro 跑起来的两种方式:Qt Creator 图形工程与命令行构建
团队里老手一般用 Qt Creator 打开.pro,然后按Ctrl + Shift + B构建。这个流程对新手的隐藏风险是:Qt Creator 会自动选择一套 Qt Kit(编译器 + Qt 库版本 + 调试器),但很多 zip 包里的代码是在另一个 Kit 下写的,比如在 MinGW 下写的槽函数签名,拿到 MSVC 下未必编译得过。
我习惯的做法是先用命令行摸清项目底细。在 Qt 安装目录的Tools/QtCreator/bin里能找到jom或mingw32-make,在Qt/5.15.2/msvc2019_64/bin里有qmake.exe。先用它构建一遍。
mkdir build cd build qmake ..\airplane_battle.pro "CONFIG+=debug" nmake 或 mingw32-makeCONFIG+=debug会把调试信息编进可执行文件,方便后边在 Qt Creator 里打断点。注意:如果 Qt 版本是 msvc2019_64,就必须用 VS2019 的nmake,不能用mingw32-make。反过来,MinGW 版的 Qt 库配nmake也会出现链接错误,提示找不到libstdc++。这是大多数「拿到源码跑不起来」的根源——不是代码问题,是工具链错配。
2.3 环境不匹配的三种典型现场:MSVC 与 MinGW 混用的翻车实录
先说第一种:你在 Qt 5.12 的 MinGW 版本里把代码写好了,换到 Qt 5.15.2 的 msvc2019_64 打开,最常见的报错是error: dependent '..\..\qt\5.15.2\msvc2019_64\include\qtwidgets' does not exist。这个错误字面意思是某个头文件路径不存在,实际含义是.pro里的模块和当前 Kit 的 Qt 库不匹配。解决方案是打开「项目」选项卡,把当前 Kit 切到 MSVC2019 64bit,然后重新执行 qmake。
第二种是开源库的问题。飞机大战如果用了QMediaPlayer做音效,MinGW 版需要额外装ffmpeg解码库,而 MSVC 版自带 Windows Media Foundation 后端,不需要额外配置。如果你的 zip 包说明文档里写着“请安装 ffmpeg”,你就要意识到它是为 MinGW 准备的。
第三种是debug和release混编。Qt 的 MSVC 版本中,debug 版 Qt 库名带d后缀(比如Qt5Cored.lib),如果你在 release 模式下链接了 debug 库,链接器不会直接报错,但运行时会闪退,而且异常点完全随机——今天在 QTimer 里崩,明天在 QGraphicsScene 里崩。我排查了一整天,最后发现是.pro里手动写了LIBS += -lQt5Cored,删掉这行让 qmake 自己判断就好了。
提示:遇到任何「看起来像代码问题」的 Qt 编译或闪退,先查 Kit 的编译器类型和 Qt 库版本,再查代码。工具链错配占 Qt 初学阶段七成以上的运行失败。
3. 飞机大战的骨架:对象树、信号槽与事件循环如何串起一局游戏
3.1 对象树与父子对象:QWidget 生命周期管理的硬约束
Qt 里有一个让 C++ 老手特别不放心、但新手又总踩坑的机制——QObject父子关系。你new一个QWidget并指定了父对象,Qt 会在父对象析构时自动删除所有子对象。这个设计叫对象树,它的好处是省掉大量手动delete,坏处是如果你手动delete了一个子对象,又没有把它从父对象的 children 列表里摘掉,程序会在父对象析构时发生双重释放崩溃。
飞机大战里最典型的场景:在GameWidget构造函数里new了一排飞机和子弹,并setParent(this)或添加到场景中。你如果在一个“重新开始游戏”的槽函数里写了delete player; player = new Player(...),第二次点击开始时大概率直接崩。原因不是delete本身,而是player的旧对象已经注册在场景或父对象里,新的delete之后,场景里还保留着指向已释放内存的悬空指针。
正确的做法是让父对象统一管理,或者在重新开始时把整个场景清空重建,而不是单独删单个图形项。
void GameWidget::restartGame() { // 清空场景里的所有图形项 scene->clear(); // 重建玩家和敌机,scene 作为它们的父对象 player = new PlayerPlane(); scene->addItem(player); ... }scene->clear()会把所有QGraphicsItem从场景中移除,并删除它们。这里不要手动再去delete player。你只需要让player成为scene管理的对象即可。这条规则在 Qt 的图形框架里是硬约束——凡是丢进QGraphicsScene的QGraphicsItem,生命周期归场景管,有新玩家逻辑时,先clear()再重建。
3.2 信号槽不是回调:从按键响应看事件分发
信号槽可能是整个 Qt 里被误解最深的东西。很多从 C 转过来的人把connect当成注册回调函数,确实有点像,但有一个根本差异:信号槽的连接是类型安全的,发送方不需要知道接收方的任何细节,而且一个信号可以连多个槽,多个信号可以连同一个槽。飞机大战里按键移动就是个极好的切入点——你按下A键,QKeyEvent先进入事件循环,然后 Qt 把事件分发到当前有焦点的 widget,你在keyPressEvent里发射一个自定义信号,由GameWidget的槽函数处理。
// playerplane.h class PlayerPlane : public QGraphicsObject { Q_OBJECT signals: void moveLeftRequested(); public slots: void onMoveLeft(); protected: void keyPressEvent(QKeyEvent *event) override; }; // playerplane.cpp void PlayerPlane::keyPressEvent(QKeyEvent *event) { if (event->key() == Qt::Key_A) { emit moveLeftRequested(); } }这段代码的核心是:PlayerPlane自己不知道谁会去响应这个按键——它只负责发出请求。在GameWidget的构造函数里,你必须把这个信号连到实际做移动的槽上。
connect(player, &PlayerPlane::moveLeftRequested, this, &GameWidget::onPlayerMoveLeft); void GameWidget::onPlayerMoveLeft() { player->setX(player->x() - kPlayerSpeed); }注意:connect的第五个参数默认是Qt::AutoConnection,同一线程内直接调用槽函数;跨线程则变成队列连接。飞机大战所有对象都在主线程,所以不用担心线程问题。但有个习惯要养成——槽函数里不要做耗时操作。如果onPlayerMoveLeft里加载了图片或解析了 JSON,游戏会卡顿,因为此时主线程被占住了。
3.3 QTimer 驱动游戏主循环:帧率控制与定时器精度
飞机大战本质上是一个不断重绘的游戏,敌人定时生成、子弹按帧移动、碰撞持续检测。Qt 没有内置的GameLoop类,常见做法是用QTimer驱动刷新。但QTimer有个精度问题:它依赖系统事件循环,如果有人拖动窗口、弹出菜单、或者系统繁忙,定时器可能积累延迟。
QTimer *gameTimer = new QTimer(this); gameTimer->setInterval(16); // 约 60 FPS connect(gameTimer, &QTimer::timeout, this, &GameWidget::updateFrame); gameTimer->start();setInterval(16)给出的是目标间隔,实际触发时间由系统调度决定。如果你追求稳定帧率,不要依赖QTimer::timeout的连续间隔,而是记录每次触发的时间戳,用时间差来算位移。
void GameWidget::updateFrame() { qint64 now = QDateTime::currentMSecsSinceEpoch(); qint64 delta = now - lastFrameTimeMs; lastFrameTimeMs = now; // 用 delta 而不是固定步长移动 player->moveBy(kPlayerSpeed * delta / 16.0, 0); }这样即使某帧延迟到 30ms 才触发,移动距离也会按比例放大,视觉上不会出现“卡一下然后跳一大截”的生硬感。飞机大战这种小游戏,很多人忽略时间戳,直接用固定步长,结果系统负载稍高时游戏就变得奇慢,这就是帧率失控的典型症状。
提示:QTimer 的最小精度是毫秒级,不是做逐帧物理模拟的可靠时钟。需要更高精度的计时,用
QElapsedTimer或std::chrono::steady_clock。
4. 把飞机大战拆成四个可落地模块:场景、精灵、碰撞与音效
4.1 场景管理:用 QGraphicsScene / QGraphicsView 还是自绘
启动飞机大战项目时,第一个架构决策是:界面用什么画。常见选项有两个——QGraphicsScene + QGraphicsView框架,或者自绘QWidget重写paintEvent。对这项目来说,我的结论是:除非你只有三五个物体且画面极简,否则用 QGraphicsScene 框架,别自绘。
自绘需要自己管理所有物体的坐标、重叠关系、重绘范围,一个物体移动就要手动update(),物体一多,重绘区域控制不好就闪屏。QGraphicsScene 框架帮你处理了大部分重绘优化,每个图形项只在自己的 boundingRect 里绘制,场景自动计算脏区域。写飞机大战,这是降本增效的决定。
scene = new QGraphicsScene(this); scene->setSceneRect(0, 0, 480, 640); view = new QGraphicsView(scene); view->setRenderHint(QPainter::Antialiasing, true); view->setWindowTitle("飞机大战"); view->resize(480, 640); view->show();setSceneRect设置的是场景坐标系的范围,这决定了你能移动的区域。如果没设置,场景会根据所有图形项的 boundingRect 自动调整,当你把飞机移动到左上角时,场景会“漂移”,视图跟着缩放,这是一个特别隐蔽的坑。先把场景矩形锁定,再往里放物体。
4.2 用 QGraphicsItem 体系定义飞机、子弹与敌机
定义游戏实体,标准做法是继承QGraphicsObject或QGraphicsItem。前者自带信号槽能力,后者更轻量。飞机大战中推荐QGraphicsObject,因为你要给飞机加碰撞信号、死亡信号,这些用信号槽远比手动回调精神清晰。
class PlayerPlane : public QGraphicsObject { public: explicit PlayerPlane(QGraphicsItem *parent = nullptr); QRectF boundingRect() const override; void paint(QPainter *painter, const QStyleOptionGraphicsItem *option, QWidget *widget) override; protected: void advance(int phase) override; };boundingRect()是给场景做碰撞和重绘判定用的,一定要画得比实际图形略大一圈,否则快速移动时图形边缘容易“拖影”或碰撞检测漏掉。经验值是实际大小向外扩 2~3 像素。paint()里画飞机的形状——可以用QPainter::drawPixmap贴图,也可以drawPolygon画三角。这个项目如果自带图片资源,记得把图片加载放在构造函数里缓存成QPixmap,不要在paint()里现场加载,否则每帧加载一次图片,帧率直接腰斩。
QPixmap PlayerPlane::loadTexture() { QPixmap pixmap(":/images/player.png"); if (pixmap.isNull()) { qWarning() << "Failed to load player texture, using fallback shape."; // 加载失败时返回一张程序内生成的位图 pixmap = QPixmap(48, 48); pixmap.fill(Qt::transparent); QPainter p(&pixmap); p.setBrush(QBrush(Qt::blue)); p.drawPolygon(...); } return pixmap; }这段代码看起来啰嗦,但它处理的是一个特别严重的隐性坑:资源路径写错时QPixmap不报编译错,只在运行时发出一句“Unknown error”警告,然后画出一个空白物体。游戏里表现为飞机“隐身”。用qWarning()打出日志,是第一个发现手段。
4.3 碰撞检测:图形项相交判定与性能取舍
飞机大战的碰撞检测,QGraphicsScene 提供了现成接口:collidesWithItem()和items()。前者判断两个图形项是否相交,后者返回场景中与指定区域相交的所有项。你可以不用手写任何几何计算,直接让场景替你判断。
QList<QGraphicsItem *> hitItems = collidingItems(player); for (QGraphicsItem *item : hitItems) { EnemyPlane *enemy = qgraphicsitem_cast<EnemyPlane *>(item); if (enemy) { emit playerHit(enemy); } }qgraphicsitem_cast是 Qt 提供的类型安全转换,它要求图形项的 type() 字段返回一个自定义枚举值。新手最容易犯的错是直接用static_cast,把一个不相关的图形项硬转成EnemyPlane *,然后访问不存在的成员变量,直接段错误。碰撞检测在每帧里做,性能是硬约束。如果每帧都遍历场景里所有的项做相交测试,物体一多帧率就掉。常见优化是分粗检和精检:粗检比 boundingRect,不相交直接跳过;精检才做像素级判断。对飞机大战而言,粗检通常就够了——飞机和子弹都是简单矩形或圆,相交结果基本可信。
bool collidesRoughCheck(PlayerPlane *player, EnemyPlane *enemy) { return player->boundingRect().translated(player->pos()).intersects( enemy->boundingRect().translated(enemy->pos())); }这个函数没有调用任何 Qt 的场景相交接口,纯矩形相交判定。它比 QGraphicsScene 的相交快一个量级,因为不需要遍历场景索引。小游戏里碰撞逻辑最简单可靠的方案就是这种「两重循环 + 矩形相交」。飞机和子弹数量都在几十这个量级,两重循环的 O(n²) 是完全可以接受的。
4.4 音频与资源加载:QMediaPlayer 与 Qt 资源系统
飞机大战的听觉反馈再简陋也都要有:发射子弹一个短促音效、敌机爆炸一个长一点的音效。Qt 里播放短音效有两个选择——QSoundEffect和QMediaPlayer。QSoundEffect 适用于 WAV 格式的短音,延迟低;QMediaPlayer 支持更多格式(MP3、OGG),但因为它是多线程解码,启动延迟略高,不适合高频触发。
QMediaPlayer *explodePlayer = new QMediaPlayer(this); QAudioOutput *audioOutput = new QAudioOutput(this); explodePlayer->setAudioOutput(audioOutput); explodePlayer->setSource(QUrl("qrc:/sounds/explode.mp3")); audioOutput->setVolume(0.8f); // 在碰撞槽里重新播放 void onPlayerHit(EnemyPlane *enemy) { explodePlayer->setPosition(0); explodePlayer->play(); }这里要注意:Qt 5.15 的QMediaPlayer需要Qt Multimedia模块,在.pro里要加QT += multimedia。MSVC2019 64bit 版本默认使用 Windows Media Foundation 后端,MP3 直接支持;如果代码是从 Linux 环境来的,那里可能用了 GStreamer 后端,封装格式不同,播放会失败。拿到跨平台项目包,音效不能播放时,先看.pro有没有multimedia,再看目标系统的后端依赖。
资源系统(.qrc文件)在 Windows 上有个编码坑:.qrc文件里的资源路径如果包含中文,编译器会根据系统代码页解析,在简体中文系统上没问题,但你把项目发给用英文系统的人,资源路径全断。全部用英文命名资源文件和路径,是 Qt 项目的铁律。
5. 飞机大战避坑:我在这类小游戏上踩过的五个典型问题
5.1 现象一:敌人动起来一卡一卡,帧率只有十几
这是我做第一版飞机大战时最崩溃的问题。敌人移动逻辑看起来没问题,但帧率显示只有 12 FPS,飞机像在放慢动作。后来用QElapsedTimer测量发现,updateFrame()里光paint()就占掉 40ms,比整个事件循环还慢。
原因有两个:一是paint()里加载了QPixmap,每帧从磁盘读图片;二是碰撞检测用了scene->items(QPointF, Qt::IntersectsItemShape),这个函数要遍历场景中所有项并调用各自的shape()方法,代价很高。
解决方式:所有图片在构造函数里一次性加载为QPixmap成员变量;碰撞检测用手写的矩形相交函数替代场景级items()查询。修完帧率从 12 FPS 直接跳到 60 FPS。这事的教训是,Qt 的接口方便不等于它就是性能最优解,热点路径上的每行代码都要考虑耗时。
5.2 现象二:Qt 崩溃提示 "double free" 或访问已释放的 QGraphicsItem
在“重新开始”功能里,我先delete player;再new PlayerPlane(nullptr)再scene->addItem(player),结果第二次点重新开始时直接崩溃,调试器停在free()函数内部。
原因是player第一次被创建时通过scene->addItem(player)加入了场景,场景成为它事实上的父对象(所有权归场景)。我手动delete player后,场景的图形项列表里还留着指向这块内存的指针,下一次场景刷新时访问悬空指针。
解决办法:重建逻辑里不单独delete任何曾加入场景的图形项,直接用scene->clear()清空整个场景,再重新创建所有实体。如果某次只想移除单个敌机且不确定它的父对象是谁,先调用scene->removeItem(enemy)从场景摘除,再delete enemy。顺序不能反。
5.3 现象三:中文路径或中文资源名导致构建失败
把项目放在D:\课程设计\飞机大战\目录下打开,编译直接报error: dependent '..\..\..\Qt\5.15.2\...' does not exist,但目录明明在。我试着把整个项目目录移到纯英文路径D:\Projects\airplane_battle\,一次编译通过。这是 Qt 的 msvc 工具链对非 ASCII 路径的处理缺陷,官方没有彻底修复,所以血泪经验是:任何 Qt 项目的根目录必须全英文。同理,.qrc里的资源路径也不要有中文。
5.4 现象四:Qt 5.15.2 在 Windows 上双击闪退
编译和运行都没问题,但只要把 exe 复制到另一台机器上,双击就在启动画面一闪而过之后崩掉。多数情况是目标机器没装 Visual C++ Redistributable。Qt 5.15.2 的 MSVC2019_64 版本依赖 VC++ 2015-2019 运行库,装完就能跑。还有一种隐藏原因:exe 目录下缺少platforms插件目录。Qt 程序运行时需要加载qwindows.dll平台插件,如果你的部署方式只是把 exe 拷走,就必须把Qt\5.15.2\msvc2019_64\plugins\platforms整个目录复制到 exe 相邻目录下,否则程序连 QApplication 构造都过不去。
5.5 现象五:坐标体系混乱,子弹从机腹发射却从屏幕中心冒出来
QGraphicsItem默认的坐标是相对父项或场景的局部坐标。飞机作为场景的直接子项,它的pos()是场景坐标;但飞机内部的炮口坐标是基于飞机自身原点的相对坐标。我最初把炮口坐标直接加到场景坐标里,算出子弹的初始位置是player->pos() + QPointF(0, 20),实际跑起来子弹从飞机底部冒出来,虽然勉强能用但方向总差一点。
后来统一用mapToScene()把局部坐标转成场景坐标,问题一次解决。
QPointF muzzlePos = player->mapToScene(QPointF(player->boundingRect().center().x(), 0)); Bullet *bullet = new Bullet(); bullet->setPos(muzzlePos); scene->addItem(bullet);这种局部/场景坐标混用的问题,在飞机大战里几乎必现。一个统一的约定是:所有游戏逻辑一律用场景坐标,只有绘制时才依赖局部坐标。物体之间的相对位置计算,先用mapToParent或mapToScene转成同一坐标系再加加减减。
提示:碰到坐标相关的诡异问题,先打印物体自身的
pos()、boundingRect()和mapToScene(QPointF(0,0))三个值,对照输出很快就能看出是哪一层的坐标系没统一。
6. 把这个项目改到自己手上:三个验证技巧与 Qt Creator 调试习惯
6.1 用 qDebug 与 Qt 命令行工具验证对象树与 QSS 加载
拿到任何一个 Qt 源码包,我第一件事不是跑游戏,而是插入一段dumpObjectTree()调用来验证对象树结构。在main.cpp里,在MainWindow构造完成后加一行:
MainWindow w; w.show(); w.dumpObjectTree();这段输出会列出所有带父对象的QWidget和QObject,能看出哪些对象没有按预期挂到父节点下。我之前排查一个对话框关闭后内存不释放的问题,就是靠它发现某个子窗口的父对象是nullptr,导致每次关闭都泄漏。飞机大战这种小游戏,跑一遍dumpObjectTree能快速确认所有 keyPressEvent 处理器是否挂在正确窗口下——如果某个QWidget不在树里,它的按键事件永远传不出去。
Qt 还有个命令行工具叫qDebug()输出重定向。Windows 上默认输出到调试器,看不到。在main.cpp里安装一个日志处理器,把输出写到本地文件,排查闪退时能捞到最后一刻的日志。
6.2 用 Qt Creator 的调试器验证信号槽连接与内存释放
信号槽的经典坑是:槽函数没被调用,但代码看起来连得正确。老手会在断点停在发射emit之后,查看QObject::receivers()的返回值——如果返回 0,说明信号一个槽都没连上。Qt Creator 的调试器可以直接查看对象的d指针里的连接列表,但更直观的办法是在槽函数第一行打断点,然后触发相应动作。如果断点没命中,沿信号发射点往回查。
内存释放这块,Qt Creator 的「内存使用」工具配合 AddressSanitizer 编译选项最好用。在.pro里加QMAKE_CXXFLAGS += -fsanitize=address,跑一局游戏退出时,泄漏的QGraphicsItem会直接打印在输出窗口,比猜快得多。注意 ASan 只支持 MSVC 的 debug 模式,release 模式加这段编译选项会报错。
6.3 给飞机大战加一个护盾道具:一次完整的信号槽扩展
验证你真正吃透了这套代码,最好的方式是加一个小功能。我给项目加过一个护盾道具:每 10 秒在场景左上角出现一个金色圆环,玩家碰到后获得 3 秒无敌。
关键步骤只有三处。第一步,定义信号和槽:
class ShieldPickup : public QGraphicsObject { Q_OBJECT signals: void shieldCollected(); };第二步,在GameWidget里连接信号并实现无敌计时。第三处是给PlayerPlane增加一个setInvincible(bool)方法,在paint()里画一层半透明金色蒙层作为视觉反馈。这个功能 30 行以内完成。如果你能顺利做出来,说明信号槽、对象树、事件循环这三块已经内化成你自己的东西了。当年我在这个功能上卡了一下午——不是代码写不出来,而是忘记在restartGame()里重置护盾状态,导致重开一局后玩家永久无敌。现在每次加新状态,我都会顺手检查所有reset路径,不放过任何一条能通往旧状态的分支。希望你拿到这个项目时,也能比我少踩几个坑。希望帮到你。
本文还有配套的精品资源,点击获取