资讯动态

Qt5翻金币游戏源码拆解:动画、状态机与打包避坑

发布时间:2026/10/25 13:25:17 来源:尧图企业网站定制
简介《QT翻金币游戏》是一套基于 Qt5 的跨平台互动游戏源码面向已掌握 C 基础、希望借助完整项目理解 Qt 界面开发与动画机制的开发者。项目以翻金币闯关为核心包含开始界面、菜单界面和关卡选择界面借助按钮悬停点击特效、金币翻转动画与流畅窗口切换串联完整流程并设置 20 个关卡状态适合用作课程设计、Qt 入门实战或界面交互学习的参考样例。压缩包共 104 个文件、约 13.53MB主体由 21 个 cpp 与 10 个 h 源文件构成配合 .pro 工程文件、.ui 界面文件和 .qrc 资源文件另含 18 张 png 图片、4 个 wav 音效及编译产物与中间文件可直接查看工程组织、信号与槽连接、资源打包及界面素材的组织方式。已有 911 人学习便于读者从源码中拆解布局管理、QPropertyAnimation 动画、QSS 样式与多界面切换思路进而复用到自己的 Qt 项目中。1. 一个压缩包里的 Qt5 翻金币游戏它到底能干什么同事把这份QT翻金币游戏.rar发给我时我以为又是哪个培训班的课堂作业。解压、用 Qt Creator 打开.pro、点运行从主菜单点进选关界面4×4 的金币在 240 毫秒里一帧帧翻过去配对成功有音效通关有弹窗——我闭嘴了。这是一份完整的 Qt5 翻金币游戏源码Qt Widgets qmake 工程自绘按钮做翻转动画多个场景靠信号槽串起来关卡数据和素材全部走.qrc资源。它适合两类人想把信号槽、QTimer、事件分发这套东西从课本搬到能跑的程序里的新手和想找一个干净小工程练手重构、加功能的老手。下面按「能干什么 → 怎么跑 → 怎么改」一步步拆。2. 工程骨架三个场景加一个金币按钮类文件不多但分工明确2.1 目录结构谁负责什么解压之后看到的结构基本是这样的CoinFlip/ ├── CoinFlip.pro # qmake 工程描述 ├── main.cpp # 入口只有十几行 ├── mainscene.h/.cpp # 主菜单开始、退出 ├── chooselevelscene.h/.cpp # 选关20 个关卡按钮 返回 ├── playscene.h/.cpp # 对局棋盘、配对、胜负结算 ├── mycoin.h/.cpp # 金币按钮继承 QPushButton ├── dataconfig.h/.cpp # 关卡数据4×4 的 0/1 矩阵 └── res/ ├── Coin0001.png ~ Coin0008.png # 翻转动画的 8 帧 ├── BoardNode.png # 棋盘底图 ├── LevelIcon.png # 关卡按钮图标 ├── ConFlipSound.wav # 配对成功音效 ├── WinSound.wav # 通关音效 └── res.qrc三个场景类全部继承QWidget彼此之间没有父子窗口关系靠new出来show()、把自己hide()完成切换。MyCoin是唯一的控件子类也是整个工程里唯一重写了交互行为的地方。DataConfig是个纯数据类不继承任何 Qt 基类只负责把某个关卡的数字吐给PlayScene。一层一层的依赖方向很干净main.cpp只认识MainSceneMainScene拉起ChooseLevelSceneChooseLevelScene把选中的关卡号传给PlayScenePlayScene批量造MyCoin。没有循环依赖也没有全局单例这一点比很多教程工程强——想单独把MyCoin拿出去复用复制两个文件就够。2.2.pro和res.qrc两个最容易被忽略的入口CoinFlip.pro是整套构建的根QT core gui multimedia # QSound 在 Qt5 里属于 multimedia 模块 greaterThan(QT_MAJOR_VERSION, 4): QT widgets TARGET CoinFlip TEMPLATE app CONFIG c11 SOURCES \ main.cpp \ mainscene.cpp \ chooselevelscene.cpp \ playscene.cpp \ mycoin.cpp \ dataconfig.cpp HEADERS \ mainscene.h \ chooselevelscene.h \ playscene.h \ mycoin.h \ dataconfig.h RESOURCES \ res/res.qrcQT multimedia这一行是新手翻车率最高的地方。漏了它#include QSound直接报No such file or directory而报错指向的是头文件不是.pro很多人第一反应是去翻 Qt 安装目录。greaterThan(QT_MAJOR_VERSION, 4)这句是从 Qt4 时代带过来的兼容写法——Qt4 里 widgets 属于 gui 模块Qt5 才拆出去保留它没坏处删了在 Qt5 上也能编。CONFIG c11看似可有可无但代码里用了 lambda 连接信号槽connect(timer, QTimer::timeout, this, [this](){...})没有它 MSVC 会直接拒编。res.qrc决定资源路径怎么拼RCC qresource prefix/ fileCoin0001.png/file fileCoin0002.png/file fileBoardNode.png/file fileConFlipSound.wav/file fileWinSound.wav/file /qresource /RCC这里的file路径是相对于 .qrc 文件自身所在目录的。因为res.qrc就躺在res/里面图片和它同级所以只写文件名。代码里访问时前缀/加上文件名得到:/Coin0001.png。如果哪天你把.qrc挪到工程根目录所有路径立刻失效而且QPixmap加载失败不会抛异常只会静默返回空图——界面上一片白控制台一声不吭。提示Qt Creator 左侧的 Resources 面板里右键任意资源选「Copy Path」拿到的是运行时能用的完整:/...字符串比自己拼靠谱得多。2.3 场景切换不用 QStackedWidget直接 new show hide主菜单点「开始」的那一刻发生了什么// mainscene.cpp void MainScene::onStartClicked() { // 按需创建避免程序一启动就加载 20 个关卡按钮 ChooseLevelScene *choose new ChooseLevelScene; choose-setGeometry(this-geometry()); // 保持窗口位置和尺寸一致 choose-show(); this-hide(); // 选关界面点返回时把主菜单重新亮出来 connect(choose, ChooseLevelScene::backToMain, this, [](){ this-show(); choose-hide(); }); }这段代码有三处值得说。第一setGeometry(this-geometry())保证了场景切换时窗口位置和大小不跳。少了这一句从主菜单跳到选关界面窗口会突然跑到屏幕中间视觉上像换了个程序。第二new出来的ChooseLevelScene没有存成员指针也没设Qt::WA_DeleteOnClose。严格说这是泄漏但游戏里选关界面只会被创建有限次泄漏量可以忽略真正要小心的是如果你在ChooseLevelScene里再new一个PlayScene并且反复进出那就要么加setAttribute(Qt::WA_DeleteOnClose)要么把它提升成MainScene的成员变量复用。第三connect的第五个参数默认是Qt::AutoConnection在这里信号和槽都在主线程实际走的是直连lambda 里直接操作this的窗口是安全的。一旦你以后把加载关卡数据挪到工作线程这个默认行为会立刻变成隐患——Qt 的信号槽跨线程默认是队列连接lambda 里碰 UI 会崩。3. 跑起来从 Qt5 环境到第一次点开金币3.1 环境准备版本和编译器怎么选这份源码是 Qt5 时代的产物QSound、QDesktopWidget这些类在 Qt6 里已经被移除或迁移所以第一原则是别用 Qt6 打开它。推荐 Qt 5.12 或 5.15 的 LTS 版本安装器里勾选这几项组件是否必需说明Qt 5.15.x 下的 MinGW 64-bit 或 MSVC 2019 64-bit必需编译器套件二选一Qt Creator必需建议 4.11 以上认 Qt5 工程Qt Multimedia必需QSound就在这里漏勾选后面必卡Qt Widgets必需默认勾选别手动取消CMake / Ninja不需要工程是 qmake装了也用不上用 MinGW 还是 MSVC如果你只是想把游戏跑起来看效果MinGW 更省事——不需要装 Visual StudioQt 安装器自带。如果你打算后面加第三方库、或者要把程序发给没装运行库的人MSVC 的 Release 产物更干净。血泪经验是MinGW 编出来的 exe 依赖libgcc_s_seh-1.dll、libstdc-6.dll这一串漏一个就是启动闪退MSVC 依赖少一些但要求目标机器装了 VC 运行库。装完之后在 Qt Creator 里「文件 → 打开文件或项目」选中CoinFlip.pro选择套件直接点左下角的绿色三角。qmake 会自动执行一遍生成Makefile然后编译、链接、运行。3.2 编译与运行三个必查项第一次构建失败很正常按这个顺序排查能省掉大半时间。第一看「常规消息」面板里的 qmake 输出。如果它报Project ERROR: Unknown module(s) in QT: multimedia说明你选的套件里没装 Qt Multimedia。不用重装整个 Qt打开 Qt 维护工具给当前版本补勾 Multimedia 就行。第二看构建目录有没有生成资源文件。qmake 会把res.qrc编译成qrc_res.cpp再参与链接。如果构建目录里找不到这个文件说明.pro的RESOURCES路径写错了或者.qrc文件被其他杀毒软件隔离了。资源没编进去的表现是程序能正常启动、界面能显示就是所有图片和音效全都缺失。第三看运行时的应用程序输出。Qt Creator 底部的「应用程序输出」面板会捕获qDebug()和qWarning()。原版代码里加载图片的地方没有判空如果你自己加了一句if (pix.isNull()) qWarning() imgPath;路径错了当场就能看到具体是哪个文件不用靠猜。跑起来之后你应该看到一个固定尺寸的窗口背景是棋盘图中间 4×4 排着 16 枚银币左上角返回按钮底部显示「第 1 关」。点任意一枚金币它会播放 8 帧翻转动画变成金币再点另一枚两枚朝向相同就锁死并响一声不同就 350 毫秒后一起翻回去。3.3 打包发给别人windeployqt 和资源遗漏自己机器能跑不代表别人能跑。Qt 的动态链接方式决定了 exe 旁边必须有一堆 DLL手工拷容易漏。标准做法是先用 Release 配置构建再让 Qt 自带的部署工具补齐# 1) 建一个空目录把 Release 产物拷进去 mkdir deploy cp release/CoinFlip.exe deploy/ # 2) 用 Qt 命令行环境执行保证 windeployqt 在 PATH 里 windeployqt --release --no-translations deploy/CoinFlip.exe # 3) 检查关键依赖是否到位 ls deploy/platforms/qwindows.dll # 没有它程序启动就报「找不到平台插件」 ls deploy/Qt5Multimedia.dll # 没有它音效静默失效--no-translations是砍掉几十种语言的翻译文件能省十几兆。--release必须和你的构建配置一致混用会让 windeployqt 拷错版本的 DLL运行时直接0xc000007b。有个细节容易忽略.qrc里的资源是被编进 exe 的不需要额外拷贝图片但如果你后面把某张图换成了 JPG 格式就得确认deploy/imageformats/下有qjpeg.dll。PNG 是 Qt 内建支持的不需要插件WAV 也不需要。这个差异很多人第一次遇到会以为是资源路径问题其实是格式插件没部署。4. 核心模块拆解翻转动画和对局状态机4.1 MyCoin8 帧定时器动画是怎么翻的金币翻转不是 3D 变换是 8 张预渲染的图片轮播。这个做法在 2D 小游戏里非常常见——比QPropertyAnimation配合QTransform省性能也比纯代码画图省事。头文件里的关键成员// mycoin.h class MyCoin : public QPushButton { Q_OBJECT public: explicit MyCoin(const QString coinImg, QWidget *parent nullptr); void flip(); // 播放一次 8 帧翻转动画 void lockCoin(); // 配对成功后锁死不再响应点击 int posX 0; // 在 4×4 棋盘里的列 int posY 0; // 在 4×4 棋盘里的行 bool flag false; // true 金币朝上false 银币朝上 bool isAnimation false; bool isMatched false; private: QTimer *timer nullptr; int curFrame 1; static const int MAX_FRAME 8; };实现部分// mycoin.cpp MyCoin::MyCoin(const QString coinImg, QWidget *parent) : QPushButton(parent) { QPixmap pix(coinImg); if (pix.isNull()) { qWarning() 金币素材没加载上: coinImg; // 排障用别省 } setFixedSize(pix.size()); setStyleSheet(QPushButton{border:0px;}); // 去掉默认边框和按下态 setIcon(pix); setIconSize(pix.size()); timer new QTimer(this); connect(timer, QTimer::timeout, this, [this](){ QPixmap frame(QString(:/res/Coin000%1.png).arg(curFrame)); if (!frame.isNull()) { setIcon(frame); setIconSize(frame.size()); } if (curFrame MAX_FRAME) { curFrame 1; // 复位帧号否则下一轮会从中间帧开始 timer-stop(); isAnimation false; // 动画结束允许下一次点击 } }); } void MyCoin::flip() { if (isAnimation || isMatched) return; // 动画期间的重入直接丢弃 isAnimation true; curFrame 1; timer-start(30); // 8 帧 × 30ms ≈ 240ms接近真实硬币翻面的节奏 } void MyCoin::lockCoin() { isMatched true; setEnabled(false); // 顺手屏蔽 hover / pressed 的视觉反馈 }几个参数值得掰开说。setFixedSize(pix.size())用的是首帧图片的尺寸所以 8 张帧图必须等大——尺寸不一致会导致按钮在动画过程中抖动肉眼非常明显。setStyleSheet(QPushButton{border:0px;})是必须的QPushButton默认带边框和按下时的下沉效果套在金币图上就是一圈灰边。timer-start(30)里的 30 毫秒对应大约 33 帧每秒是动画流畅度和 CPU 占用的折中值改成 16 会更快更顺但 16 毫秒的定时器在 Windows 上实际精度只有 15.6 毫秒容易跳帧。flip()开头那句if (isAnimation || isMatched) return;是整个动画模块的防重入闸门。没有它手快的玩家在一枚金币还没翻完时再点一次curFrame会被重置成 1波形就乱了——表现出来就是金币翻到一半突然停顿然后接着翻。4.2 PlayScene4×4 棋盘和点击配对状态机对局逻辑的核心是一个 4×4 的MyCoin*二维数组加上两个指针记录本轮点开的第一枚和第二枚。// playscene.h 关键成员 class PlayScene : public QWidget { Q_OBJECT public: explicit PlayScene(int level, QWidget *parent nullptr); signals: void backToLevel(); private slots: void onCoinClicked(); private: void buildBoard(const QListint levelData); void checkPair(); void resetSelection(); void winGame(); MyCoin *m_coins[4][4] {nullptr}; MyCoin *m_first nullptr; MyCoin *m_second nullptr; int m_level 1; int m_matched 0; // 已配对数达到 8 就算通关 bool m_locked false; // 错配回翻期间锁住整个棋盘 };铺棋盘的代码void PlayScene::buildBoard(const QListint levelData) { for (int y 0; y 4; y) { for (int x 0; x 4; x) { const int v levelData.at(y * 4 x); // 行优先取数据 const QString img (v 1) ? :/res/Coin0001.png : :/res/Coin0008.png; MyCoin *coin new MyCoin(img, this); coin-posX x; coin-posY y; coin-flag (v 1); coin-move(57 x * 55, 124 y * 55); // 55 图标宽 间隙 connect(coin, MyCoin::clicked, this, PlayScene::onCoinClicked); m_coins[y][x] coin; } } }55这个数字是金币图标宽度约 50 像素加间隙57和124是棋盘底图上第一个格子的左上角坐标。这三个数决定了金币能不能严丝合缝地落在棋盘格子里换背景图就得跟着调。点击处理是全局唯一的槽void PlayScene::onCoinClicked() { if (m_locked) return; MyCoin *coin qobject_castMyCoin *(sender()); if (!coin || coin-isAnimation || coin-isMatched) return; coin-flip(); coin-flag !coin-flag; // 状态立刻改动画只是表现层 if (!m_first) { m_first coin; return; } if (m_first coin) { m_first nullptr; return; } // 连点同一枚撤销选择 m_second coin; checkPair(); } void PlayScene::checkPair() { const bool matched (m_first-flag m_second-flag); if (matched) { m_first-lockCoin(); m_second-lockCoin(); m_matched; QSound::play(:/res/ConFlipSound.wav); resetSelection(); if (m_matched 8) winGame(); } else { m_locked true; // 回翻期间不接受任何输入 QTimer::singleShot(350, this, [this](){ m_first-flip(); m_first-flag !m_first-flag; m_second-flip(); m_second-flag !m_second-flag; m_locked false; resetSelection(); }); } }这段代码里唯一真正难的地方是coin-flag !coin-flag;和coin-flip();的顺序。flip()是异步的240 毫秒之后才走完但flag必须在这一刻就翻转。为什么因为玩家可能在动画还没播完时就点了第二枚如果等到动画结束再改flagcheckPair()读到的就是旧值配对判断会完全错乱。这是「状态与表现分离」的教科书案例——状态同步更新动画只负责把它演出来。m_locked这个标志位管的是另一件事错配之后两枚金币要一起翻回去这期间如果玩家还能点第三枚就会进入m_first已被置空的中间态逻辑直接崩掉。所以整盘锁 350 毫秒比逐枚加锁简单可靠。QTimer::singleShot的第三个参数this是上下文对象窗口销毁时这个延时回调会被自动取消不会野指针。4.3 关卡数据和胜负判定DataConfig负责把第 N 关的 16 个 0/1 交给PlayScene。原版常见两种做法硬编码进一个QMapint, QListint或者从二进制.dat文件里用QDataStream读。后者的实现长这样void DataConfig::loadFromFile(const QString path) { QFile file(path); if (!file.open(QIODevice::ReadOnly)) { qWarning() 关卡数据打不开: path file.errorString(); return; } QDataStream in(file); in.setVersion(QDataStream::Qt_5_12); // 不显式设置跨版本会读出乱码 int levelCount 0; in levelCount; for (int i 0; i levelCount; i) { QListint level; for (int j 0; j 16; j) { int v 0; in v; level v; } mData.insert(i 1, level); } }setVersion这行是硬性要求。QDataStream的序列化格式在不同 Qt 版本之间有细微差异写入时用的是 Qt 5.9、读取时用的是 5.15可能读出全 0。胜负判定就一句话m_matched 8。4×4 一共 16 枚两两配对正好 8 对。通关的收尾void PlayScene::winGame() { QSound::play(:/res/WinSound.wav); QTimer::singleShot(600, this, [this](){ QMessageBox::information(this, 过关, QString(第 %1 关通过).arg(m_level)); emit backToLevel(); // 通知选关界面刷新 }); }600 毫秒的延迟是为了让最后一对金币的翻转动画播完再弹窗否则玩家点下最后一枚的瞬间就被弹窗糊脸观感很差。这种「几百毫秒的等待」在游戏里到处都是参数调好了是手感调不好就是玄学。5. 避坑与排查五个我真实踩过的坑5.1 界面能出来但所有图片一片空白现象程序正常启动窗口尺寸也对但金币、棋盘、按钮图标全是空白控制台一条错误都没有。原因QPixmap加载失败时返回空对象setIcon(QIcon(空图))不会报错Qt 只是安静地什么都不画。绝大多数情况是.qrc里的file相对路径、prefix前缀、代码里的:/...三者对不上或者.qrc压根没写进.pro的RESOURCES。解决在每个加载图片的地方加一句if (pix.isNull()) qWarning() path;。然后去 Qt Creator 的 Resources 面板确认文件真的在树里右键「Copy Path」拿到准确字符串别手打。5.2 手速一快配对就错乱现象连续快速点击三枚金币配对被跳过、或者计数变成 9 对直接卡死。原因240 毫秒的翻转动画期间按钮仍然可点。第一枚还在翻、flag已经改过一次第二枚进来又改一次checkPair()拿到的是脏数据。同时flip()没有防重入curFrame被重置动画帧序错乱。解决flip()开头加if (isAnimation) return;动画结束时把curFrame复位成 1场景层再加一个m_locked在任何需要等待的时刻错配回翻、通关弹窗把整个棋盘锁住。两层保护缺一不可只做一层在极端手速下还是会漏。5.3 音效完全没声音控制台也不报错现象点击、配对、通关逻辑都对就是一点声都没有。原因三种可能。一是.pro里漏了QT multimedia编译期就该报错但有些人把QSound的 include 注释掉了绕过去二是 Qt6 环境下QSound已经被移除挪到了QSoundEffect三是 WAV 的采样率是 48kHz 双声道个别平台的多媒体后端解不了。解决先确认 Qt 版本。Qt5 加 multimedia 模块Qt6 换成QSoundEffect并且注意它的setSource要吃QUrlqrc 路径要写成QUrl(qrc:/res/ConFlipSound.wav)多一个qrc:前缀。音频素材统一转成 44.1kHz / 16bit / 单声道 WAV体积小、兼容性最好。5.4 换台机器就报 0xc000007b 或者缺 DLL现象自己电脑上跑得好好的拷给别人就弹「无法启动此程序因为计算机中丢失 Qt5Core.dll」或者更笼统的0xc000007b。原因只拷了 exe。Qt 程序依赖Qt5Core、Qt5Gui、Qt5Widgets、Qt5Multimedia以及platforms/qwindows.dll。0xc000007b通常是 32 位和 64 位的 DLL 混在了一起——比如 exe 是 64 位但 PATH 里有 32 位的 Qt 库被优先加载了。解决用windeployqt一把补齐然后务必检查deploy/platforms/目录下有没有qwindows.dll。最终验证不要在自己机器上做找一台没装过 Qt 的干净机器或者新建一个虚拟机跑一遍这是唯一的真验证。5.5 关卡数据读出来全是 0选哪关都是满盘银币现象选第 3 关棋盘上 16 枚全是银币按什么顺序点都没法通关。原因QDataStream版本不一致。写.dat的人和读.dat的人用的 Qt 版本不同序列化格式有差异读出来全变成默认值 0。解决写入和读取两端都显式setVersion(QDataStream::Qt_5_12)。或者更干脆——20 关的 4×4 数据一共才 320 个 int直接硬编码进DataConfig的QMap里省掉文件 IO 和版本问题维护成本反而更低。6. 二次开发换皮、加难度、做难度曲线6.1 把素材和格子参数抽成一份主题配置原版最麻烦的地方是路径和坐标散落在三四个文件里换一套皮肤要改七八处。我一般会先抽一个结构体出来// theme.h —— 换皮只改这一处 struct Theme { QString framePattern :/res/Coin000%1.png; // 8 帧%1 从 1 到 8 QString boardBg :/res/BoardNode.png; QString flipSound :/res/ConFlipSound.wav; QString winSound :/res/WinSound.wav; int cellStep 55; // 格距 QPoint origin QPoint(57, 124); // 棋盘左上角 };MyCoin的构造函数改成接收framePattern而不是单张图buildBoard里用theme.origin QPoint(x, y) * theme.cellStep算位置。这样接第二套皮肤就是新建一个Theme实例的事不用碰任何逻辑代码。6.2 加计时、步数和难度曲线加计时器只需要十几行但顺序有讲究——必须在第一枚金币被点开时才启动不能一进关卡就走表// 在 onCoinClicked 里第一次记录 m_first 的同时 if (!m_clock-isActive()) m_clock-start(1000); connect(m_clock, QTimer::timeout, this, [this](){ m_seconds; m_timeLabel-setText(QString(%1:%2) .arg(m_seconds / 60, 2, 10, QChar(0)) .arg(m_seconds % 60, 2, 10, QChar(0))); });arg(值, 字段宽度, 进制, 填充字符)这个四参数重载是补零的关键写成QString::number(m_seconds / 60)得到的是1:5而不是01:05。难度曲线我建议这么铺关卡范围棋盘配对时限步数上限改动点1 ~ 54×4不限不限保持原版让玩家熟悉节奏6 ~ 124×460 秒20 步加计时器超时判负13 ~ 185×475 秒24 步棋盘尺寸要参数化MyCoin *m_coins[4][4]得换成QVectorQVectorMyCoin*19 ~ 206×490 秒30 步加「错配扣 3 秒」的惩罚棋盘从 4×4 往上扩第一个要改的不是数组是布局。57 x * 55这个硬本文还有配套的精品资源点击获取

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

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

免费获取报价 →
↑