☰
Qt宝可梦游戏源码解析:QGraphicsView渲染与回合制战斗实现
2026/10/7 20:53:35 网站建设 项目流程

简介:这是一份面向Qt/C++初学者的2D角色扮演游戏源码包,以《宝可梦》核心玩法为蓝本,适合想通过完整项目学习QGraphicsView图形渲染、碰撞检测、回合制战斗与TMX地图加载的开发者。资源共20个文件,压缩包约19KB,以9个cpp和8个h源代码文件为主体,另有TMX地图、工程文件和资源文件;代码按游戏世界、宝可梦、战斗、玩家四大系统组织,模块边界清晰。游戏世界系统负责2D俯视角地图与角色移动,宝可梦系统覆盖属性相克、技能和进化,战斗系统实现回合制技能选择与战斗动画,玩家系统包含角色管理与背包。已有103人学习浏览。可快速看清地图加载、角色移动、属性相克、进化与回合制战斗的实现流程,适合作为Qt课程设计或入门练手项目,后续可在同一框架下扩展新宝可梦、地图场景与剧情任务。

1. 这套 Qt 宝可梦源码:从“会写控件”到“能跑一个游戏”的中间件

在 QT C++ 入门这条路上,最尴尬的阶段不是语法不会,而是控件都会了、却不知道怎样把它们拼成一个完整的小游戏。这个宝可梦小游戏源码补的正是这段距离:它用 Qt 的 QGraphicsView 做渲染,实现了俯视角地图、角色移动与碰撞、回合制战斗、属性相克、技能与进化,核心逻辑全部落在 core 目录,界面层与逻辑层分开。它不是教学 demo,而是一套可以编译运行的 2D 回合制 RPG 工程。适合三类人:Qt 学完基础想找一个完整工程啃一遍的人,想摸清游戏状态机怎么转的人,课程设计选题带游戏方向的人。

2. 先看工程骨架:.pro、渲染管线与四个模块怎么分工

2.1 打开 .pro:先搞清哪些文件参与了编译

拿到这份源码的第一步,我不会急着点运行,而是先打开根目录的 PokemonGame.pro。qmake 工程里这个文件就是总装配图,SOURCES、HEADERS、FORMS、RESOURCES 四个变量决定了项目把哪些文件装进构建,缺一个源文件,后面就会报链接错误或者出现“类里有声明却找不到实现”这类翻车。

从文件结构看,这份工程的源文件分成三摊:根目录的 main.cpp、ui/ 下的 MainWindow 窗体,加上场景层 GameScene、BattleScene,核心逻辑集中在 core/ 里(MapLoader、PlayerCharacter、Pokemon、Skill、BattleManager)。按 qmake 的习惯,.pro 里应该这样组织:

SOURCES += \ main.cpp \ core/MapLoader.cpp \ core/PlayerCharacter.cpp \ core/Pokemon.cpp \ core/Skill.cpp \ core/BattleManager.cpp \ GameScene.cpp \ BattleScene.cpp \ MainWindow.cpp HEADERS += \ core/MapLoader.h \ core/PlayerCharacter.h \ core/Pokemon.h \ core/Skill.h \ core/BattleManager.h \ GameScene.h \ BattleScene.h \ MainWindow.h FORMS += ui/MainWindow.ui RESOURCES += resources.qrc

注意一点:这里我没把 GameWorld 展开,因为它从结构看更像世界上下文的承载者,具体是单例还是普通管理器,不同版本写法不同。你在 Creator 里打开工程后,左侧的项目树能看到实际文件分组,对照正文列出的文件名逐个核对,哪个文件没在 SOURCES 里,哪个就是隐患。我第一次接手这类 Qt 工程时吃过亏:头文件漏在 HEADERS 外,编译没问题,但 Qt Creator 的自动补全和“转到定义”全部失灵,查了一下午,最后把文件拖进 .pro 就好了。

这不是玄学,而是 qmake 依赖显式枚举文件。你加了文件、点了保存,qmake 才会把它纳入 Makefile;在 Qt Creator 里右键“添加现有文件”会自动帮你在 .pro 里加一行。如果是从压缩包直接解压的老工程,建议先执行一次“清理 → 执行 qmake → 重新构建”,让工程加载新路径。

