☰
Qt桌面游戏开发实战:RPG连连看架构与状态机设计
2026/11/2 11:35:45 网站建设 项目流程

简介:这是一份基于Qt框架开发的RPG风格连连看桌面游戏源码项目,面向C++与Qt初学者及高校课程实践者,旨在通过轻量级游戏开发掌握图形界面设计、信号槽机制、QGraphicsView场景绘制及基础游戏逻辑实现。资源共26个文件,包含5个cpp与4个h核心源码文件(如mainwindow.cpp、double_game_window.cpp等)、3个ui界面文件、1个pro工程配置、1个qrc资源文件、8张jpg游戏素材图及2个txt模式说明文档,完整覆盖编译构建、UI布局、资源加载与游戏模式切换全流程,压缩包仅1.99MB,结构清晰、开箱即用。已有373人学习下载,适合用于Qt入门实训、C++课程设计或小型游戏开发练手——读者可直接编译运行,深入理解QTimer计时控制、自定义QPushButton封装、双游戏模式(普通/信号)切换逻辑,以及RPG元素(如经验积累、模式解锁)在简易框架中的落地方式。

1. 这不是玩具:一个用 Qt 写的、能跑通完整 RPG 逻辑链的连连看——战斗、存档、技能树全在 QWidget 里跑起来了

你见过在连连看里打 Boss 吗?不是弹个提示框说“通关了”,而是点击两个火球图标触发「烈焰连击」,角色头顶飘出 -127 伤害,敌人血条实时缩进,紧接着自动播放一段 3 帧动画,然后掉落一张「初级炼金配方」道具卡——这张卡还能拖进背包格子,双击使用后解锁新关卡地图。这不是 Unity 插件 Demo,也不是 Electron 套壳网页,它就压在一个 8.2MB 的.rar包里,主程序是MatchGame.exe(Windows)或MatchGame.app(macOS),源码目录下没有CMakeLists.txt外的任何构建脚本,main.cpp第一行就是#include <QApplication>。项目面向的是想把游戏逻辑真正“落地”到原生桌面端的 Qt 开发者:你不需要懂 OpenGL 渲染管线,但得清楚QGraphicsScene怎么和QTimer配合做帧同步;你不用写网络同步,但得明白QSettings存档时为什么不能直接序列化QVector<QPixmap>;你可能刚写完三个按钮的登录界面,但这次要亲手把“消除→得分→技能触发→状态变更→UI 刷新→存档写入”这条链路从头焊死。它不教 Qt 是什么,它只负责让你在qDebug()打出第一行"Player HP: 42"时,手指悬在键盘上停三秒——因为你知道,这行字背后连着七层信号槽。


2. 架构拆解:从main.cpp到SkillSystem.h,Qt 桌面游戏的五层洋葱结构

这个项目没用 QML,所有 UI 全是 QWidget + QGraphicsView 组合拳。整个工程像一颗剥开的洋葱:最外层是用户看到的窗口容器,往里是游戏状态机,再往里是核心匹配引擎,接着是 RPG 系统胶水层,最中心才是像素级的图块操作。这种结构不是为了炫技,而是为了在不引入额外依赖的前提下,让每个模块都能被单独单元测试——比如TileMatcherTest.cpp里,你甚至可以不启动 GUI,只传入一个QVector<TileData>就验证连通性算法是否漏判斜向路径。

2.1 主程序入口与窗口生命周期管理:main.cpp和GameWindow.h

main.cpp只干三件事:初始化 QApplication、构造GameWindow实例、调用show()。真正的门面在GameWindow.h——它继承自QMainWindow,但重写了closeEvent():

// GameWindow.cpp void GameWindow::closeEvent(QCloseEvent *event) { if (m_gameEngine->isRunning()) { m_gameEngine->saveCurrentProgress(); // 关键:退出前强制存档 QMessageBox::information(this, "已保存", "当前进度已存入 local.conf"); } event->accept(); }

这里埋了个血泪经验:Qt 默认关闭窗口会直接销毁对象,但QGraphicsScene里的QGraphicsItem如果持有QPixmap引用,而QPixmap又来自QResource(即:/images/tile_fire.png),那么在QApplication析构后期,资源系统可能已被释放,导致QPixmap::isNull()返回 true 却不报错,后续绘图直接黑屏。解决方案是:所有QPixmap加载必须在GameWindow构造函数中完成,并用QScopedPointer<QPixmap>管理其生命周期。项目里TileFactory.cpp就是这么做的:

