资讯动态

Qt+C++植物大战僵尸课程设计源码解析:架构、实现与避坑

发布时间:2026/10/5 12:09:28 来源:尧图企业网站定制
简介基于Qt与C框架编写的简易植物大战僵尸游戏完整源码已打包为zip压缩包面向计算机相关专业在校学生、教师及企业开发者可作课程设计、毕业设计或大作业的原型参考。项目采用模块化组织方式地图、植物、僵尸、阳光、冷却系统及音效分别由独立类实现并通过线程机制调度植物攻击与僵尸生成帮助读者理解Qt事件循环、信号槽、面向对象设计和多线程协作等关键知识点。包内共37个文件含17个cpp源文件与16个h头文件cpp实现地图绘制、植物攻击、僵尸移动与阳光生成等核心逻辑h头文件声明各类接口另有pro工程配置、qrc资源管理文件及README说明文档整体压缩后仅32KB目录与模块划分清晰便于按需定位代码位置。目前已有536人学习下载代码经测试运行成功适合需要快速上手游戏开发、完成课程报告或演示项目的人群学习借鉴。1. 简易植物大战僵尸的课程设计源码拿到手不要只想着“跑起来”很多大三学生拿到这个 Qt C 的植物大战僵尸源码包第一反应是打开 .pro 文件点运行期待屏幕上立刻跳出豌豆射手。实际上一套课程设计源码的用法完全不是这样。这个“简易版”不是商业游戏的缩小版而是一个刻意只保留了“种植→射击→僵尸进攻→胜负判定”闭环的教学项目覆盖了 QGraphicsView 图元管理、QTimer 定时器驱动、信号槽解耦和 C 的继承多态正好卡在“你会语法但没写过完整工程”的能力断点上。适合三类人要交课程设计但不想从零搭骨架的学生、想用一个小游戏验证 Qt 学习成果的前端转行开发者、以及需要带新人练手的导师。看懂它比跑通它值钱得多。2. Qt C 做植物大战僵尸先从架构上决定游戏怎么活着课程设计跟玩具级 demo 最大的区别是老师会追问“你这里为什么这样写”。植物大战僵尸这种游戏表面是一场实时攻防实际在代码层面就是“一堆带图片的矩形在场景里按规则移动、碰撞、死亡”。框架选对能省掉三分之二的力气。2.1 为什么是 QGraphicsView图元、场景、视图三层分离先明确一个常见选择用 QWidget 直接在 paintEvent 里画也不是不能做但植物、僵尸、子弹的碰撞判定都要手动维护坐标和生命周期画一次全帧重绘还容易闪。QGraphicsView 的方案把界面拆成三层QGraphicsScene 管逻辑场景和碰撞QGraphicsView 负责显示和鼠标交互每个可动对象是一个 QGraphicsItem。移动、缩放、点击检测由框架代劳你只需要把“每个对象自己动起来”的规则写进 item 里。常见做法是让植物、僵尸、子弹继承 QObject QGraphicsPixmapItemQObject 给它们信号槽能力QGraphicsPixmapItem 让它们能被场景绘制和选中。这里有一个新手必踩的坑QGraphicsPixmapItem 默认不接收鼠标事件记得在构造函数里 setAcceptedMouseButtons(Qt::LeftButton) 并开启 setAcceptHoverEvents(true)不然鼠标点在植物上场景收不到事件种植和铲除植物点不中。场景坐标也是容易出问题的地方。sceneRect 不设置时默认是从 (0,0) 开始不断扩张玩家滚动滚轮后可能把视图滚到空白区域。固定棋盘就应该调用 setSceneRect(0, 0, 800, 520) 这类硬值同时把 QGraphicsView 的 scrollBarPolicy 设为 ScrollBarAlwaysOff让“一局一屏”不出现滚动条。图元的上下层关系由 ZValue 决定一般约定地面背景 0植物 1僵尸 2子弹 3阳光 4。数值不用很大但必须分层否则子弹会被植物图片盖住看起来像穿模。2.2 类职责划分六个类撑起一局游戏一套简易课程设计类的数量不要贪多。六个核心类足够了多出来的类大概率是没想清楚职责就硬拆的。GameWidget游戏主窗口持有 scene 和 view负责初始化关卡布局。Plant 与若干子类向日葵、豌豆射手、坚果墙子类只覆写行为差异。Zombie僵尸基类管理移动速度、血量、啃食状态。Bullet子弹直线飞行命中判定。SunManager阳光生成、拾取和计数。GameControllerQObject 子类持有全局 QTimer驱动兵线生成与状态检查。为什么把 QTimer 单独放 GameController 而不是窗口类因为窗口类负责界面生命周期定时器负责游戏逻辑混在一起会出现“窗口关了定时器还在跑进程退出不了”的经典问题。后面避坑章会展开。这里顺带说一句别把 MVVM 框架那套分层习惯硬塞进 QGraphicsView 小游戏。植物大战僵尸的实体天然带图片和坐标强行抽 ViewModel 只会让 item 和逻辑类互相拉扯代码量翻倍还更难讲清楚。课程设计阶段Controller 只做一件事接收信号、改变状态、驱动定时器。2.3 继承与多态植物基类怎么设计// plant.h 植物基类公共逻辑收敛到这里子类只覆盖差异点 #ifndef PLANT_H #define PLANT_H #include QObject #include QGraphicsPixmapItem #include QTimer class Plant : public QObject, public QGraphicsPixmapItem { Q_OBJECT public: enum Type { SunFlower 0, Peashooter, WallNut, CherryBomb }; explicit Plant(Type t, QGraphicsItem *parent nullptr); virtual ~Plant(); int getCost() const { return cost; } int getHealth() const { return health; } void takeDamage(int d); signals: void requestShoot(QPointF pos); // 子类通过信号请求发射子弹 protected: virtual void onTimerFired() 0; // 每个子类决定“周期到了干什么” int cost; // 种植阳光费用 int health; // 当前血量 int maxHealth; // 满血用来画血条 Type type; QTimer *actionTimer; // 周期动作引擎 }; #endif // PLANT_H逻辑说明这个基类把植物共有属性全部收拢子类构造函数只需要赋值 cost、health并调用 actionTimer-setInterval(...) 决定多久触发一次然后重写 onTimerFired()。外部代码不用知道在操作哪种植物调用 getCost、takeDamage 就够了。这就是答辩时最值得讲的一句话“植物系统的变化点被封装在虚函数里新增植物不影响原有代码”。参数说明cost 和 health 是游戏平衡核心。第一次做很容易把所有数值拍脑袋定结果向日葵和豌豆射手价格一样玩家永远无脑种豌豆。常见的入门参数组合是向日葵 50 阳光、豌豆射手 100、坚果 50僵尸初始血量 200植物血量 300。这套组合能保证前期阳光紧缺、玩家需要运营而不是纯粹拼手速。再看一个子类实现。// peashooter.cpp 豌豆射手重写周期动作发射子弹 #include peashooter.h #include gamecontroller.h Peashooter::Peashooter() : Plant(Plant::Peashooter) { cost 100; health maxHealth 300; actionTimer-setInterval(1500); // 1.5 秒射一发 } void Peashooter::onTimerFired() { emit requestShoot(pos()); // 信号交给 GameController 统一生成子弹 }逻辑说明Peashooter 自己不做 new Bullet 这件事而是发 requestShoot 信号让 GameController 统一生成子弹。为什么这样设计因为子弹需要加入场景而场景句柄在 GameController 手里。如果植物直接 new Bullet就要把 scene 指针传来传去类与类耦合立刻变重。这是典型的信号槽解耦答辩时很加分。参数说明setInterval(1500) 注意 Qt 定时器精度是毫秒节奏手感全靠它。不要盲目追求短间隔1500 到 1800 毫秒是手感不错的区间低于 800 毫秒会出现“子弹叠在一起飞”的画面。2.4 源码目录怎么读入口、控制、实体三步走规范的 Qt 课程设计源码包目录结构通常长这样根目录放 .pro 或 CMakeLists.txt、README 或设计报告headers/ 放 .h 头文件sources/ 放 .cpp 源文件res/ 放 qrc、PNG 图片、音效。翻阅顺序不要跟着文件名字母排序走按“入口→控制→实体”来。main.cpp 字数最少只负责创建 QApplication 和 MainWindow。GameWidget 构造函数里能看到 scene 创建、背景设置、第一波僵尸初始化。实体类从 Plant 和 Zombie 读它们最简单能快速建立“对象自己动”的认知。最容易跳过的是 GameController但它是整局游戏的心脏全局 QTimer 管阳光、僵尸生成、胜负判定。.pro 文件里的配置决定了你能不能按下 F5。Qt 5.15 常见写法QT core gui widgets greaterThan(QT_MAJOR_VERSION, 4): QT widgets CONFIG c11 TARGET pvz TEMPLATE app SOURCES main.cpp \ mainwindow.cpp \ gamewidget.cpp \ plant.cpp \ peashooter.cpp \ zombie.cpp \ bullet.cpp \ sunmanager.cpp HEADERS mainwindow.h \ gamewidget.h \ plant.h \ peashooter.h \ zombie.h \ bullet.h \ sunmanager.h RESOURCES res.qrc参数说明QT widgets 在 Qt5 里必须写少了会报与 QWidgets 相关的配置错误。CONFIG c11 保证 QTimer、Lambda 连接这些语法特性可用Qt 6 项目可以提到 c17。TARGET 是生成 exe 的名字和课程设计标题保持一致更省事。RESOURCES 一定不能漏资源文件编不进去加载 PNG 会全部失败。3. 把源码在本地跑通环境选型、编译排错与最小验证拿到源码别急着双击 .pro环境不对直接死在编译阶段。课程设计源码包的运行环境比功能本身更要求“稳”——你答辩用的电脑大概率不是开发那台。3.1 环境选型Qt 5.15.2 配 MSVC 2019 是最常见组合Windows 上做 Qt Widgets 课程设计最常见是 Qt 5.15.2 搭配 MSVC 2019 64 位。理由很实在在线资料最多报错信息在搜索引擎能直接命中。除非源码明确写了 Qt 6 特性否则 QGraphicsView 这套 API 在 Qt5/Qt6 差异不大跑通后可以迁移。装好 Qt 后打开 .pro 时需选择对应套件。调试选“Qt 5.15.2 MSVC2019 64-bit Debug”发布选 Release。一个容易忽略的细节Qt 5.15.2 msvc2019_64 配 VS2022 经常出现编译器和 Qt 库版本不匹配提示。如果遇到右键项目属性检查“VC 目录”里的库路径指向哪个 Qt 目录。这属于“玄学”现场通常是装了多个 Qt 版本导致的检查 PATH 解决一大半。MinGW 和 MSVC 的选择也值得说。有些源码用到的第三方库只编了 MSVC 版用 MinGW 编译会报一堆“无法解析的外部符号”。课程设计源码基本不依赖第三方库MinGW 也能跑但如果带 .pro 和 .sln 两套工程优先用 MSVC 套件报错更少。3.2 打开项目后报错先确认这三个位置场景一双击 .pro 后Qt Creator 提示 “No suitable kit found”。原因是只装编译器没装 Qt 套件。解决工具→选项→Kits→Qt Versions手动添加 qmake.exe 路径再在编译器中添加 VS 的 cl.exe。场景二编译瞬间报错找不到 qtwidget 头文件。这大概率是 .pro 里 QT 声明不全或者套件选错。解决.pro 里显式写 QT widgets确认套件与源码要求一致。场景三构建完找不到 exe。原因是 Qt Creator 默认构建目录在 build-xxx-Desktop_Qt_5_15_2_MSVC2019_64bit-Debugexe 在 debug 或 release 子目录里不在源码目录。这不是代码问题是 Qt Creator 的默认构建策略。还有一个是源码本身带中文注释或中文字符串时MSVC 编译会报 C4819 警告甚至 C2065 错误。解决是用 UTF-8 with BOM 保存源文件或者在 .pro 里加 MSVC 的 utf-8 编译选项。课程设计源码经常出现作者用 GBK 写注释的情况拷到新版系统上翻车率极高。3.3 构建细节Debug 与 Release 目录部署带不带 dll调试阶段用 Debug 构建编译器不优化报错行号准断点也能命中。答辩演示建议用 Release 构建游戏更流畅崩溃概率更低。但 Release 构建的 exe 单独拎出来是跑不起来的因为缺少 Qt 运行库。部署常见做法是 Qt Creator 里选择“构建→运行 qmake”后再运行 windeployqt 工具。这个工具是 Qt 自带的命令行工具执行 windeployqt pvz.exe它会把需要的 Qt 运行库和插件复制到 exe 所在目录。注意要在命令行里指向 Qt 的 bin 目录或者把该目录加入 PATH。不带插件时最常见的现象是本机双击能运行拷到别的电脑双击毫无反应或闪退任务管理器里进程一闪而过。这属于“看起来能跑实际没部署”的典型翻车。3.4 最小验证路径先构建、再运行、最后才动代码部分读者拿到源码会先改一堆变量发现画面不对又改回去。我习惯用“三遍法”。第一遍默认构建什么都不要改目标只是编译通过、窗口弹出。第二遍改一个无关紧要的数值比如向日葵产阳光间隔确认改动能生效。第三遍在 Plant 构造函数加一句 qDebug() plant created跑起来看控制台有没有打印。三遍下来环境是通的、资源是通的、改动入口是通的再进真正玩法修改。这套路径对应一个很实在的结论源码能跑起来不算成功你能在源码里找到一个自己改的点并验证生效才算真正接手。课程设计答辩最怕“老师让我改一个参数我改完发现没变”那比讲不清原理还致命。4. 种植、射击、僵尸波次、阳光经济四个核心系统怎么实现这一章进到具体玩法代码。每个玩法都涉及“输入→逻辑→反馈”三个环节只看局部代码容易迷路所以先把环节串起来再贴关键段。4.1 种植判定鼠标点击到棋盘格子的换算植物大战僵尸的棋盘一般是 5 行 9 列。常见做法是把 QGraphicsView 的鼠标点击坐标转换为场景坐标再用常量把场景坐标映射为行列。这个映射值不能随便写我实际用的是格子宽 80、高 100偏移原点 (40, 80)。// gamewidget.cpp 鼠标点击种植判定 void GameWidget::mousePressEvent(QMouseEvent *event) { QPointF scenePos mapToScene(event-pos()); // 视图坐标转场景坐标 int row (scenePos.y() - 80) / 100; // 转换为行 int col (scenePos.x() - 40) / 80; // 转换为列 if (row 0 || row 5 || col 0 || col 9) return; // 棋盘外忽略 if (gridOccupied[row][col]) return; // 已有植物防止叠种 if (sunCount selectedPlantCost()) return; // 阳光不足直接 return Plant *p createPlantByType(currentSelection); p-setPos(col * 80 40, row * 100 80); // 对齐到格子中心 scene-addItem(p); gridOccupied[row][col] true; sunCount - p-getCost(); }逻辑说明mapToScene 把界面坐标换算成场景坐标是 QGraphicsView 体系里最基础的调用。行和列计算取决于格子大小与原点常量一致时绘制和点击判定自然对齐。gridOccupied 用二维 bool 数组即可不必上复杂结构。阳光不足先检查再扣费防止“阳光不够也种上了”的负资产 bug。参数说明格子宽 80、高 100、原点 (40, 80) 是我常用的黑板参数。换一套图片素材时格子宽高要按实际图片大小调整否则会出现“植物种在地砖缝隙里”的观感。row 和 col 的计算里先减原点再除尺寸顺序不能反括号一错所有格子都会偏移。4.2 阳光经济生成、拾取与卡牌选择阳光系统的核心是 SunManager。它要管三件事定期从天空掉落阳光、向日葵每隔一段时间在自身位置产阳光、点击阳光将其计入 sunCount 并更新 UI。// sunmanager.cpp 阳光生成与拾取核心逻辑 SunManager::SunManager(QGraphicsScene *scene) : m_scene(scene), m_sunCount(50) { m_dropTimer new QTimer(this); m_dropTimer-setInterval(10000); // 每 10 秒掉一个阳光 connect(m_dropTimer, QTimer::timeout, this, SunManager::dropSun); } void SunManager::dropSun() { Sun *s new Sun(); int row QRandomGenerator::global()-bounded(5); s-setPos(QRandomGenerator::global()-bounded(720) 40, 0); // 从顶部掉落到指定行动画交给 Sun 自己 m_scene-addItem(s); }逻辑说明阳光这个图元被设计成会“自己动”构造后它开始下落落到目标行停住再过 6 秒渐隐消失。这是一种常见的 QGraphicsItem 动画模式不用外部队列管理item 内部用 QTimer 或 QVariantAnimation 驱动自身。点击拾取则是重写 mousePressEvent在内部判断点击命中后发射 collected 信号SunManager 收到后加阳光数量并 delete 该图元。参数说明初始阳光设 50刚好够种一棵向日葵这是原版节奏。dropTimer 间隔 10 秒一落玩家要等两波才凑出 100 阳光种豌豆运营压力合适。把间隔改到 5 秒游戏难度会断崖式下降这也是答辩现场最容易演示的参数。卡牌选择 UI 一般做在窗口上方用一个 QListWidget 横向排列植物图标。选择时点击图标把 currentSelection 设为对应类型有些实现还做了冷却灰度课程设计里可用可不用。竞品做法是铲子功能右键点击已种植植物回收并返还部分阳光这个功能实现小但演示效果好有精力的可以加上。4.3 子弹飞行与碰撞检测方向、对齐和伤害值豌豆射手产子弹不难难的是“子弹打不打得中僵尸”。要点是分成两个环节发射逻辑放在植物侧碰撞判定放在场景侧。子弹只需要知道自己的方向和速度不关心场上有多少僵尸。// bullet.cpp 子弹移动与命中检测 void Bullet::advance() { setPos(x() speed, y()); // 横向飞行 QListQGraphicsItem * items collidingItems(); for (QGraphicsItem *item : items) { Zombie *z dynamic_castZombie *(item); if (z) { z-takeDamage(damage); delete this; // 命中后自杀注意迭代安全性 return; } } if (x() scene()-sceneRect().right() 50) { delete this; // 飞出边界清理 } }逻辑说明collidingItems 由 QGraphicsScene 提供返回当前 item 与其他图元的重叠列表。碰撞检测不用手写矩形相交这是框架红利。用 dynamic_cast 区分子弹碰到的是僵尸还是其他植物避免误伤。delete this 的合法性要注意QGraphicsItem 在 advance 循环中删除前提是该对象后续不再被访问。稳妥做法是用一个标记变量把待删除对象推迟到帧结束统一处理。参数说明speed 我给 8 到 10 像素/帧伤害 20。速度太快子弹会穿透因为每帧位移超过僵尸宽度太慢则手感粘滞。典型穿透场景一帧移 40 像素僵尸图宽 60 像素碰撞检测只在两个离散点子弹可能跳过僵尸。解决方式不是加复杂算法而是把每帧位移控制到小于僵尸最小宽度的一半。轴向碰撞判定可以用 boundingRect() 做缩放检测把图片透明边缘裁掉命中率会准很多。4.4 僵尸波次QTimer 生成与难度曲线僵尸不能一次性全出也不能让玩家挂机。常见实现是 GameController 里两个 QTimer一个按固定间隔生成僵尸另一个缩短间隔、拉高难度。难度曲线课程设计不用做太精细我一般设四波前 30 秒每 10 秒出 1 只接下来 30 秒每 7 秒出 1 只再往后每 5 秒出 2 只最后一波出 3 只。每波切换时用 qDebug 打印提示方便演示讲故事。生成位置只随机行列固定在右侧边缘。如果列也随机僵尸会直接出现在场地中央玩法失衡。随机行用 QRandomGenerator::global()-bounded(5) 更规范qrand() 已废弃某些新版本编译会报警告。胜负判定放在同一个 Controller当有僵尸走到最左侧时立即判负当累计击杀数达到目标时判胜。判负条件要放在僵尸位置更新后立刻检查否则可能出现“僵尸走出屏幕还继续啃”的诡异画面。5. 避坑从编译报错到运行黑匣子的排查笔记这章是我实际跑源码时积累的踩坑记录按“现象→原因→解决”写比泛泛的“注意内存管理”有用得多。5.1 编译错误 dependent qtwidget套件不匹配还是 .pro 缺模块现象新建工程打开 Qt Creator 直接构建报 -1: error: dependent ............\qt\5.15.2\msvc2019_64\include\qtwidget... 后面跟着循环依赖构建中断。原因几乎总能落在两点。一是 .pro 的 QT 段少了 widgetsmoc 生成的文件引用了未声明的模块头文件二是机器上装多套 Qtqmake 被 PATH 指到与套件不匹配的版本。解决先检查 .pro 是否有 QT widgets补上后重新 qmake。再开“项目→构建套件”确认当前套件用 msvc2019_64 且 Qt 版本 5.15.2。最后把项目里的 .pro.user 文件删掉重新打开项目能清掉九成“依赖找不到”类问题。5.2 图片加载成透明或黑块qrc 路径与资源前缀现象窗口出来了但所有植物、僵尸是透明的或黑色方块控制台打印 “No such file or directory”。原因qrc 里注册的是 “/images/sunflower.png”代码里写 “images/sunflower.png”。Qt 资源查找要求写 “:/images/sunflower.png”缺冒号或前缀错一个字符都加载失败。解决统一写 QPixmap(:/images/sunflower.png) 带 :/ 的写法。做一份清单把 qrc 里的 alias 和实际图片文件名对好写一个启动自检函数加载失败时打印 qCritical 而不是让图片静默空着。答辩时一张图变黑块就是老师追问“异常情况有没有处理”的最好素材。5.3 信号槽连接了但没触发QObject 继承顺序与元类型注册现象豌豆射手 requestShoot 信号发了GameController 的槽没被触发但代码看着完全正确。原因多重继承时 QObject 不在第一位或类没加 Q_OBJECT 宏。Plant 继承 QObject 和 QGraphicsPixmapItem 时QObject 必须在最前否则 moc 生成的元对象代码因内存布局错乱无法连接。另一个隐蔽问题带自定义参数的信号槽跨线程连接时没 qRegisterMetaType 注册会静默失败。解决QObject 放继承列表第一位编译后确认 moc 文件生成。自定义参数连接用 Qt::DirectConnection 做最小验证链路通了再改默认连接方式。5.4 关闭窗口后进程还在后台运行现象游戏窗口关掉了任务管理器里 exe 还在风扇开始狂转。原因定时器循环没停或对象没被正确析构。常见于全局 QTimer 放 GameController而 GameController 没有被父对象管理窗口 CloseEvent 只关窗口不关 GameController定时器在后台继续 tick。解决MainWindow 的 closeEvent 里手动调 gameController-stop() 并 deleteLater。更规范是让 GameController 继承 QObject以窗口为 parent 实例化窗口析构时子对象自动清理。这个坑靠断点不好发现建议写日志验证关窗口后等两秒看进程是否还在输出。5.5 图元一多就卡成幻灯片性能排查三板斧现象前期流畅第七八波僵尸一出帧率掉到 20光标都飘。原因先看是否每帧全场景 update()。QGraphicsView 支持局部更新直接调 scene-update() 会把所有图元重绘应换成 item-update()。其次看图片是否有大面积空白一张 1000x1000 的透明图PNG 渲染耗时会成倍增长。最后看有没有在 QTimer 里高频创建临时 QPixmap。解决开 Qt Creator 性能分析器确认热点函数再检查 sprite 图片是否裁剪压缩最后把不需要的图元 setVisible(false) 而不是 delete减少频繁 new/delete。课程设计规模到不了需要空间索引的程度这三板斧足够。6. 进阶让课设从“能跑”到“能讲”的一页自查清单课程设计答辩的评分点不在游戏难度而在“你知不知道自己在写什么”。给一份自查清单按分数从低到高排列。第一档是能跑默认构建能编译、窗口弹出、植物能种、僵尸能走、子弹能打、胜负能判。第二档是能改随便改三个参数向日葵产阳光间隔、僵尸血量、子弹伤害改完直接生效。第三档是能讲拿到项目两个小时内能用十分钟讲清类关系、信号槽链路、定时器机制。我在这个环节吃过一次亏当时能熟练改代码但老师问“豌豆射手发射子弹这个流程信号发给谁为什么不是自己 new”我愣了几秒才反应差点丢分。我的习惯是给源码目录补一份注释版 README把六条关键链路写清楚点击种植链路、阳光生产链路、子弹发射链路、僵尸生成链路、碰撞死亡链路、胜负判定链路。每条链路写三行一行触发信号一行处理槽一行关键参数。这份东西答辩前夜看一遍比翻代码找函数快得多。最后一个小技巧把 GameController 的僵尸生成间隔调成一个能手动拨的变量注册一个快捷键加减。演示时当评委面把僵尸生成速度调快调慢展示难度曲线的影响这比静态画面有说服力得多。如果这个源码包读到这里还没在本地跑通先别碰玩法回到第 3 章的“三遍法”把最小验证走完。代码能跑只是门槛能讲清楚为什么这么设计才是这个课程设计真正的收获。希望帮到你。本文还有配套的精品资源点击获取

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价 →
↑