2.2 四个系统在代码里的分工

摘要里把功能分成四个系统,文件上也能对齐。我给一份对应关系:

系统涉及文件职责
游戏世界系统MapLoader、GameScene、GameWorld2D 地图加载、场景渲染、玩家移动与碰撞
宝可梦系统Pokemon、Skill宝可梦属性、技能、进化数据
战斗系统BattleScene、BattleManager回合制战斗流程与结算
玩家系统PlayerCharacter、MainWindow 相关角色状态、背包入口

这份源码最值得学的是依赖方向:UI 层的 GameScene、BattleScene 负责接收输入和展示,core 里的 MapLoader、BattleManager 负责计算和流转。GameScene 不直接改玩家在地图上的像素坐标,而是先问 MapLoader 给的碰撞层“能不能走”,PlayerCharacter 拿到目标位置后再决定移动。战斗那边同理,BattleScene 只暴露技能列表让玩家点,真正算伤害、扣血量、判断胜负的是 BattleManager。

依赖方向一旦反掉,小游戏就会变成“改一处牵全身”的泥潭——我在别的项目见过把战斗结算写在按钮槽函数里的写法,后来加了防御指令,整个按钮回调重写了一遍。这套工程把计算逻辑下沉到 core,UI 层只是壳,后续加新技能、新宝可梦时不用碰场景代码。

2.3 渲染职责:QGraphicsView 这条管线解决了什么

做 Qt 2D 游戏,我见过三条路:QWidget 重写 paintEvent、QGraphicsView 体系、Qt Quick/QML。这套源码选 QGraphicsView/QGraphicsScene,是一个很务实的选择。QGraphicsScene 管理场景里的 item,每个 QGraphicsPixmapItem 自带坐标、z 值、碰撞区域,我们要做的就是把地图瓦片、角色精灵按坐标挂到 scene 上,然后通过继承 QGraphicsScene 来拦截键盘输入。

对比 QWidget 自绘,QGraphicsView 免去了脏矩形和重绘排序的麻烦,瓦片图片用 setPixmap 挂上去就行,这是 QWidget 路线里最痛的部分。对比 QML,QGraphicsView 保持了纯 C++,调试时直接断点进 C++ 代码,对刚读完 C++ 基础的人友好得多。代价是 item 数量多时性能会下降,但宝可梦这种俯视角场景,一屏瓦片也就几十个,完全够用。

工程里 GameScene 继承 QGraphicsScene,BattleScene 显然也走同一套渲染:战斗界面里的技能按钮、状态文本,其实也挂在场景或相应的 widget 上。读代码时先看 MainWindow 构造里 setCentralWidget 挂的是哪一个 view,这就找到了入口,剩下的就是沿着 scene 的输入回调往下追。

启动顺序值得单独说一句。打开 main.cpp 你会发现流程是标准 Qt 程序的写法:QApplication 初始化 → 创建 MainWindow → show() → exec()。MainWindow 的构造函数里会创建 GameScene 并设置到 QGraphicsView 上,地图数据通过 MapLoader 从 resources 里读 test_map.tmx 加载成瓦片,PlayerCharacter 被实例化后加入 Scene。这个顺序决定了你调试时的“可见性”:如果地图没出来,先看 MapLoader 返回的图层和瓦片 item 数量;如果角色没出来,看 PlayerCharacter 的 pixmap 是否设置了。按这个顺序走,能把“空白场景”这种问题缩小到一个函数。

3. 把 TMX 地图端进场景:MapLoader 的解析步骤与碰撞边界

3.1 TMX 格式里先看这两个字段

TMX 是 Tiled 地图编辑器的导出格式。data/maps/test_map.tmx 是个 XML 文件,但不要被大文件吓到。解析时只需要盯住四个属性:map 节点的 width、height、tilewidth、tileheight,它们决定了地图有多少瓦片、每格占多少像素。然后是 tileset 节点的 firstgid 和 image 路径,firstgid 是当前图块集合在第一块瓦片里的编号偏移,解析时如果不减掉它,画出来的图全会错位。