// TileFactory.cpp QScopedPointer<QPixmap> TileFactory::loadPixmap(const QString &path) { auto pixmap = QScopedPointer<QPixmap>(new QPixmap()); if (!pixmap->load(path)) { qCritical() << "Failed to load pixmap:" << path; // 回退到纯色占位图,避免崩溃 pixmap->fill(Qt::darkGray); } return pixmap; }

提示:QScopedPointer在GameWindow析构时自动释放QPixmap,且释放时机早于QApplication资源清理阶段,这是 Qt 桌面应用避免“退出闪退”的硬核姿势。

2.2 游戏状态机:GameStateMachine.h与七种状态的流转契约

RPG 连连看最反直觉的设计在于:它把“玩家正在选中两个图标”定义为一种独立状态,而不是临时变量。GameStateMachine用枚举定义了kIdle,kSelectingFirst,kSelectingSecond,kMatching,kBattle,kInventory,kGameOver七种状态,并通过QStateMachine的addTransition()显式声明流转条件:

// GameStateMachine.cpp m_stateIdle->addTransition(m_signalEmitter, &SignalEmitter::tileSelected, m_stateSelectingFirst); m_stateSelectingFirst->addTransition(m_signalEmitter, &SignalEmitter::tileSelected, m_stateSelectingSecond); m_stateSelectingSecond->addTransition(m_matcher, &TileMatcher::matchFound, m_stateMatching);

关键点在于:所有状态变更必须由信号驱动,禁止直接调用setState()。这样做的好处是,当你要加“暂停功能”时,只需在kIdle状态下监听QKeySequence::Pause信号,发射一个gamePaused()信号,然后让kIdle过渡到kPaused(新增状态)——而原有七种状态的代码完全不用动。项目里BattleState.cpp就靠这套机制实现了“战斗中点击背包按钮 → 自动切到kInventory→ 战斗计时器暂停 → 返回时恢复倒计时”。

2.3 核心匹配引擎:TileMatcher.h与带权重的连通性算法

连连看的“连通性判断”是性能瓶颈。本项目没用 BFS/DFS,而是实现了一个带路径代价的 A变种*:每条路径的“代价”不是步数,而是转向次数 + 直线段长度倒数。这样能优先匹配“L 形”路径(转向 1 次),而非“Z 形”(转向 2 次),更符合人眼直觉。算法核心在TileMatcher::findPath():

// TileMatcher.cpp struct PathNode { QPoint pos; int turns; // 当前路径已转向次数 int straightLen; // 当前直线段长度 int cost; // 总代价:turns * 100 + (1000 / straightLen) }; QList<QPoint> TileMatcher::findPath(const QPoint &start, const QPoint &end) { std::priority_queue<PathNode, QVector<PathNode>, CompareCost> openSet; QMap<QPoint, PathNode> cameFrom; QMap<QPoint, int> gScore; // 初始化起点 PathNode startNode{start, 0, 1, 0}; openSet.push(startNode); gScore[start] = 0; while (!openSet.empty()) { PathNode current = openSet.top(); openSet.pop(); if (current.pos == end) { return reconstructPath(cameFrom, end); } for (const QPoint &neighbor : getValidNeighbors(current.pos)) { int turnCost = (isDirectionChanged(current.pos, neighbor)) ? 100 : 0; int newCost = gScore[current.pos] + turnCost + (1000 / (current.straightLen + 1)); if (!gScore.contains(neighbor) || newCost < gScore[neighbor]) { gScore[neighbor] = newCost; PathNode nextNode{neighbor, current.turns + (turnCost ? 1 : 0), turnCost ? 1 : current.straightLen + 1, newCost}; cameFrom[neighbor] = current; openSet.push(nextNode); } } } return {}; // 无路径 }

注意:isDirectionChanged()判断相邻两步是否同向(如从 (0,1) 到 (0,2) 是直行,从 (0,1) 到 (1,1) 是转向)。这个设计让“消除两个相邻火球”比“消除两个隔一格的冰锥”优先级更高,玩家手感更顺。

2.4 RPG 系统胶水层:SkillSystem.h与技能效果的声明式注册

RPG 部分最精妙的是SkillSystem——它不写 if-else 判断技能类型,而是用函数指针注册表 + JSON 配置驱动。所有技能效果定义在resources/skills.json:

