简介:这是一款基于Qt框架与C++语言开发的宝可梦风格2D角色扮演游戏工程源码,面向Qt入门学习者及对游戏开发感兴趣的读者,重点呈现图形视图框架、地图场景搭建与回合制战斗逻辑的完整项目实践。资源以zip压缩包发布,整体仅19KB,共20个文件,包含9个cpp源文件与8个h头文件,另附TMX地图文件、pro工程配置与qrc资源管理文件,代码按游戏世界、宝可梦培养、战斗系统与玩家系统四大模块划分,结构清晰,便于分模块研读。目前已有103人学习浏览,适合希望快速掌握Qt游戏框架、移动碰撞检测与状态管理的读者。通过该工程可系统学习QGraphicsView与QGraphicsScene渲染实现、TMX地图加载解析、宝可梦属性相克与进化逻辑、技能选择与战斗动画,以及角色背包管理等完整玩法。项目预留了良好的扩展接口,后续可增补更多宝可梦种类、地图场景与剧情任务,兼具教学演示与二次开发价值,是入门Qt游戏开发的优质参考。
1. 基于QT(C++)的宝可梦小游戏:为什么值得自己写一遍
我见过不少想入门 C++ 的开发者,第一个项目就去啃引擎源码,结果被构建系统和蓝图节点劝退。其实宝可梦这种回合制 RPG,骨架刚好卡在 QT(C++)最擅长的那条线上:一个 QApplication 加状态机就能跑完地图、战斗、背包三条主线,不依赖任何第三方引擎。这个练手方向能解决的核心问题是——用最小的库依赖把 C++ 对象、事件循环和绘图 API 串成一个完整游戏,同时交付一个别人愿意点开玩两分钟的成品。它适合两类人:一类是 Qt Widgets 只写过表单、想试试中型应用怎么划分模块的客户端开发者;一类是刚啃完语法、想知道结构体、多态和信号槽在真实程序里怎么落地的初学者。下面按 Windows + Qt 5.15.2 + MSVC 2019 64位这套最常见的组合展开,顺手把 Ubuntu 下搭 Qt 开发环境的差异也点一下。
2. 架构先行:场景状态机与 Qt Widgets / QML 的选型
2.1 为什么是 Widgets 而不是 Qt QML:按界面复杂度算账
先回答一个绕不开的问题:Qt 现在有 Widgets 和 QML 两套界面体系,宝可梦小游戏该选哪套?我的建议是 Qt Widgets,除非你已经明确要用大量自定义动画。原因很简单:回合制 RPG 的特点是场景之间切换频繁,但单个场景内的界面复杂度不高。主菜单一个按钮列表,战斗界面一组指令菜单加文本区,Widgets 自带 QStackedWidget、QListWidget、QLabel,全部是现成控件,不需要像 QML 那样为了一个焦点导航去写状态绑定。
QML 的优势在流畅的转场动画和粒子特效,它和 Widgets 的差别有点像前端里声明式框架和传统操作 DOM 的区别:有前端基础的人会觉得 QML 亲切,纯 C++ 背景则走 Widgets 上手更快。至于搜索引擎里常看到的 qt mvvm 框架,对小游戏是过度设计——你总共三个页面,引入一层 ViewModel 反而要多维护一套映射关系。把精力留在游戏逻辑上,不要被框架热词带偏。
2.2 最小状态机:四个状态加 QStackedWidget 切换
我一般会把游戏主线拆成四个状态:主菜单、野外地图、战斗、收服/暂停弹窗。背包和设置这类次级界面不单独占状态,做成战斗内的模态弹窗更省事。切换统一收敛到一个函数里,避免散落的setCurrentIndex调用把状态搅乱。
// GameState.h #ifndef GAMESTATE_H #define GAMESTATE_H #include <QMainWindow> #include <QStackedWidget> // 游戏主线只保留三个页面,弹窗类界面独立管理 enum class GameState { MainMenu, // 主菜单:新游戏 / 读档 MapScene, // 野外地图:角色移动、草丛遇敌 BattleScene // 回合制战斗:招式 / 换宠 / 逃跑 }; class MainWindow : public QMainWindow { Q_OBJECT public: explicit MainWindow(QWidget *parent = nullptr); // 所有跨场景跳转都走这个统一入口 void switchTo(GameState newState); private: QStackedWidget *stack; QWidget *menuPage; QWidget *mapPage; QWidget *battlePage; GameState currentState = GameState::MainMenu; }; #endif // GAMESTATE_H// MainWindow.cpp #include "MainWindow.h" MainWindow::MainWindow(QWidget *parent) : QMainWindow(parent) { stack = new QStackedWidget(this); // 三个页面都作为 stack 的子控件注册,索引顺序与枚举顺序一致 menuPage = new QWidget(this); mapPage = new QWidget(this); battlePage = new QWidget(this); stack->addWidget(menuPage); stack->addWidget(mapPage); stack->addWidget(battlePage); setCentralWidget(stack); switchTo(GameState::MainMenu); } void MainWindow::switchTo(GameState newState) { currentState = newState; switch (newState) { case GameState::MainMenu: stack->setCurrentIndex(0); break; case GameState::MapScene: stack->setCurrentIndex(1); break; case GameState::BattleScene:stack->setCurrentIndex(2); break; } }这里有两个参数细节值得注意。第一,状态枚举值必须和addWidget的调用顺序一一对应,如果中间插入一个设置页,后面的索引全部错位,跳转就会跳到错误的页面,这是新手最容易踩的坑。第二,这个方案是同步切换页面,不要和浏览器那种压栈出栈的导航混为一谈;如果你后续想让角色从地图平滑过渡到战斗场景,只需要在switchTo里先播一段动画再切换索引。
提示:用枚举而不是整数存状态,编译器会在 switch 分支里帮你检查遗漏,改状态名时全局搜索也方便。小项目可能感觉不到,等状态加到六个以上时收益很明显。
有没有更省事的方案?如果你从 Qt 5.15 升级到 Qt 6,这套代码基本不用改,QStackedWidget的接口非常稳定。入口文件就更简单了,直接构造MainWindow,不需要额外包一层 QApplication 之外的容器。
// main.cpp #include <QApplication> #include "MainWindow.h" int main(int argc, char *argv[]) { QApplication app(argc, argv); MainWindow w; w.resize(960, 640); // 固定窗口大小,避免地图场景被拉伸 w.show(); return app.exec(); }这段代码里QApplication必须存在,它负责事件循环和资源管理;argv 里以 -qt 开头的参数会被 Qt 自动解析,其余参数原样保留,这个特性在你以后做命令行调试时会用到。
2.3 环境准备:Qt Creator + MSVC 2019,还是 VS2022 + Qt Solutions
编译套件我推荐 MSVC 2019 64位,而不是 MinGW。原因有两点:第一,MSVC 生成的调试信息在 Qt Creator 里和 Windows 的符号服务器配合更好,崩溃时能直接定位到系统 DLL 的调用栈;第二,项目以后要接第三方库,比如读取存档文件的加密库,基本都是 MSVC 编译的二进制,MinGW 的 ABI 不兼容,到时候哭都来不及。习惯 Visual Studio 的人也可以装 vs2022 qt solutions 插件,在 VS 里写 Qt 项目的体验同样成熟。
提示:如果只做跨平台小项目,Ubuntu 下用
apt install qt5-default qtcreator装好环境,同一份代码直接切 kit 编译,Widgets 项目基本不需要改文件。
如果你之前是在 VS Code 里配置 C/C++ 环境写控制台程序,迁到 Qt 项目后会多一层 .pro 文件的概念。qmake 和 CMake 二选一,我默认用 qmake,因为 Qt 5.15 的官方示例和文档仍然以 qmake 为主,部署时用 Qt 自带的命令行工具windeployqt可以一键把依赖 DLL 收集到发布目录,比手动拷贝靠谱得多。
3. 地图与角色:让宝可梦在屏幕上“走起来”
3.1 地图绘制选型:QLabel 拼贴 vs QPainter 自绘
地图这块有个血泪经验:不要用几百个 QLabel 拼瓦片。第一版我图省事,每块草地放一个 QLabel,角色走一步就改一堆 label 的 pixmap,结果事件循环被大量控件拖慢,切换场景时明显掉帧。正确的常见做法是继承 QWidget 重写paintEvent,在事件里用 QPainter 一次性绘制可见区域的所有图块。
// MapSceneWidget.h #ifndef MAPSCENEWIDGET_H #define MAPSCENEWIDGET_H #include <QWidget> #include <QPixmap> #include <QKeyEvent> class MapSceneWidget : public QWidget { Q_OBJECT public: explicit MapSceneWidget(QWidget *parent = nullptr); protected: void paintEvent(QPaintEvent *event) override; void keyPressEvent(QKeyEvent *event) override; private: int playerRow = 1; // 角色所在行 int playerCol = 1; // 角色所在列 QPixmap grassTile; // 草地 QPixmap treeTile; // 树,不可通行 }; #endif // MAPSCENEWIDGET_HpaintEvent里做的就是纯绘图:先按地图宽高计算瓦片数,再双层循环画背景,最后把角色画在中心。这里所有图片都要提前用 QPixmap 加载好,不能在paintEvent里做文件 IO,否则每次重绘都会触发磁盘读取,卡顿是必然的。如果你只是画一条线或者几个矩形,QPainter 的drawLine、drawRect和画图块是同一套 API,Qt 桌面画线、绘图这块的知识点可以完全复用,不需要额外学渲染框架。
3.2 角色帧动画:QTimer 加图片序列切帧
角色行走动画最简单的实现是帧动画:一组走路图按固定间隔轮流显示。下面这个类核心成员就三样:帧列表、定时器和当前帧下标。用 QTimer 驱动,每到一个超时信号就切到下一帧并触发重绘。
// SpriteWidget.h #ifndef SPRITEWIDGET_H #define SPRITEWIDGET_H #include <QWidget> #include <QPixmap> #include <QTimer> class SpriteWidget : public QWidget { Q_OBJECT public: explicit SpriteWidget(QWidget *parent = nullptr); // 从文件路径列表加载帧,intervalMs 是每帧显示时长 void setFrames(const QStringList &paths, int intervalMs = 100); protected: void paintEvent(QPaintEvent *event) override; private slots: void nextFrame(); // 定时切换帧 private: QList<QPixmap> frames; // 加载好的帧,避免 paintEvent 做 IO QTimer animTimer; int frameIndex = 0; }; #endif // SPRITEWIDGET_H// SpriteWidget.cpp #include "SpriteWidget.h" #include <QPainter> SpriteWidget::SpriteWidget(QWidget *parent) : QWidget(parent) { animTimer.setInterval(100); // 默认 10 帧/秒 } void SpriteWidget::setFrames(const QStringList &paths, int intervalMs) { for (const QString &path : paths) { QPixmap px(path); if (px.isNull()) continue; // 路径不对就跳过,避免程序崩溃 frames.append(px); } if (!frames.isEmpty()) { frameIndex = 0; setFixedSize(frames.first().size()); animTimer.setInterval(intervalMs); animTimer.start(); connect(&animTimer, &QTimer::timeout, this, &SpriteWidget::nextFrame); } } void SpriteWidget::paintEvent(QPaintEvent *event) { QPainter painter(this); if (!frames.isEmpty()) { painter.drawPixmap(0, 0, frames.at(frameIndex)); } QWidget::paintEvent(event); } void SpriteWidget::nextFrame() { if (frames.isEmpty()) return; frameIndex = (frameIndex + 1) % frames.size(); update(); // 请求重绘,真正画图在 paintEvent 里做 }这里有两个关键点。第一,update()是异步合并的,连续调用多次只会触发一次paintEvent,所以不要在nextFrame里直接调repaint,那会强制同步刷新,动画反而更卡。第二,intervalMs取 100 毫秒是 10 帧每秒,对于像素风格的角色足够,想更平滑可以降到 80 毫秒,但低于 50 毫秒后肉眼不太分得出差别,反而加重 CPU 负担。帧动画的帧率并不是越高越好,宝可梦这种低清素材用 8~12 帧最合适。
3.3 键盘移动与碰撞:重写 keyPressEvent 加查表
角色移动我建议做成按格子走,每次按键固定移动一个瓦片,不做像素级平滑,这样碰撞检测就从“矩形相交”退化成一个查表操作,逻辑简单且稳定。地图用二维数组描述,0 表示可走,1 表示树、水这类障碍。
void MapSceneWidget::keyPressEvent(QKeyEvent *event) { const int step = 32; // 一个瓦片 32 像素 int dx = 0, dy = 0; switch (event->key()) { case Qt::Key_Up: dy = -step; break; case Qt::Key_Down: dy = step; break; case Qt::Key_Left: dx = -step; break; case Qt::Key_Right: dx = step; break; default: QWidget::keyPressEvent(event); return; } // 计算目标格子,越界直接忽略 int newRow = playerRow + dy / step; int newCol = playerCol + dx / step; if (newRow < 0 || newRow >= MAP_H) return; if (newCol < 0 || newCol >= MAP_W) return; // 查表:1 表示障碍物,不做像素级碰撞 if (mapData[newRow][newCol] == 1) return; playerRow = newRow; playerCol = newCol; update(); // 触发重绘,角色整体移动一个瓦片 }这段代码里dy / step是整除,方向键每次都产生 ±32 的位移,所以结果一定是 ±1,不用担心除出小数。要注意的是窗口必须设置焦点策略,否则按键事件不会到达MapSceneWidget。我在构造时调用setFocusPolicy(Qt::StrongFocus),它同时接受 Tab 和鼠标点击聚焦。
注意:连按方向键时,
keyPressEvent会连续触发,角色会快速连续走格,这正是回合制 RPG 想要的“走一步触发一次遇敌检测”的行为。如果你做的是动作游戏,反而要改成记录按下状态,在keyReleaseEvent里清除。这个区分想清楚,游戏手感就对了。
4. 回合制战斗系统:从数据结构到伤害计算
4.1 用 C++ 结构体把宝可梦描述成数据
宝可梦的核心不是画面,是数据。一只宝可梦就是一组结构体:名字、等级、六项能力、状态、最多四个招式。用结构体而不是 class,是我反复权衡后的选择,这个规模的数据只做读取和减法,写一堆 private 加 getter 是自我感动,反而是结构体让战斗回合计伤害的代码清晰很多。
// data.h #ifndef DATA_H #define DATA_H #include <QString> #include <QVector> // 招式 struct MoveInfo { QString name; // 招式名 int power = 0; // 威力,0 表示变化类招式 int accuracy = 100; // 命中率,0~100 int pp = 35; // 剩余使用次数 }; // 一只宝可梦 = 基础信息 + 能力值 + 招式列表 struct PokemonData { QString name; int level = 5; int maxHp = 20, hp = 20; int atk = 10, def = 10, spd = 10; // 攻击、防御、速度 QVector<MoveInfo> moves; // 最多 4 个 }; #endif // DATA_H为什么招式列表用QVector不用固定数组MoveInfo moves[4]?虽然宝可梦最多带四个招式,但后续做招式学习、遗忘界面时,动态数组的增删明显省事。Qt 的QVector在写入时是写时拷贝,复制一只宝可梦传到战斗逻辑里不会立刻深拷贝底层数据,性能完全没问题。C++ 结构体链表基本语法在这里也用得上:如果你要给队伍排出战顺序,std::sort按速度字段排一次就够了,不需要手写链表。
4.2 伤害公式:经典物理/特殊二分
战斗伤害公式我直接套经典回合制模板:先算整数基准值,最后乘一个 0.85 到 1.00 的随机浮动。这个公式的微妙之处在于中间的整除顺序,先乘后除和先除后乘结果完全不同,写的时候要按下面这段的括号顺序来。
// battle_math.cpp #include <QRandomGenerator> #include <QtMath> // 物理伤害简化版,不考虑属性克制和暴击 int calcPhysicalDamage(int level, int power, int atk, int def) { // 第一段:经典伤害公式的整数部分 int base = ((level * 2) / 5 + 2) * power * atk / def / 50 + 2; if (base <= 0) return 1; // 防御极高时至少打 1 点血 // 第二段:随机浮动 0.85 ~ 1.00 double randFactor = 0.85 + QRandomGenerator::global()->generateDouble() * 0.15; return static_cast<int>(base * randFactor); } // 命中判定:返回 true 表示命中 bool isHit(int accuracy) { int roll = QRandomGenerator::global()->bounded(100); // 0~99 return roll < accuracy; }level * 2 / 5这一串整数运算必须放在最前面,浮点数误差会在多次除法里被放大;随机因子放在最后乘,这样基准伤害可以单独做单元测试。QRandomGenerator::global()是 Qt 5.10 之后推荐的随机数入口,它内部自己管理种子,比qrand()和rand()都安全,多线程调用也不会互相干扰。显示伤害数字时,QString::number(damage, 'f', 2)可以把 double 转成保留两位小数的字符串,这个 Qt double 转字符串的接口在做战斗日志时很常用。
4.3 战斗 UI 导航和“打字机”文本
战斗界面无非三块:我方信息栏、对方信息栏、指令菜单。指令菜单用四个按钮或者一个 QListWidget 都能做,我更推荐 QListWidget,因为上下键选择天然支持,鼠标点击也兼容。难点在战斗文本的呈现——一行“皮卡丘使出了电光一闪”如果瞬间全部显示,玩家根本来不及看,逐字显示才有回合制味道。
// BattleTextPrinter.h #ifndef BATTLETEXTPRINTER_H #define BATTLETEXTPRINTER_H #include <QLabel> #include <QTimer> // 战斗文本打字机效果,每 30ms 打一个字 class BattleTextPrinter : public QLabel { Q_OBJECT public: explicit BattleTextPrinter(QWidget *parent = nullptr); // 开始打印一整段文本 void startPrint(const QString &text); private slots: void tick(); private: QTimer printTimer; QString fullText; int currentLength = 0; }; #endif // BATTLETEXTPRINTER_H// BattleTextPrinter.cpp #include "BattleTextPrinter.h" #include <QtGlobal> BattleTextPrinter::BattleTextPrinter(QWidget *parent) : QLabel(parent) { printTimer.setInterval(30); // 约 33 字/秒 } void BattleTextPrinter::startPrint(const QString &text) { fullText = text; currentLength = 0; printTimer.start(); } void BattleTextPrinter::tick() { currentLength = qMin(currentLength + 1, fullText.size()); setText(fullText.left(currentLength)); if (currentLength >= fullText.size()) { printTimer.stop(); // 显示完毕,停止定时器 } }注意tick里用qMin包了一下,防止定时器槽函数里出现下标越界;fullText.size()在 Qt 5 返回的是 UTF-16 编码的字符数,中文字符按一个算,不会出现半个字的乱码。这里的 30ms 间隔是我调过几次后的手感值,太短糊成一团,太长玩家会烦躁。如果你打算做玩家点击跳过文本,在startPrint里存一个标志位,鼠标点击时直接setText(fullText)并停掉定时器就够了。战斗结算阶段,逻辑一定要先算完整再逐条显示结果,而不是边算边显示,否则撤回操作时会有一堆文本残留。
提示:战斗逻辑和 UI 显示分离,是我在这个项目里最坚持的一条原则。伤害计算做成纯函数,UI 只负责把结果打出来。这样以后加新招式、新特性,只需要改数据结构,不用碰界面层。
5. 避坑与排查:编译报错、中文乱码和动画翻车现场
5.1 现象:-1: error: dependent '..\..\qt\5.15.2\msvc2019_64\include\qtwidgets'include 找不到
第一次用 Qt Creator 新建项目,.pro 文件里只写了QT += core,编译直接报找不到 qtwidgets 头文件。原因不是路径错了,而是 Qt 的模块机制:MSVC 套件下 include 路径由 qmake 根据 .pro 文件声明的 QT 模块推导,你没写QT += widgets,qmake 就不会把 QWidget 相关的头文件目录加进编译参数。解决很简单,.pro 里补上QT += widgets,然后执行 qmake 重新生成 Makefile。至于那一串..\..\..\的相对路径,是 Qt Creator 为影子构建生成的,不要去手动改。
5.2 现象:代码里全是中文,编译没报错,运行却是乱码
MSVC 编译器默认按操作系统的本地代码页读取源文件,中文 Windows 上是 GBK;如果你用 VS Code 或 Qt Creator 保存成 UTF-8 无 BOM,字符串字面量在编译期就被按 GBK 解析,运行结果自然是乱码。两个解决办法:第一,源文件统一存成 UTF-8 with BOM,MSVC 能自动识别 BOM;第二,在 .pro 里加一行msvc: QMAKE_CXXFLAGS += /utf-8,强制编译器按 UTF-8 处理源文件。我推荐第二种,因为团队成员用不同编辑器存 BOM 的几率太高了。另外字符串拼接别用+,统一QString::arg,否则中文字面量混合数字时最容易出幺蛾子。
5.3 现象:Debug 版本一进战斗就崩溃,Release 却正常
这类“崩和不崩看心情”的问题,九成是数组越界或迭代器失效。Debug 下 Qt 的容器访问带有断言检查,越界直接抛异常给你看;Release 关闭断言,越界读到的是旁边的垃圾数据,运气好程序不崩,但伤害值会变成负数或者宝可梦血量清零后还能继续战斗。解决方法是在战斗逻辑里对招式列表做边界检查。每只宝可梦最多四个招式,玩家在选招界面连续按方向键,下标很容易走到 4。我先是用qWarning打日志,后来直接改成if (index < 0 || index >= moves.size()) return;,问题从根上断掉。
5.4 现象:图片总是加载失败,日志显示file not found
加载图片用的是相对路径../../images/grass.png,在 Qt Creator 里能跑,打包后就找不到。原因是 Qt Creator 运行时的当前工作目录是构建目录,不是源码目录,相对路径在不同环境下的表现完全不一样。解决是用 Qt 资源系统,把图片加进 .qrc 文件,加载路径写成:/images/grass.png,这个路径只依赖资源文件内部结构,与部署目录无关。判断文件是否存在用QFile::exists,想拿文件大小、修改时间就用QFileInfo,这些 Qt 获取文件信息的接口在存档功能里也大量用到。另外注意 qrc 内路径大小写敏感,Windows 下平时不敏感,到了 Linux 打包就会炸。
5.5 现象:QTimer 驱动的动画忽快忽慢,切后台再回来跳帧
QTimer 默认是粗粒度定时器,误差在 5% 左右,平时看不出来,帧动画每秒切 10 次,误差累积后就明显卡顿。更糟的是窗口最小化时,定时器回调被事件循环挂起,恢复后定时器不会补帧,角色会瞬间跨过十几帧跳到动画末尾。处理办法分两层:动画本身改成QPropertyAnimation驱动,它按属性插值,不受定时器误差影响;非用 QTimer 不可的场景,调用animTimer.setTimerType(Qt::PreciseTimer)切换到精确模式,并在窗口hideEvent里暂停、showEvent里重启。这两招能解决 95% 的动画翻车现场。
6. 扩展与验证:存档、音效和可玩性打磨
6.1 用 QSaveFile 做原子存档
存档最大的风险是写一半崩溃,原存档文件损坏。直接拿QFile::write当然也能写,但不是原子操作。我推荐QSaveFile,它先写临时文件,全部写完再commit()一次性替换原文件,失败时原文件完好无损,这就是玩家的“后悔药”。
// save_game.cpp #include <QSaveFile> #include <QJsonDocument> #include <QJsonObject> #include <QJsonArray> bool writeSave(int playerRow, int playerCol, const QVector<PokemonData> &party) { QJsonObject root; root["playerRow"] = playerRow; root["playerCol"] = playerCol; QJsonArray team; for (const PokemonData &p : party) { QJsonObject obj; obj["name"] = p.name; obj["hp"] = p.hp; obj["maxHp"] = p.maxHp; team.append(obj); } root["team"] = team; QSaveFile file("save.json"); if (!file.open(QIODevice::WriteOnly)) { return false; // 打开失败多半是目录权限问题 } file.write(QJsonDocument(root).toJson()); return file.commit(); // 这里才真正替换原文件 }为什么不选 QSettings 或者 SQLite?JSON 的可读性最好,玩家可以直接打开存档文件改数值,宝可梦类游戏的社区文化里这是重要一环。commit()返回 false 时要保留旧文件,玩家能继续游戏而不是重新开档。
6.2 一套够用的验证方法
做完第一版后,我用的是最小冒烟测试:启动程序直接调switchTo(BattleScene),手动模拟一次攻击,观察血量变化是否符合预期。伤害计算这类纯函数单独写成 QTest 用例,验证calcPhysicalDamage(5, 40, 10, 10)的输出在 1 到 300 的区间内,顺便检查防御为 0 时会不会除零崩溃。地图碰撞没有自动化,让非程序员朋友走十分钟,记录他们按错键的位置,那些方向键连按导致越界的报错,比我自己测一小时发现得都快。我的教训是——第一版把战斗逻辑全塞进槽函数里,一放技能整个界面卡住,后来把所有计算拆成纯函数才收敛。按这套做完,你手里是一份能演示、能存档、能自由加新宝可梦的完整基础工程,加音效用QSoundEffect,加技能动画也是往里添砖。希望帮到你。
本文还有配套的精品资源,点击获取