图层顺序同样关键。TMX 里一层 layer 对应场景的一张平面,常见做法是把可通行区域放底层、碰撞物件放中间、装饰放上层。MapLoader 解析时按 layer 自上而下遍历,把每个 gid 转成 pixmap 后按顺序挂到场景,后挂的 item 会盖在先挂的上面,这就是 2D 游戏“遮挡”的实现。

在一份简化的宝可梦工程里,图层可能简化成两个:一个底图 layer、一个 collision objectgroup 或者带自定义属性的碰撞瓦片。判断依据看 MapLoader.h 里是否维护了一个存储碰撞标记的二维数组,或者 QSet 存“不可通行 gid”。没有这个标记,后面的碰撞检测就没数据源。实际我要提醒你,TMX 里如果用的是 objectgroup,那里面存的不是瓦片 gid,而是一组 rectangle 对象,每个对象有 x、y、width、height 和自定义属性,解析时要单独开一段代码,把矩形区域换算成网格坐标,再写进碰撞层。

3.2 MapLoader 的加载流程与核心代码

MapLoader 的职责是“把 TMX 变成场景能用的瓦片 item”。我按常见实现把主干逻辑抽出来,这份工程的具体写法可能有细节差异,但骨架是一致的:

// MapLoader::loadMap 的核心流程 bool MapLoader::loadMap(const QString& path, QGraphicsScene* scene) { QFile file(path); if (!file.open(QIODevice::ReadOnly)) return false; QXmlStreamReader xml(&file); while (!xml.atEnd() && !xml.hasError()) { xml.readNext(); if (!xml.isStartElement()) continue; if (xml.name() == "map") { mapWidth = xml.attributes().value("width").toInt(); mapHeight = xml.attributes().value("height").toInt(); tileSize = xml.attributes().value("tilewidth").toInt(); } else if (xml.name() == "tileset") { firstGid = xml.attributes().value("firstgid").toInt(); tilesetImage = xml.attributes() .value("source").toString(); } else if (xml.name() == "layer") { parseLayer(xml, scene); // 内部再逐 tile 处理 } } return !xml.hasError(); }

逻辑说明:这段用 QXmlStreamReader 做流式解析,而不是 QDomDocument 一次性载入。TMX 动辄几百 KB,流式解析可以边读边建 item,内存占用更稳。firstGid 在这里被单独存下来,解析某个 tile 时,实际图片的本地编号是 gid - firstGid,这个偏移算错,整张地图都会错位。

参数说明:mapWidth/mapHeight 是网格数量,不是像素值。tileSize 来自 tilewidth,常见是 16 或 32,宝可梦素材多用 16 的倍数。tilesetImage 是图块集图片路径,在 TMX 里通常是相对路径,这个路径在运行时最容易出问题,后面避坑章单独讲。

接着把 gid 转成图片。这里常见的是从图块集大图里“切”出小图:

QPixmap tileset(tilesetImagePath); int columns = tileset.width() / tileSize; for (int gid : layer.gidList) { if (gid == 0) { continue; // gid 为 0 表示空格子 } int localId = gid - firstGid; int sx = (localId % columns) * tileSize; int sy = (localId / columns) * tileSize; QPixmap tile = tileset.copy(sx, sy, tileSize, tileSize); QGraphicsPixmapItem* item = new QGraphicsPixmapItem(tile); item->setPos(x * tileSize, y * tileSize); scene->addItem(item); }

参数说明:columns 是图块集横向一排放多少个瓦片,由整图宽度除以单个瓦片宽得到。copy 的参数是“源图里的起始坐标和切片尺寸”,不是缩放,pixmap 多大会在场景里显示多大。setPos 用瓦片坐标乘 tileSize,换算成场景像素坐标。这里的 x、y 是遍历 gidList 时的列与行索引。

切图是 MapLoader 里最容易出现视觉效果“错位一格”的地方。问题多半出在 firstGid 没减,或者 localId 取余时的列数算错。调试时不要看整图,先取一个已知瓦片断点,看 sx、sy、localId 三个值,就能定位是偏移问题还是列数问题。加载完地图后,我习惯加一句 qDebug() << scene->items().count() 验证瓦片 item 数量,数量对得上,MapLoader 就没白干。

3.3 碰撞检测:用碰撞层挡移动,少改坐标

碰撞不一定要做复杂的物理引擎。俯视角格子地图上,常见做法是在解析时维护一个 bool 二维数组,或者 QSet,记录哪些格子不可通行。移动时不是改角色坐标,而是先算出“如果走过去了,角色矩形会覆盖哪些格子”,再拿这些格子的可通行标记做判断:

bool PlayerCharacter::canMoveTo(const QPointF& target, const CollisionGrid& grid) { QRectF rect(target, QSizeF(width, height)); int x0 = int(rect.left()) / tileSize; int x1 = int(rect.right()) / tileSize; int y0 = int(rect.top()) / tileSize; int y1 = int(rect.bottom())/ tileSize; for (int row = y0; row <= y1; ++row) { for (int col = x0; col <= x1; ++col) { if (!grid.isWalkable(row, col)) return false; } } return true; }

逻辑说明:这段代码的核心是“目标矩形是否覆盖了不可通行瓦片”。先用目标的左上角坐标构造 QRectF,再除以 tileSize 求覆盖的网格范围。遍历这些网格,只要有一个不可通行,就拒绝这次移动。这样角色走到墙边是停在墙外一格,而不是半身进墙。

参数说明:target 是精灵移动后的左上角场景坐标;width、height 是角色精灵逻辑尺寸,宝可梦素材里角色可视区域通常比整张图小,很多工程用 32×32 的图、但逻辑碰撞只有 20×20,差值用来做视觉上的“描边”,这个尺寸要和地图瓦片匹配,否则会出现能走但看着悬空的情况。

移动代码里还要注意帧间隔和 speed 的单位。常见做法是 speed 以“像素/帧”为单位,每帧最多移动一个 tileSize,这样和碰撞网格对齐最省事。如果 speed 大于 tileSize,角色可能一帧跨过不可通行格,就会出现“瞬移穿墙”的错觉。我见过一个翻车案例:碰撞判断只检查了 target 的 left/top 这一个点,结果是角色往右走能压进墙里半个身子,往下走又完全进不去——因为角色宽高大于 0 后,一个点代表不了整体。后来改成矩形和网格求交,问题就消失了。这也解释了为什么 PlayerCharacter 的移动逻辑不应该直接操作像素坐标,而是先询问碰撞层再决定。

4. 回合制战斗怎么转起来:从 BattleManager 状态机到属性相克

4.1 BattleManager 与 BattleScene 的分工边界

战斗逻辑分为两个文件:BattleScene 负责展示和输入——技能列表、HP 变化、攻击动画;BattleManager 负责规则——谁先手、技能命中、伤害结算、胜负判断。这是这套源码里我认为最值得讲给你听的分层:场景只是“显示屏”,状态机才是“大脑”。

为什么要这样拆?因为回合制战斗天然是一个状态机:等待玩家指令、执行玩家回合、执行敌方回合、结算状态变化、判断是否结束。把状态机放在 BattleManager 里,BattleScene 只需要在玩家点击技能后调用 manager 的某个接口,然后根据返回结果刷新 UI。你后面想加“换宝可梦”“使用道具”,都只需要在状态机里加状态,UI 层加按钮,两者在预定的接口处对接。

我拿过一份把战斗流程全写在按钮槽函数里的代码,加一个“防御”指令时,六个槽函数的逻辑全乱掉。这种教训一次就够,之后接手战斗类功能,第一件事就是找状态机在哪个类里。GameScene 触发战斗则通常靠信号槽:玩家移动到草丛或踩到遇敌格子,GameScene 发信号,MainWindow 收到后隐藏地图场景、创建 BattleScene 和 BattleManager,战斗结束后反向切回,这套代码里大概率也是这个模式。

4.2 回合推进的状态机与结算顺序

战斗开始后,BattleManager 维护一个枚举状态,按回合循环推进。简化版的状态定义长这样:

enum class BattleState { PlayerSelect, // 等待玩家选择技能 PlayerAction, // 执行玩家技能 EnemyAction, // 执行敌方技能 RoundEnd, // 处理状态变化、判胜负 };

BattleManager 持有当前状态和一个指向双方宝可梦的指针。每次玩家确认技能后,流程是:

void BattleManager::onPlayerSkillSelected(Skill* skill) { if (state != BattleState::PlayerSelect) return; selectedSkill = skill; state = BattleState::PlayerAction; applyPlayerAttack(selectedSkill); // 计算伤害并扣血 if (enemyPoke->isFainted()) { state = BattleState::RoundEnd; endBattle(true); return; } state = BattleState::EnemyAction; applyEnemyAttack(); // 敌方回击 if (playerPoke->isFainted()) { state = BattleState::RoundEnd; endBattle(false); return; } state = BattleState::PlayerSelect; // 进入下一回合 }

逻辑说明:这个流程把“玩家出招 → 敌方出招 → 判胜负”的顺序固定下来。isFainted 是宝可梦血量归零的判定,两个回合各查一次,查完才回到等待状态。这里有个小细节:速度快的应该先手,真实宝可梦规则里按速度属性排序,这套源码如果没做先手处理,一般就是固定玩家先出招,按顺序执行。

参数说明:selectedSkill 在 PlayerSelect 状态下被写入,然后立刻转移状态,用来防止同一个技能被连点触发多次。endBattle 传入的 bool 代表胜利或失败,BattleScene 根据它决定弹出胜利信息还是回到地图。如果以后加“换人”指令,只需要在状态机里加一个 Switch 状态,结算顺序不变。技能释放后还要做 PP 扣减,ppLeft 减到 0 时技能不可用,这部分逻辑应该在 applyPlayerAttack 入口处做校验,不然会出现 PP 为负还能发招的 bug。

4.3 技能数据表与属性相克怎么落代码

技能系统是这套工程数据化最明显的部分。Skill.h 里一般定义一个结构体或类的字段:名称、属性类型、威力、PP。我习惯把它写成纯数据:

struct SkillData { QString name; int type; // 0 普通,1 火,2 水,3 电,4 草 int power; // 威力:0 表示变化类技能 int pp; // 技能次数上限 int ppLeft; // 剩余次数,战斗内递减 };

属性相克是战斗有趣的核心。真实宝可梦有 18 种属性,属性相克表是个 18×18 的倍率矩阵。这份工程为了控制篇幅,很可能只做了简化的五行或几行表,但从数据结构的写法上,扩展原理是一样的:一个二维数组,行是攻击方属性,列是防御方属性,值是倍率 0、0.5、1、2 等。

// 简化相克表:行攻击、列防御 // 普通 火 水 电 草 float typeChart[5][5] = { {1.0f, 1.0f, 1.0f, 1.0f, 1.0f}, // 普通 -> 各系 {1.0f, 0.5f, 0.5f, 1.0f, 2.0f}, // 火 -> 各系 {1.0f, 2.0f, 0.5f, 1.0f, 0.5f}, // 水 -> 各系 {1.0f, 1.0f, 2.0f, 0.5f, 0.5f}, // 电 -> 各系 {1.0f, 0.5f, 2.0f, 1.0f, 0.5f}, // 草 -> 各系 };

逻辑说明:typeChart[attack][defense] 取出来就是倍率。火打草是 2.0,水打火是 2.0,电打草是 0.5,这些数值直接决定伤害公式里最后乘多少。倍率表是单调数组,不好查错,调试时可以在战斗里故意选克制技能,断点看 typeRate 是不是 2.0。

伤害公式是典型的宝可梦第一世代简化版:

int calcDamage(Pokemon* atk, Pokemon* def, Skill* skill) { float typeRate = typeChart[skill->type][def->type]; float random = (85 + (qrand() % 16)) / 100.0f; int base = (2 * atk->getLevel() / 5 + 2) * skill->power * atk->getAttack() / def->getDefense() / 50; return int(base * typeRate * random + 2); }

参数说明:random 是 0.85~1.00 的随机浮动,让每回合伤害不固定;getLevel 是等级,等级的影响在 base 里;getAttack/getDefense 分别是攻防属性。公式里的魔法数字 50 和 2 是第一世代伤害公式的常量,看起来别扭,但改的时候要整套一起改,只动一个会让数值崩掉。想调难度,先调 level 和 power,这两个是线性影响,最容易感知。

如果你要添加新宝可梦,核心工作就是填充 Pokemon 的初始化数据:种族值、属性、可学技能列表。这些数据在 Pokemon.cpp 的构造函数里,而不是散落在战斗代码里——我把话说在前面:宝可梦数据如果散在战斗代码里,扩展一个图鉴就要改 8 个文件,别问我怎么知道的。

5. 编译和运行避坑:Qt 版本、路径、编码这五关怎么过

5.1 幽灵报错:dependent '......\qt\5.15.2\msvc2019_64\include\qtwid

现象:在 Qt Creator 打开工程后,构建输出第一行就是 error: dependent '............\qt\5.15.2\msvc2019_64\include\qtwidgets/...',整个工程标红,找不到任何头文件。

原因:Qt Creator 会生成一个 .pro.user 文件,记录上次用的 Qt 版本和编译器路径。这份工程如果是别人机器上打包的,.pro.user 里写的是他的 Qt 安装目录,到你机器上路径当然不存在;或者是本机 Qt 版本升级后旧路径残留。

解决:关掉 Qt Creator,进工程目录把 .pro.user 和 shadow build 目录(build-PokemonGame-xxx)一起删掉,重新打开 .pro,选择你自己的套件,比如 MSVC2019 64bit 配 Qt 5.15.2。重新执行 qmake 后,.pro.user 会按你的环境重新生成。如果系统里的 Qt 是 5.12,把 .pro 里的版本写法(如果有)改成 5.12 能识别的模块即可。这套源码用到的模块不多,基本不会出现“低版本跑不了”的情况。

5.2 地图空白或瓦片灰块:TMX 图片路径丢了

现象:程序能启动,角色也能走,但地图区域一片空白,或者瓦片全是灰块。

原因:Tiled 保存的 TMX 里,图块集图片用的是相对路径 image source=“../tilesets/xxxx.png”。程序运行时的工作目录是构建目录,不是 data/maps 目录,相对路径找不到图。另一个常见场景是图片路径里用了反斜杠,在 Windows 上没问题,换到 Linux 就炸。

解决:首选把图集图片放进 resources.qrc,并从 qrc 加载 TMX 和图片,比如路径写 :/data/maps/test_map.tmx,图集路径也在 qrc 里。MapLoader 在解析 TMX 时,看到相对路径要做一个“前缀修正”,把相对路径重写成 qrc 路径或绝对路径。其次,把 TMX 和图片放在同一个平级目录,让相对路径不跨级。

我在复现别的地图工程时踩过这个坑:换了地图图片到 resources 里,但 MapLoader 里用了 QFile("data/maps/..."),本地能跑,打包后资源找不到。后来统一改成读取 qrc 路径,问题一次解决。

5.3 中文乱码:宝可梦名字和技能名显示成乱码

现象:源文件里明明是“妙蛙种子”,运行出来是一堆乱码,或者编译警告 C4819。

原因:MSVC 编译器默认按本地代码页(GBK)解析源码,而 Qt 工程源码多数是 UTF-8 保存,两者对不上。字面字符串被转换层曲解,显示自然错乱。

解决:在包含中文字符串的源文件顶部加:

#if defined(_MSC_VER) #pragma execution_character_set("utf-8") #endif

并且把字符串统一用 QStringLiteral 包裹,比如 QStringLiteral("妙蛙种子"),不要写 const char* name = "妙蛙种子"。文件另存为 UTF-8 编码,Qt Creator 右下角能看编码格式。这样处理后,MSVC 会把 UTF-8 字面量直接转成 UTF-16,显示正常。这个问题只影响 MSVC 套件,MinGW 一般遇不到。

5.4 按键穿墙或移动两格:碰撞判定和 keyPress 的问题

现象:角色朝墙走,能压进墙里一格再被弹回,或者按一次方向键连走两格。

原因:穿墙大概率是碰撞判断只取了精灵左上角一个点,没有用角色矩形扫网格;连走两格则多半是 keyPressEvent 里直接移动,而 Qt 会在按键按住时连续触发 keyPress,一帧处理了两次位移。这类问题不属于源码逻辑损坏,而是实现细节粗糙。

解决:碰撞统一改成 3.3 节的矩形遍历判断;移动在移动结束后用一个 bool 标志锁住,直到位移完成或收到 KeyRelease 再解锁。把位移增量改成按时间步进,而不是按事件次数步进——每帧最多走一个瓦片,速度可控,也方便和碰撞检测对齐。

我通常会在 PlayerCharacter 里加一个 isMoving 标志,keyPress 里先判断这个标志,false 才发起移动;move 完成动画后置回 false。这样再快的连按也只会一个栅格一个栅格地走。

5.5 ui_xxx.h 重复编译或找不到

现象:报错找不到 ui_MainWindow.h,或者工程里明明有 ui_mainwindow.h,但提示多重定义。

原因:Qt 的 ui_MainWindow.h 是由 uic 工具在构建目录里自动生成的,不需要放进源码或者 .pro,也不需要自己新建。有的同学把自动生成的 ui_xxx.h 复制到源码目录,然后手工 include,结果系统自动生成了一份、手放了一份,两份同时参与编译,链接阶段必然冲突。

解决:MainWindow.h 里 include <ui_MainWindow.h>,不要写相对路径“ui/ui_mainwindow.h”;FORMS 变量里确认有 FORMS += ui/MainWindow.ui;把工程里所有手放的 ui_xxx.h 删除,再去构建目录确认自动生成版本存在。构建目录里 uic 会自动挑出表单文件,不需要你干预。

6. 验证与扩展:先跑通流程,再换皮成自己的游戏

6.1 最小验证清单:手测这套流程

拿到这份源码,我建议按下面的顺序验证,不要一上来就改代码。验证顺序也是读代码顺序:

步骤操作现象
1启动程序MainWindow 出现,地图可见
2WASD/方向键移动角色按格移动,墙体处被挡住
3走向草丛或与 NPC 交互切换到 BattleScene
4选择技能敌方 HP 变化,回合推进
5战斗结束回到地图,状态保留

哪一步断了,就从最后一步的代码反向追。地图空白查 MapLoader;战斗卡住查 BattleManager 的 state 是否在 PlayerSelect;角色穿墙查 3.3 的矩形检测。上述五项全过,说明工程在你本机完整落地了,可以开始动手改。

6.2 按“素材 → 数据 → 规则”的顺序换皮

改这套源码,我建议严格按三个层次推进,一次只动一层。第一层换素材:把 resources.qrc 里的角色精灵图、瓦片图集、技能特效图换成自己的图,注意保持尺寸一致,地图用了 32×32 的瓦片,你的图也必须是 32 的倍数,否则 setPos 的换算全乱。第二层改数据:在 Skill.cpp 的初始化里加技能,在 Pokemon.cpp 里加新宝可梦属性,在 typeChart 里加属性行,这些都是常量数组的增删,不碰流程代码。第三层改规则:在 BattleManager 的状态机里加状态,在伤害公式里调数值,这层才动逻辑。

我把第一层换素材当作最好的练手:替换一张瓦片、运行看地图变化、再登录下一张,每一步都能肉眼验证,而且不会碰坏战斗系统。等素材层稳定了再深入数据层,循序渐进。

最后说个我的习惯:拿到任何一份 Qt 工程,我先跑一遍最小验证,再按上述三层顺序改,每次只改一层、编译一次、验证一次。这份宝可梦源码我是按同样的流程拆的,改完技能表后跑战斗,属性相克数值不对,断点查 typeChart 才发现是行序和枚举对不上——从那以后我改属性表前,先查枚举定义顺序再看数组,省了不少回头路。希望帮到你。

本文还有配套的精品资源,点击获取

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询