{ "fire_blast": { "name": "烈焰爆破", "cost": 15, "effect": "damage_enemy", "params": {"base_damage": 80, "crit_rate": 0.15} }, "heal_light": { "name": "微光愈合", "cost": 10, "effect": "heal_player", "params": {"base_heal": 30} } }

SkillSystem.cpp在启动时解析 JSON,然后调用注册函数:

// SkillSystem.cpp void SkillSystem::registerEffect(const QString &effectName, EffectFunc func) { m_effectRegistry[effectName] = func; } // 在构造函数中: registerEffect("damage_enemy", [this](const QJsonObject &params) { int damage = params["base_damage"].toInt() * (1 + m_playerLevel * 0.2); if (QRandomGenerator::global()->bounded(100) < params["crit_rate"].toDouble() * 100) { damage *= 2; emit criticalHitTriggered(); } m_enemyHP -= damage; emit enemyHpChanged(m_enemyHP); });

这种设计让策划改技能数值只需改 JSON,程序员不用碰 C++ 代码;新加技能也只要在SkillSystem::init()里加一行registerEffect("xxx", ...),彻底解耦。

2.5 图块渲染层:TileItem.h与 QGraphicsItem 的复用陷阱

每个可点击的图标都是一个TileItem,继承自QGraphicsItem。但这里有个大坑:QGraphicsItem默认不启用ItemIsMovable和ItemIsSelectable,而项目里用了QGraphicsItemGroup来组合“图标+底纹+高亮边框”,结果发现点击事件收不到。根本原因是:QGraphicsItemGroup的boundingRect()默认返回空矩形,必须手动重写:

// TileItem.cpp QRectF TileItem::boundingRect() const { // 必须返回包含所有子 item 的最小矩形 QRectF rect = m_pixmapItem->boundingRect(); rect = rect.united(m_highlightItem->boundingRect()); // 高亮边框可能更大 return rect.adjusted(-2, -2, 2, 2); // 留 2px 点击容差 } void TileItem::paint(QPainter *painter, const QStyleOptionGraphicsItem *, QWidget *) { // 关键:先画底纹,再画图标,最后画高亮 m_backgroundItem->paint(painter, nullptr, nullptr); m_pixmapItem->paint(painter, nullptr, nullptr); if (m_isHighlighted) { m_highlightItem->paint(painter, nullptr, nullptr); } }

提示:QGraphicsItem::paint()里不要调用update()或scene()->update(),否则会触发无限重绘。高亮状态变更应通过QGraphicsScene::update()指定区域刷新,项目里TileGrid::highlightTile()就是这么做的。


3. 编译与运行:从零开始跑通MatchGame的四步实操清单

别被.rar后缀骗了——这项目不是编译好的二进制,而是完整的 Qt 源码工程。它用的是 Qt 5.15.2(兼容 Qt 6.2+,但需微调信号语法),构建系统是纯 qmake,没有 CMake。以下步骤在 Windows 10/11、macOS 12+、Ubuntu 22.04 均验证通过。

3.1 环境准备:Qt 安装与路径校验

首先确认 Qt 版本。打开终端(Windows 用Qt 5.15.2 for Desktop (MinGW 7.3 64-bit)的命令行):

qmake --version # 应输出:QMake version 3.1, Using Qt version 5.15.2 in ...

如果提示qmake: command not found,说明 Qt 未加入 PATH。Windows 用户请运行C:\Qt\5.15.2\mingw73_64\bin\qtenv2.bat;macOS 用户执行source ~/Qt/5.15.2/clang_64/bin/qt-conf.sh;Linux 用户则需export PATH=~/Qt/5.15.2/gcc_64/bin:$PATH。

注意:项目MatchGame.pro文件里硬编码了QT += widgets gui core,但没写QT += network——这意味着你不能在NetworkManager.cpp里用QNetworkAccessManager,否则链接失败。这是故意为之:所有存档走QSettings,所有资源走QResource,杜绝网络依赖。

3.2 工程配置:qmake 生成 Makefile 并修正路径

进入解压后的根目录(含MatchGame.pro的文件夹):

# 生成 Makefile(Windows) qmake -spec win32-g++ "CONFIG+=release" MatchGame.pro # 生成 Makefile(macOS) qmake -spec macx-clang "CONFIG+=release" MatchGame.pro # 生成 Makefile(Linux) qmake -spec linux-g++ "CONFIG+=release" MatchGame.pro

