
简介基于Qt框架、以C实现的RPG风格连连看桌面游戏源码将经典连连看玩法与角色成长、经验值等RPG元素融合适合作为Qt入门学习或课程设计的参考项目。资源共26个文件约1.99MB包含5个.cpp与4个.h源码文件、3个.ui界面文件、8张jpg图片资源、Qt资源文件.qrc及项目配置.pro等另附一份PDF说明文档结构清晰便于对照学习。已有372人学习/下载。通过阅读代码与界面设计可以掌握QGraphicsView绘图、信号槽交互、QTimer计时、资源系统加载等Qt常用技术理解如何将简单游戏逻辑与UI结合并了解一个完成度较高的课堂作业项目从搭建到实现的过程。1. 用Qt制作的RPG形式连连看一个RAR包里装了什么一个 .rar 解压出来没有安装向导只有 exe 和一堆 dll双击后先弹出一段剧情对话然后进入一个布满图标的棋盘消除一对图标角色血条跟着变化——这就是用 Qt 做的 RPG 形式连连看最直观的样子。它把“连连看”从休闲小游戏变成战斗系统棋盘即战场消除即指令剧情和数值成长负责给玩家继续点下去的正反馈。对想练 Qt 的开发者来说这个项目把一个完整 App 必需的零件全凑齐了窗口与绘图、事件处理、状态机、资源管理、发布打包每一步都能在代码里看到对应实现不是那种一行 main 函数就结束的玩具工程。适合三类人刚学完 C 和 Qt 基础、想拿一个完整项目当作品的人想了解 QGraphicsView 框架实际怎么用的人以及想自己做个带剧情的小游戏练手的人。2. 把RPG和连连看缝合起来玩法拆解与Qt模块选型很多人一看到“RPG形式”第一反应是外面套一层对话窗口里面还是传统连连看。这个理解太浅。真正做起来玩法、数据结构、状态流转都要为 RPG 目标改掉否则做出来就是个换了皮肤的连连看玩家不会觉得在“扮演角色”。2.1 RPG形式的两个关键设计消除即战斗剧情即关卡传统连连看的目标是清空棋盘RPG 化之后必须让每一次消除都有数值含义。我做这套项目时的约定是一次有效消除等于角色发动一次攻击攻击力等于当前角色的 attack 属性连续消除会累积 combocombo 每加一层多给 10% 伤害如果一段时间内没有消除怪物会发动攻击扣掉角色 HP。这样玩家不再无脑找对角线而是会规划先消哪一对、能不能凑出连续消除来抢伤害。另一层设计是“剧情即关卡”。一张地图不是一个棋盘而是一个关卡开场用角色对话把战斗目标讲清楚比如“前方的史莱姆堵住了矿洞入口”关卡不设无限时而是给一个回合数或倒计时倒计时结束怪物攻击力翻倍逼着玩家在压力下做选择。胜利后更新一段剧情文本再进入下一张地图。这种设计让连连看变成战斗系统的一部分RPG 元素才立得住。2.2 用QGraphicsView还是Qt Quick/QML老手换皮与从零新写实现层面第一个选型问题是用 QWidget 体系的 QGraphicsView还是用 Qt Quick/QML。这里我推荐 QGraphicsView尤其当你是拿这个项目练手时。QGraphicsView/QGraphicsScene 是 QWidget 家族的组件C 代码直接写事件、自绘、动画逻辑透明断点调试方便。连连看棋盘本质是一堆图元itemQGraphicsPixmapItem 天然对应棋子点击、移动、碰撞这些交互在 scene 里都有现成事件。Qt Quick/QML 写对话框和过渡动画确实更顺但它需要 qml 目录、Qt Quick 插件等一堆运行时依赖发布包比 Widgets 复杂C 与 QML 混编还要维护两套上下文对一个中小型桌面游戏来说有点杀鸡用牛刀。如果目标是以后的移动端可以先用 QGraphicsView 把核心算法做稳之后再用 QML 重写界面层算法部分封装成 C 类直接复用。版本上Qt 5.15.2 是很多人还在用的 LTS 版本教程和踩坑记录最全Qt 6 也能编译这套代码但新手建议先锁 Qt 5.15.2避免一边写功能一边适配 API 变动。2.3 棋盘、状态与配置用结构体先把数据模型立住动手写界面之前先把数据模型定义好。以下是这套项目最核心的几个数据结构// 棋子类型与资源图片一一对应 enum class TileType : int { None 0, Fire, Water, Grass, Metal, Wood, Count }; // 棋盘每个格子类型 图元指针 动画锁 struct BoardCell { TileType type TileType::None; QGraphicsPixmapItem* item nullptr; // 当前占位图元 bool busy false; // 动画中禁止点击 }; // 角色状态RPG数值成长的核心 struct RoleState { int hp 100; int maxHp 100; int mp 50; int attack 12; int level 1; int exp 0; int nextLevelExp 50; }; // 关卡配置从JSON或代码内定义加载 struct LevelConfig { int rows 8; // 玩法区行数 int cols 10; // 玩法区列数 int timeLimit 30; // 关卡秒数0为不限 int maxCombo 6; // 叠加到6层后触发技能 QString enemyName; int enemyHp 120; };参数说明TileType::Count 作用是给随机生成棋子提供上界用 rand() % Count 只会得到 Fire 到 WoodNone 保留给空格。BoardCell 里的 item 指针和 type 必须永远同步——改 type 的同时改 item这是整个项目最容易出错的地方。busy 标志防止动画期间再次被点击。RoleState 先硬编码后面再做配置化LevelConfig 可以用 JSON 或 QSettings 存第一版建议直接写在代码里等玩法稳定再抽出去。棋盘在类里的建议声明是一个二维数组比如 std::vectorstd::vector m_cells。注意这个数组要比玩法区大一圈下标从 1 开始外围一圈全部是 None 空格作为连连看路径绕行的通路第 3 章讲算法时会看到它的作用。3. 从空工程到可玩Demo用Qt Creator把棋盘、消除与剧情跑通这一章是整篇的核心。我会按“建工程 → 搭棋盘 → 连通判定 → 消除与掉落 → RPG状态机”的顺序给出一套最小可运行实现。你不需要一次抄完但建议按顺序在 Qt Creator 里跟一遍。3.1 建工程和.pro模块别让Qt模块声明成为第一个坑在 Qt Creator 里新建 Qt Widgets Application类名可以叫 RpgLink基类选 QMainWindow。新建时选 Qt 5.15.2 的套件注意 MinGW 64-bit 和 MSVC 要区分开新手强烈建议用 MinGW 一套部署少一道 VC 运行库的坑。项目的 .pro 文件建议这样写QT core gui widgets greaterThan(QT_MAJOR_VERSION, 4): QT widgets TARGET RpgLink TEMPLATE app CONFIG c11 DEFINES QT_DEPRECATED_WARNINGS SOURCES \ main.cpp \ MainWindow.cpp \ BoardScene.cpp \ Board.cpp \ RpgLogic.cpp HEADERS \ MainWindow.h \ BoardScene.h \ Board.h \ RpgLogic.h RESOURCES resources.qrc逻辑说明QT widgets 在 Qt 5 里必须要写否则 new QApplication 都会编译失败。RESOURCES 指向资源文件棋子的 PNG 图标放在 resources.qrc 里统一管理这样发布时不需要额外携带图片目录。Qt Designer 可以用来布置主窗口上的血条和对话按钮但棋盘区域不要尝试用 Designer 拖拽它属于 QGraphicsScene必须用代码构建。3.2 用QGraphicsScene搭棋盘格子、棋子与点击处理QGraphicsView 放在主窗口中央棋盘 scene 的尺寸由棋子大小决定。这里约定棋格 48×48四周留 4 像素边框。注意棋盘数组下标从 1 开始外围 0 和 rows1 是空通路但视觉上这些空位不画棋子只参与路径计算。// board_scene.cpp const int kCellSize 48; const int kBorder 4; const int kRows 8; const int kCols 10; BoardScene::BoardScene(QObject* parent) : QGraphicsScene(parent) { int width (kCols 2) * kCellSize 2 * kBorder; int height (kRows 2) * kCellSize 2 * kBorder; setSceneRect(0, 0, width, height); } void BoardScene::placeTile(int row, int col, TileType type) { // row, col 是数组下标范围 1..kRows / 1..kCols QPixmap pix loadPixmap(type); // type - 棋子图片 QGraphicsPixmapItem* item addPixmap(pix); item-setData(kRowRole, row); item-setData(kColRole, col); item-setPos(QPointF(kBorder (col - 1) * kCellSize, kBorder (row - 1) * kCellSize)); item-setAcceptedMouseButtons(Qt::LeftButton); item-setCursor(Qt::PointingHandCursor); m_cells[row][col].item item; m_cells[row][col].type type; }逻辑说明setData 把数组下标存进图元点击时直接取不用遍历查找。setAcceptedMouseButtons(Qt::LeftButton) 是必须的QGraphicsItem 默认不接收任何鼠标事件漏掉这行会出现“棋子看得见但点不中”的怪问题。setPos 的算法里col-1 和 row-1 是把数组下标映射到视觉坐标因为外围留了一圈空通路。点击选择逻辑写在 BoardScene::mousePressEvent 里void BoardScene::mousePressEvent(QGraphicsSceneMouseEvent* event) { if (m_animating) return; // 动画中不允许连点 QGraphicsItem* clicked itemAt(event-scenePos(), QTransform()); if (!clicked) return; int row clicked-data(kRowRole).toInt(); int col clicked-data(kColRole).toInt(); if (m_cells[row][col].type TileType::None) return; if (!m_firstItem) { m_firstItem clicked; highlight(clicked, true); return; } int r1 m_firstItem-data(kRowRole).toInt(); int c1 m_firstItem-data(kColRole).toInt(); int r2 row, c2 col; if (r1 r2 c1 c2) { highlight(m_firstItem, false); m_firstItem nullptr; return; } if (m_board-canConnect(r1, c1, r2, c2)) { m_animating true; emit pairMatched(r1, c1, r2, c2); } else { highlight(m_firstItem, false); m_firstItem clicked; highlight(clicked, true); } QGraphicsScene::mousePressEvent(event); }这里的核心是 m_animating 锁。玩家手速再快也不能让第二次点击打断第一次消除动画否则数据会乱。highlight 可以给图元加一个不同颜色的边框提示选中的是哪个棋子。3.3 连连看连通判定直线、一个拐角、两个拐角的实现连连看规则是两个相同棋子之间最多经过两个拐角的路径且路径上的格子都必须是空格。因为棋盘数组下标从 1 开始外层多出来的 0 和 rows1 行/列可以让路径沿着棋盘外沿绕过障碍。先写直线检测它是所有判断的基础// board.cpp bool Board::straightLine(int r1, int c1, int r2, int c2) { if (r1 r2) { int step (c2 c1) ? 1 : -1; for (int c c1 step; c ! c2; c step) if (m_cells[r1][c].type ! TileType::None) return false; return true; } if (c1 c2) { int step (r2 r1) ? 1 : -1; for (int r r1 step; r ! r2; r step) if (m_cells[r][c1].type ! TileType::None) return false; return true; } return false; }参数说明这个函数检查两个端点之间的格子是否全部为空不含两个端点本身。循环用 step 正负统一处理向上和向下方向比较简洁。看了这个再写完整判定bool Board::canConnect(int r1, int c1, int r2, int c2) { if (r1 r2 c1 c2) return false; if (m_cells[r1][c1].type ! m_cells[r2][c2].type) return false; if (m_cells[r1][c1].type TileType::None) return false; // 0 个拐角直线 if (straightLine(r1, c1, r2, c2)) return true; // 1 个拐角拐点在 (r1, c2) 或 (r2, c1) if (m_cells[r1][c2].type TileType::None) { if (straightLine(r1, c1, r1, c2) straightLine(r1, c2, r2, c2)) return true; } if (m_cells[r2][c1].type TileType::None) { if (straightLine(r1, c1, r2, c1) straightLine(r2, c1, r2, c2)) return true; } // 2 个拐角先在同一列找中间行再在同一行找中间列 for (int r 0; r kRows 1; r) { if (r r1 || r r2) continue; if (m_cells[r][c1].type TileType::None m_cells[r][c2].type TileType::None) { if (straightLine(r1, c1, r, c1) straightLine(r, c1, r, c2) straightLine(r, c2, r2, c2)) return true; } } for (int c 0; c kCols 1; c) { if (c c1 || c c2) continue; if (m_cells[r1][c].type TileType::None m_cells[r2][c].type TileType::None) { if (straightLine(r1, c1, r1, c) straightLine(r1, c, r2, c) straightLine(r2, c, r2, c2)) return true; } } return false; }中间这段两个拐角的循环效率不高但视觉上很容易理解固定一条边让路径从第一段直线走到中间过渡点再走一段水平或垂直到目标附近最后进目标。8×10 的棋盘只有 80 个格子O(n) 遍历完全够用。缺少外围空边界的话靠边的棋子绕不过去很多开局明明能连却判定失败这就是“看起来能消却连不了”的常见原因。3.4 消除、下落与RPG状态机让战斗节奏转起来连通成功后先把两个棋子从 scene 移除再把数组清空然后触发下落补位。这一步最容易出的问题就是“先改数据再放动画”或者反过来导致视觉和图元数据对不上。void Board::removePair(int r1, int c1, int r2, int c2) { removeItemAt(r1, c1); removeItemAt(r2, c2); // 伤害飘字由 UI 层处理 emit attackEnemy(m_role.attack); // 延迟 300ms 后执行重力下落让消除动画先播完 QTimer::singleShot(300, this, [this]() { for (int col 1; col kCols; col) applyGravity(col); }); }下落采用“从底向上压缩”的思路比逐格冒泡更稳定void Board::applyGravity(int col) { int target kRows; for (int row kRows; row 1; --row) { if (m_cells[row][col].type TileType::None) continue; if (target ! row) { m_cells[target][col] m_cells[row][col]; m_cells[row][col] BoardCell{}; animateMove(m_cells[target][col].item, target, col); } --target; } // 顶部的空格补新棋子这里省略生成动画 for (int row target; row 1; --row) { TileType t randomTile(); placeTile(row, col, t); } }animateMove 是动画与数据同步的关键void Board::animateMove(QGraphicsPixmapItem* item, int row, int col) { item-setData(kRowRole, row); item-setData(kColRole, col); QPropertyAnimation* anim new QPropertyAnimation(item, pos, this); anim-setDuration(180); anim-setStartValue(item-pos()); anim-setEndValue(QPointF(kBorder (col - 1) * kCellSize, kBorder (row - 1) * kCellSize)); anim-start(QAbstractAnimation::DeleteWhenStopped); }注意先搬数据、后移动图元并且移动的过程中 item 的 row/col 数据已经更新这样动画期间如果发生位置点击也不会读到旧的坐标。RPG 状态机用枚举加延迟回调串联enum class GameState { Selecting, Animating, EnemyTurn, Victory, GameOver }; void Board::onPairMatched(int r1, int c1, int r2, int c2) { m_state GameState::Animating; removePair(r1, c1, r2, c2); m_role.exp 5; if (m_role.exp m_role.nextLevelExp) levelUp(); m_state GameState::EnemyTurn; QTimer::singleShot(600, this, Board::enemyAttack); }这样消除结束后会自动进入怪物回合怪物攻击再交回给玩家选择RPG 的战斗循环就跑起来了。延迟时间 600ms 是给玩家看伤害数字和掉落动画的缓冲太短会显得突然太长影响节奏这个值可以放到配置里让策划慢慢调。4. 把工程发布成.rarQt依赖收集、插件目录和首次运行验证代码写完不是终点Qt 程序要发出去依赖收集是最容易翻车的部分。很多人本地跑得好好的拷给别人就报缺少 Qt5Widgets.dll 或者平台插件加载失败。4.1 release构建与windeployqt别在debug目录里做白工发布前先切到 release 模式。debug 模式下 Qt 会链接一堆 debug 版 DLL体积大且目标机器大概率没有对应调试运行库。在 Qt Creator 左下角选择 Release重新构建。构建完成后用 Qt 安装目录自带的命令行工具来收集依赖cd /d D:\build\RpgLink-Desktop_Qt_5_15_2-MinGW-64bit\release C:\Qt\Qt5.15.2\5.15.2\mingw81_64\bin\windeployqt.exe RpgLink.exe命令说明windeployqt 会扫描 exe 导入了哪些 Qt DLL把它们复制到 exe 目录同时生成 platforms、styles、imageformats 等插件目录。关键是必须用与编译套件相同的 windeployqt如果你用 MinGW 编的却拿 MSVC 版工具跑不会报错但会混入两个版本的 Qt5Core.dll运行时直接崩溃。执行输出里看到 Adding Qt5Widgets、Adding Qt5Gui 这类信息就说明扫描成功。4.2 platforms、styles、imageformats这些隐藏依赖少了哪个都会当场翻车复制完 DLL 后在 exe 目录里执行 RpgLink.exe 一般能跑。但换到没有 Qt 的机器上经常是这样几个问题文件或目录作用缺失现象platforms/qwindows.dll窗口平台插件启动即报 could not find the Qt platform plugin windowsimageformats/qjpeg.dll、qgif.dll图片格式解码贴图不显示或全黑styles/qwindowsvistastyle.dllWindows 界面样式控件回到老式方框外观iconengines/qsvgicon.dllSVG 图标支持资源里的 svg 图标打不开最容易忽略的是 platforms 目录。Qt 的窗口系统插件在运行时通过 QPA 机制查找找不到就会在启动阶段退出。windeployqt 会自动生成 platforms关键是不要手动去重或精简目录如果发布包是自己手挑 DLL请务必保留整个 plugins 目录而不要只拿着几个 DLL。如果代码里用了 QFontDatabase 加载外部字体release 目录里还要带上字体文件并在启动时 addApplicationFont否则所有文字会退化成系统默认字体界面比例直接崩坏。4.3 用RAR压缩与分发结构目录长什么样才算一个能发出去的包依赖收集完成后的最终目录应该是这种扁平结构RpgLink/ ├── RpgLink.exe ├── Qt5Core.dll ├── Qt5Gui.dll ├── Qt5Widgets.dll ├── platforms/ │ └── qwindows.dll ├── styles/ │ └── qwindowsvistastyle.dll ├── imageformats/ │ ├── qjpeg.dll │ └── qgif.dll └── config/ └── levels.json用 WinRAR 压缩前先清理调试日志、临时文件、构建中间产物。压缩时建议让包内顶层就是一个 RpgLink 目录而不是把 bin 和 release 文件夹一起塞进去用户解压后一眼能找到 exe。config 目录保留是因为关卡配置我会做成外部 JSON方便不重新编译就调整数值。如果不需要外置配置可以把 JSON 当作资源编进 exe发布目录更简洁。发布后找一台干净虚拟机做“首次运行验证”解压、双击、打一关、存档、退出。这一套动作过了这个 .rar 才算真正能发出去。5. Qt开发避坑RPG连连看最容易翻车的5处真实问题以下每一条都是自己做这个项目时踩过的坑按“现象 → 原因 → 解决”的格式写方便你直接对照。5.1 点击棋盘没反应QGraphicsItem默认不接收鼠标事件现象棋盘上的棋子显示正常但鼠标点击没有选中高亮光标移上去也不是手型像一层玻璃挡在棋盘上面。原因QGraphicsItem 默认的 acceptedMouseButtons 是空集。也就是说图元不主动接收鼠标左键事件事件会被 scene 忽略自然不会触发 mousePressEvent。初学 QGraphicsView 时很容易只记得 addPixmap忘了设置 item 的接收状态。解决在 placeTile 里给每个棋子加两行item-setAcceptedMouseButtons(Qt::LeftButton); item-setCursor(Qt::PointingHandCursor);如果棋子是自定义 QGraphicsItem 子类还要保证 boundingRect 返回值不小于实际绘制区域否则点击命中测试会漏掉边缘。5.2 0x0000005闪退图元删除时机和悬空指针现象连续消除几对后程序直接闪退Windows 事件查看器里是 0x0000005 访问违规Qt Creator 调试时断在 item-pos() 或者 item-setPos() 调用上。原因最常见的是消除后先 removeItem 再手动 delete item但之前启动的 QPropertyAnimation 还持有这个图元指针另一种情况是 m_cells 数组里对应位置没有置空下一次点击拿到了一个已经删除的悬空指针。Qt 的内存管理不会自动帮你清理 QGraphicsPixmapItem 在数据结构里的引用。解决统一约定两个规则。第一图元移除后不手动 delete用 deleteLater() 让事件循环安全回收第二m_cells 数组在移除图元的同时立刻置为空void BoardScene::removeItemAt(int row, int col) { if (m_cells[row][col].item) { removeItem(m_cells[row][col].item); m_cells[row][col].item-deleteLater(); m_cells[row][col].item nullptr; } m_cells[row][col].type TileType::None; }血泪经验不要在“只是小项目”的时候放任裸指针乱飞RPG 连连看这种状态频繁切换的程序悬空指针的崩溃是迟早的事而且没有后悔药。5.3 linuxfb平台插件找不到qt.qpa.plugin报错在嵌入式设备上怎么解现象交叉编译后的程序拷贝到 ARM 开发板或麒麟系统上启动时终端输出qt.qpa.plugin: could not find the Qt platform plugin linuxfb in然后程序直接退出。原因Qt 的 QPA 插件机制没能自动发现平台插件。桌面 Windows 上 windeployqt 会生成 platforms 目录但嵌入式交叉编译时发布目录往往没有把 libqlinuxfb.so 连同目录结构一起带上。另外编译 Qt 时若没有启用 linuxfb 功能运行时也会报同样错误。解决发布前确认目标 Qt 的 plugins/platforms 目录里有 libqlinuxfb.so把它放到程序目录下的 platforms/ 中运行时显式指定平台和环境变量export QT_QPA_PLATFORMlinuxfb export QT_QPA_PLATFORM_PLUGIN_PATH./platforms ./RpgLink还要注意 linuxfb 需要 framebuffer 设备存在即 /dev/fb0 可访问。如果设备节点缺失插件加载成功后仍会报 open framebuffer 失败那是另一层问题别和插件缺失混在一起查。5.4 编译报错cannot find -lpublicLIBS路径与名称是两回事现象在 .pro 文件里加了一个第三方库编译到链接阶段报error: cannot find -lpublic但去目录里看明明有一个 public.dll 或者 libpublic.a 放在那里。原因qmake 里 -lpublic 会让链接器寻找文件名 libpublic.aLinux或 public.libWindows取决于编译器。很多人翻车在两个地方一是把路径写成了 LIBS -lD:/path/public相当于告诉链接器找一个叫 path/public 的库当然找不到二是写了 -lpublic 却忘了告诉链接器库在哪个目录即缺少 -L 参数。所以 create 出来的是 libpublic.a 还是 public.dll.a 也要先 ls 看清楚。解决统一分两步写LIBS -L$$PWD/lib LIBS -lpublic$$PWD 是 .pro 文件所在目录-L 指定搜索目录-lpublic 指定库名。如果库文件名是 libpublic.a-lpublic 没问题如果是 public.lib在 MinGW 下要看它是不是 MSVC 格式混用会报 Bad file format。新手可以先写成绝对路径验证LIBS C:/lib/public.lib编译链接通过后再改成 -L/-l 组合。每次遇到底层找不到库先看清楚实际文件名再检查 -L比瞎试快得多。5.5 动画没播完就连点busy标志和事件队列现象快速点击第二对时第一对的消除动画还在移动掉落的棋子坐标错乱甚至出现两个棋子在同一个位置。原因mousePressEvent 里只判断了有没有选中第一个棋子没有判断动画状态。动画期间 m_cells 数组可能已经被更新但图元视觉位置还没跟上此时点击落到旧坐标上行列判断和数据就对不上了。解决在 BoardScene 里加一个 bool m_animating动画开始时置 true所有动画节点完成后置 false。mousePressEvent 第一行就是if (m_animating) return;同时在每个格子结构体里维护 busy双保险。不要用禁用整个 scene 的粗暴方式因为玩家在连锁消除时会习惯预选下一个目标禁用整块棋盘体验很差。我的做法是允许点击但只在 Selecting 状态响应Animating 状态直接忽略这样手速快的人反而能提前选中很舒服。6. 最后一块砖给RPG连连看做存档读档与发布自检6.1 用QSettings存档几行代码带走全部进度RPG 项目没有存档说不过去。常见做法是用 QSettings 写 ini不需要引入第三方库。关窗时保存void MainWindow::closeEvent(QCloseEvent* event) { QSettings s(MyCompany, RpgLink); s.setValue(role/hp, m_board-role().hp); s.setValue(role/exp, m_board-role().exp); s.setValue(role/level, m_board-role().level); s.setValue(game/levelIndex, m_currentLevel); s.setValue(game/board, saveBoard(m_board-cells())); event-accept(); }saveBoard 可以把二维数组序列化成 QByteArray再转 Base64 字符串存进去。棋盘不算大8×10 个 int 压缩后很小没必要用 SQLite。启动时读取缺省值直接用第二个参数int hp s.value(role/hp, 100).toInt();这样老玩家关掉再打开剧情停在上一关棋盘原样恢复比每次从头开始更能留住人。6.2 发布自检把包当成一个陌生人去验证做完存档后我的习惯是把构建目录整个删掉重新编译release 后再跑 windeployqt然后解压到虚拟机暂时退出杀毒软件强制自己按玩家流程走一遍开新游戏看完剧情连续消除三对触发一次技能存档重启程序读档退出。只要中途任何一步报错回 Qt Creator 带调试器重跑看崩溃栈指向哪个对象。很多“玄学闪退”只在 release 出现debug 正常多半是未初始化变量或数组越界开 AddressSanitizer 编译一次能很快定位。多做一次干净环境验证就等于少收一次用户“打不开”的反馈。这套流程我每次发布 Qt 小工具都会走一遍已经成为习惯希望帮到你。本文还有配套的精品资源点击获取