Qt宝可梦游戏源码解析:QGraphicsView渲染与回合制战斗实现

📅 发布时间:2026/10/7 20:54:04
Qt宝可梦游戏源码解析:QGraphicsView渲染与回合制战斗实现
简介这是一份面向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。依赖方向一旦反掉小游戏就会变成“改一处牵全身”的泥潭——我在别的项目见过把战斗结算写在按钮槽函数里的写法后来加了防御指令整个按钮回调重写了一遍。这套工程把计算逻辑下沉到 coreUI 层只是壳后续加新技能、新宝可梦时不用碰场景代码。2.3 渲染职责QGraphicsView 这条管线解决了什么做 Qt 2D 游戏我见过三条路QWidget 重写 paintEvent、QGraphicsView 体系、Qt Quick/QML。这套源码选 QGraphicsView/QGraphicsScene是一个很务实的选择。QGraphicsScene 管理场景里的 item每个 QGraphicsPixmapItem 自带坐标、z 值、碰撞区域我们要做的就是把地图瓦片、角色精灵按坐标挂到 scene 上然后通过继承 QGraphicsScene 来拦截键盘输入。对比 QWidget 自绘QGraphicsView 免去了脏矩形和重绘排序的麻烦瓦片图片用 setPixmap 挂上去就行这是 QWidget 路线里最痛的部分。对比 QMLQGraphicsView 保持了纯 C调试时直接断点进 C 代码对刚读完 C 基础的人友好得多。代价是 item 数量多时性能会下降但宝可梦这种俯视角场景一屏瓦片也就几十个完全够用。工程里 GameScene 继承 QGraphicsSceneBattleScene 显然也走同一套渲染战斗界面里的技能按钮、状态文本其实也挂在场景或相应的 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.851.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 交互切换到 BattleScene4选择技能敌方 HP 变化回合推进5战斗结束回到地图状态保留哪一步断了就从最后一步的代码反向追。地图空白查 MapLoader战斗卡住查 BattleManager 的 state 是否在 PlayerSelect角色穿墙查 3.3 的矩形检测。上述五项全过说明工程在你本机完整落地了可以开始动手改。6.2 按“素材 → 数据 → 规则”的顺序换皮改这套源码我建议严格按三个层次推进一次只动一层。第一层换素材把 resources.qrc 里的角色精灵图、瓦片图集、技能特效图换成自己的图注意保持尺寸一致地图用了 32×32 的瓦片你的图也必须是 32 的倍数否则 setPos 的换算全乱。第二层改数据在 Skill.cpp 的初始化里加技能在 Pokemon.cpp 里加新宝可梦属性在 typeChart 里加属性行这些都是常量数组的增删不碰流程代码。第三层改规则在 BattleManager 的状态机里加状态在伤害公式里调数值这层才动逻辑。我把第一层换素材当作最好的练手替换一张瓦片、运行看地图变化、再登录下一张每一步都能肉眼验证而且不会碰坏战斗系统。等素材层稳定了再深入数据层循序渐进。最后说个我的习惯拿到任何一份 Qt 工程我先跑一遍最小验证再按上述三层顺序改每次只改一层、编译一次、验证一次。这份宝可梦源码我是按同样的流程拆的改完技能表后跑战斗属性相克数值不对断点查 typeChart 才发现是行序和枚举对不上——从那以后我改属性表前先查枚举定义顺序再看数组省了不少回头路。希望帮到你。本文还有配套的精品资源点击获取