关键点来了:MatchGame.pro里有一行RESOURCES = resources.qrc,但resources.qrc文件中<file>images/tile_water.png</file>的路径是相对resources.qrc所在目录的。如果你把整个工程移到D:\games\rpg-mahjong\,而resources.qrc在D:\games\rpg-mahjong\resources\下,那么必须手动编辑resources.qrc,把<qresource prefix="/">改成<qresource prefix="/resources">,否则QPixmap(":/images/tile_water.png")会加载失败。

3.3 编译执行:四条命令走完全流程

# 1. 生成 Makefile(已做) # 2. 编译(Windows) mingw32-make -j4 # 2. 编译(macOS/Linux) make -j4 # 3. 运行(Windows) ./release/MatchGame.exe # 3. 运行(macOS) ./release/MatchGame.app/Contents/MacOS/MatchGame # 3. 运行(Linux) ./release/MatchGame

首次运行会生成local.conf(存档)、log.txt(调试日志)、resources/(资源缓存)。如果窗口空白,立刻检查log.txt——90% 的问题是QResource路径错误,日志里会打印QPixmap: Cannot create a QPixmap from null。

3.4 调试技巧:用qInstallMessageHandler抓住 Qt 内部警告

默认 Qt 不输出警告到控制台。在main.cpp的QApplication创建后,插入:

// main.cpp void customMessageHandler(QtMsgType type, const QMessageLogContext &context, const QString &msg) { QByteArray localMsg = msg.toLocal8Bit(); switch (type) { case QtDebugMsg: fprintf(stderr, "[Debug] %s (%s:%u, %s)\n", localMsg.constData(), context.file, context.line, context.function); break; case QtWarningMsg: fprintf(stderr, "[Warning] %s (%s:%u, %s)\n", localMsg.constData(), context.file, context.line, context.function); break; case QtCriticalMsg: fprintf(stderr, "[Critical] %s (%s:%u, %s)\n", localMsg.constData(), context.file, context.line, context.function); break; } } int main(int argc, char *argv[]) { QApplication app(argc, argv); qInstallMessageHandler(customMessageHandler); // 关键! GameWindow window; window.show(); return app.exec(); }

这样,当QGraphicsScene::addItem()传入空指针时,你会在终端看到[Warning] QGraphicsScene::addItem: cannot add null item (qgraphicsscene.cpp:1234, addItem),而不是默默黑屏。

3.5 常见问题排查:五条真实翻车记录与解法

现象 1:点击图标无反应,qDebug()也不输出

原因:TileItem::mousePressEvent()里忘了调用QGraphicsItem::mousePressEvent(event),导致事件未被正确捕获。Qt 的事件传递链是QGraphicsScene → QGraphicsView → QGraphicsItem,缺一不可。
解决:在TileItem::mousePressEvent()末尾加QGraphicsItem::mousePressEvent(event);

现象 2:存档后重启,角色等级变回 1 级

原因:QSettings默认用 INI 格式存储,而QVariantMap中嵌套的QVector<int>会被序列化为字符串"[1,2,3]",读取时QSettings::value("player.skills")返回QString而非QVector<int>。
解决:统一用QSettings::setValue("player/skills", skills)(用斜杠分隔键名),读取时用settings.value("player/skills").value<QVector<int>>(),并确保QVector<int>已注册元类型:qRegisterMetaTypeStreamOperators<QVector<int>>("QVector<int>");

现象 3:macOS 上窗口无法拖动,标题栏显示“无响应”

原因:GameWindow构造函数中调用了setWindowFlags(Qt::FramelessWindowHint),但 macOS 要求 Frameless 窗口必须手动实现mouseMoveEvent来拖动。
解决:删掉setWindowFlags,改用setWindowFlag(Qt::CustomizeWindowHint)+setWindowFlag(Qt::WindowCloseButtonHint)保留原生标题栏。

现象 4:Ubuntu 上字体模糊,中文显示为方块

原因:Qt 5.15 默认用 FontConfig,但某些发行版未安装fonts-liberation。
解决:终端执行sudo apt install fonts-liberation,然后在main.cpp中添加QFont font("Liberation Sans", 10); app.setFont(font);

现象 5:连续快速点击两个图标,程序崩溃在TileMatcher::findPath()

原因:findPath()是递归算法,深度过大触发栈溢出。项目默认最大搜索深度为 20,但“Z 形”路径可能达 30 层。
解决:在findPath()开头加栈深度检查:static thread_local int depth = 0; if (++depth > 25) { depth--; return {}; },退出前depth--。


4. 核心模块代码逐行解析:BattleSystem.cpp如何把连连看变成 RPG 战斗场

RPG 部分不是贴皮,而是重构了整个交互范式。BattleSystem是连接“消除逻辑”和“战斗逻辑”的中枢,它让每一次成功匹配都成为一次攻击指令。下面这段代码,就是BattleSystem::onMatchConfirmed()的完整实现——它只有 47 行,但撑起了全部战斗表现。

// BattleSystem.cpp void BattleSystem::onMatchConfirmed(const QList<TileData> &matchedTiles) { // Step 1: 计算本次匹配的“战力值”——不是简单加总,而是按类型加权 int combatPower = 0; QMap<QString, int> typeCount; for (const TileData &tile : matchedTiles) { typeCount[tile.type]++; // tile.type 是 "fire", "ice", "lightning" 等 combatPower += tile.baseValue * m_combatMultiplier[tile.type]; } // Step 2: 触发技能判定(基于匹配类型组合) QString skillId = determineSkillFromTypes(typeCount); if (!skillId.isEmpty() && m_playerMP >= m_skillCosts[skillId]) { useSkill(skillId, combatPower); return; // 技能已消耗行动,不走普通攻击流程 } // Step 3: 普通攻击——但伤害有浮动区间 int baseDamage = combatPower * 0.8; int minDamage = static_cast<int>(baseDamage * 0.7); int maxDamage = static_cast<int>(baseDamage * 1.3); int finalDamage = QRandomGenerator::global()->bounded(minDamage, maxDamage + 1); // Step 4: 执行伤害,触发 UI 动画 m_enemyHP = qMax(0, m_enemyHP - finalDamage); emit enemyHpChanged(m_enemyHP); emit damageNumberShown(finalDamage, matchedTiles.first().pos); // Step 5: 检查战斗结束 if (m_enemyHP <= 0) { onBattleWin(); return; } // Step 6: 敌人反击(固定概率 30%) if (QRandomGenerator::global()->bounded(100) < 30) { int enemyDamage = QRandomGenerator::global()->bounded(15, 25); m_playerHP = qMax(0, m_playerHP - enemyDamage); emit playerHpChanged(m_playerHP); emit enemyAttackTriggered(enemyDamage); } }

逐行说明与参数含义:

  • Line 5-7:combatPower是本次匹配的“总战力”,但不是sum(tile.value),而是sum(tile.value * multiplier[type])。multiplier定义在BattleSystem::initMultipliers()中,例如"fire"是 1.2,"ice"是 0.9——这解释了为什么火系图标消除更多时,Boss 掉血更快。
  • Line 10-12:determineSkillFromTypes()检查typeCount是否满足技能条件。例如{"fire": 3, "lightning": 2}触发"thunder_flame"技能,而{"ice": 4}触发"frost_nova"。技能列表存在resources/skills.json,此处只是查表。
  • Line 15:useSkill()会扣减 MP、播放技能动画、调用SkillSystem::executeEffect(),然后return阻断普通攻击流程——这是 RPG 战斗节奏的关键:技能有战略价值,不能和普攻混用。
  • Line 20-22:伤害浮动不是简单rand() % 10,而是基于baseDamage的 30% 区间,保证高战力匹配必然高伤,但又保留随机性。QRandomGenerator::global()是 Qt 5.10+ 推荐的线程安全随机数生成器。
  • Line 25:emit damageNumberShown(...)是信号,被BattleView.cpp中的DamageNumberAnimation槽函数接收,创建一个QGraphicsTextItem并用QPropertyAnimation让它向上飞散消失。
  • Line 32-35:敌人反击是固定概率,但伤害值也是浮动区间(15~25),且不与玩家战力挂钩——这是平衡性设计:避免玩家堆战力后敌人反击无效化。

关键设计思想:所有战斗逻辑必须可预测、可复现、可跳过。BattleSystem里没有任何QTimer::singleShot(500, ...)这类异步延迟,所有“等待”都通过QGraphicsItemAnimation的stateChanged信号驱动。这样,当你想加“战斗加速”功能时,只需全局修改QTimeLine::setDuration(),而不用重写三十处delay()。


5. 二次开发实战:如何给 RPG 连连看加一个“合成系统”,三步接入不改主干

现在你已经跑通了基础游戏,下一步往往是加新玩法。项目预留了CraftingSystem的空壳(src/crafting/目录下只有CraftingSystem.h和空实现),但没写具体逻辑。下面我带你用三步把它变成可用的合成系统:点击两个材料图标,自动合成新道具,并更新背包 UI。

5.1 第一步:定义合成规则 JSON,用QJsonArray解析

在resources/下新建crafting_rules.json:

[ { "input": ["ore_iron", "ore_coal"], "output": "ingot_iron", "time_sec": 3, "icon": ":/icons/ingot_iron.png" }, { "input": ["ingot_iron", "gem_sapphire"], "output": "sword_steel", "time_sec": 8, "icon": ":/icons/sword_steel.png" } ]

在CraftingSystem.cpp的loadRules()中解析:

// CraftingSystem.cpp void CraftingSystem::loadRules() { QFile file(":/resources/crafting_rules.json"); if (!file.open(QIODevice::ReadOnly)) { qWarning() << "Failed to load crafting rules"; return; } QJsonParseError error; QJsonDocument doc = QJsonDocument::fromJson(file.readAll(), &error); if (error.error != QJsonParseError::NoError) { qWarning() << "JSON parse error:" << error.errorString(); return; } for (const QJsonValue &ruleVal : doc.array()) { QJsonObject ruleObj = ruleVal.toObject(); CraftingRule rule; rule.input = ruleObj["input"].toArray().toVariantList().toStringList(); rule.output = ruleObj["output"].toString(); rule.timeSec = ruleObj["time_sec"].toInt(); rule.iconPath = ruleObj["icon"].toString(); m_rules.append(rule); } }

5.2 第二步:扩展TileMatcher,增加“合成匹配”模式

TileMatcher原本只处理“消除匹配”,现在要支持“合成匹配”。在TileMatcher.h中加枚举:

// TileMatcher.h enum class MatchMode { kElimination, // 原有消除 kCrafting // 新增合成 };

然后在TileMatcher::findPath()前加判断:

// TileMatcher.cpp QList<QPoint> TileMatcher::findPath(const QPoint &start, const QPoint &end, MatchMode mode) { if (mode == MatchMode::kCrafting) { // 合成匹配只需检查两个图标类型是否在任一 rule.input 中 QString typeA = m_grid->tileAt(start).type; QString typeB = m_grid->tileAt(end).type; for (const CraftingRule &rule : m_craftingRules) { if ((rule.input.contains(typeA) && rule.input.contains(typeB)) || (rule.input.contains(typeB) && rule.input.contains(typeA))) { return {start, end}; // 直接返回两点路径,无需寻路 } } return {}; // 不匹配任何规则 } // 原有消除匹配逻辑... }

5.3 第三步:在GameWindow中注入合成流程,复用现有信号槽

GameWindow已有tileSelected()信号,我们只需在onTileSelected()里加分支:

// GameWindow.cpp void GameWindow::onTileSelected(const QPoint &pos) { if (m_gameState == GameState::kCrafting) { // 新增状态 if (m_firstSelectedPos.isNull()) { m_firstSelectedPos = pos; highlightTile(pos, true); } else { // 尝试合成 bool success = m_craftingSystem->tryCraft(m_firstSelectedPos, pos); if (success) { // 合成成功:移除两个原料图标,添加新道具到背包 m_tileGrid->removeTile(m_firstSelectedPos); m_tileGrid->removeTile(pos); m_inventoryWidget->addItem(m_craftingSystem->lastOutput()); m_statusBar->showMessage("合成成功!", 2000); } else { m_statusBar->showMessage("无法合成,请检查材料", 2000); } m_firstSelectedPos = QPoint(); clearHighlights(); } return; } // 原有消除逻辑... }

关键技巧:所有新功能必须复用现有状态机和信号流。这里没加新信号,而是复用tileSelected();没加新窗口,而是复用m_inventoryWidget;甚至合成动画都用DamageNumberAnimation类,只是把数字换成道具图标。从那以后我每次加功能,都强制走一遍“能否用现有信号/状态/组件实现”的 checklist——这比写一百行新代码更能保住项目的可维护性。

希望帮到你。

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

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

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

立即咨询