简介:这是一份基于Qt框架与C++实现的RPG式连连看桌面游戏课程项目源码,面向已掌握C++基础、希望熟悉Qt界面开发与信号槽机制的高校学生及游戏开发入门者。项目在经典连连看玩法上叠加角色成长设定,通过消除配对获取经验、升级解锁特性,适合作为课程设计参考或Qt练手范例。压缩包共26个文件,约1.99MB,以5个cpp源文件、4个头文件、3个ui界面文件为核心,辅以qrc资源文件、pro工程配置、jpg界面素材以及txt说明与pdf文档,构成一套可直接用Qt Creator打开编译的完整工程。目录中双人对战与信号模式各自独立成模块,便于分块阅读界面布局、事件处理与资源加载的实现思路。目前已有373人学习,对于想弄清QGraphicsView绘图、QTimer计时与qrc资源打包细节的读者,这份工程提供了可对照运行的实例。
1. 这不是普通连连看:Qt 里塞进 RPG 地图后先看什么
把 Qt 当成只会画按钮的框架,容易错过这类包真正有意思的地方:它把连连看棋盘塞进 RPG 地图,角色走位、NPC 触发、关卡切换和消除判定共用一个事件循环。这个资源是一份用 Qt 制作的 RPG 形式连连看源码包,适合想练 Qt Widgets、QGraphicsScene、信号槽和关卡数据驱动的人。
它不是纯算法题,也不是完整商业游戏,胜在把地图探索、棋盘消除、角色属性三条线揉在一个能跑的工程里。想照着复现,先别急着改玩法,先把构建链、场景坐标和关卡入口跑通;这三处不通,后面加技能和道具都是空中楼阁。
2. 拆包与构建:从 .rar 到能跑起来的 Qt Widgets 工程
拿到这种压缩包,最怕一上来双击.pro或直接cmake .。桌面环境不同、Qt 安装方式不同、工作目录不同,三个变量叠在一起,编译错误会像滚雪球一样越滚越大。我一般先把包解到独立目录,确认构建系统,再决定走 qmake 还是 CMake,最后才谈运行和改代码。
2.1 先判断工程用的是 qmake 还是 CMake
Qt Widgets 小游戏常见的工程组织有两类:老一点的项目用.pro,新一点的项目用CMakeLists.txt。这个包标题只说了 Qt,没有给构建系统,所以第一步不是猜,而是扫目录。压缩包解压后先看顶层有没有.pro、CMakeLists.txt、.qrc,这三个文件基本能判断工程骨架:.pro决定 qmake 路径,CMakeLists.txt决定 CMake 路径,.qrc决定地图、图块、关卡数据是不是被编进可执行文件。
mkdir -p "$HOME/work/qt-rpg-link" unrar x "用Qt制作的RPG形式的连连看.rar" "$HOME/work/qt-rpg-link/" find "$HOME/work/qt-rpg-link" -maxdepth 3 -type f \ \( -name "*.pro" -o -name "CMakeLists.txt" -o -name "*.qrc" \) -print逻辑说明:先建一个不带空格、不带中文的独立工作目录,避免 qmake 和 make 在路径转义上出问题;find只扫三层,防止把素材目录里的杂项全打出来。参数说明:maxdepth 3是保守值,多数 Qt 工程的构建文件在顶层或一级子目录;-type f只看文件,不看目录;-name里的三个模式分别对应 qmake、CMake 和 Qt 资源系统。
提示:如果系统没有
unrar,用能解 rar 的归档工具解到同一目录即可,关键是保持目录结构,不要只把源码文件拖出来。
判断出构建系统后,还要看工程是不是纯 Widgets。打开.pro看QT +=行,常见的是QT += core gui widgets;如果是 CMake,看find_package(Qt和target_link_libraries里有没有Widgets。这一步不是形式主义:RPG 地图和棋盘多半依赖QGraphicsView、QGraphicsScene,如果工程混了 QML 或第三方模块,构建命令会多出依赖,后面运行失败时至少知道该查哪一层。
2.2 两条构建路径:qmake 与 CMake 分别怎么跑
有.pro时走 qmake,但不要在压缩包外层目录直接执行。qmake 生成的 Makefile 会把相对路径写死,后面换目录、换构建类型都容易翻车。我一般先定位.pro,进入它所在目录,再用 release 配置构建。可执行文件名不要猜,构建完看TARGET或构建输出。
PRO_FILE=$(find "$HOME/work/qt-rpg-link" -name "*.pro" -print -quit) if [ -z "$PRO_FILE" ]; then echo "没有找到 .pro,改用 CMake 路径" exit 1 fi PRO_DIR=$(dirname "$PRO_FILE") cd "$PRO_DIR" || exit 1 qmake "$(basename "$PRO_FILE")" CONFIG+=release make -j"$(nproc)"逻辑说明:find -print -quit只取第一个.pro,避免多工程时误判;cd到.pro所在目录再执行 qmake,保证相对路径和资源路径按工程预期解析;CONFIG+=release减少调试符号,运行速度更接近实际体验。参数说明:-j"$(nproc)"用当前机器的核数并行编译,内存小的机器可以改成-j2或-j4;如果 qmake 不在 PATH 里,用 Qt 安装目录下 qmake 的绝对路径,不要混用系统自带 qmake 和工程需要的 Qt 版本。
有CMakeLists.txt时走 CMake,核心是源码目录和构建目录分离。源码内构建会生成一堆CMakeCache.txt、CMakeFiles,后面清理麻烦,也不利于对比 release 和 debug。
cd "$HOME/work/qt-rpg-link" BUILD_DIR="$HOME/work/qt-rpg-link/build-release" cmake -S . -B "$BUILD_DIR" -DCMAKE_BUILD_TYPE=Release cmake --build "$BUILD_DIR" -j"$(nproc)"逻辑说明:-S .指定源码根目录,-B指定独立构建目录,CMAKE_BUILD_TYPE=Release控制优化级别;cmake --build不绑定生成器,Makefile、Ninja、Xcode 工程都能用。参数说明:如果 CMake 报找不到 Qt,加-DCMAKE_PREFIX_PATH=/path/to/Qt,这里的路径指向 Qt 安装前缀,不是bin目录,具体以本机安装位置为准;并行数同样按内存调整,编译 RPG 地图资源时内存吃紧就降下来。
注意:qmake 和 CMake 不要在同一源码目录里来回切换。先选一条路跑通,再清掉构建产物换另一条,否则缓存文件会互相干扰,报错信息很玄学。
2.3 运行前检查资源前缀、工作目录和关卡入口
构建通过不等于能跑。Qt 工程常见的运行失败不是编译问题,而是资源路径和工作目录。地图图块、棋盘图案、关卡 JSON 可能被.qrc编进二进制,也可能放在外部目录按相对路径读。两种方式对启动目录的要求完全不同:前者用:/前缀,后者依赖当前工作目录。先看.qrc里注册了什么前缀,再看外部有没有关卡文件,能省掉很多“黑屏但没报错”的排查时间。
cd "$HOME/work/qt-rpg-link" # 查看 qrc 注册前缀,确认地图、图块、关卡数据是 :/ 路径还是外部文件 grep -n "<qresource" -A4 --include="*.qrc" -r . 2>/dev/null || true find . -maxdepth 5 -type f \( -name "*.json" -o -name "*.xml" -o -name "*.ini" \) -print逻辑说明:grep找.qrc里的资源前缀,-A4把前缀和文件列表带出来;find找外部关卡数据,判断运行时是否需要切工作目录。参数说明:--include="*.qrc"只搜资源文件;maxdepth 5覆盖常见data/levels层级;如果grep没输出,可能是 qrc 文件命名不同,回到上一步的find结果里确认。
运行时先在构建输出目录启动,不要从桌面双击。双击的当前工作目录可能是用户主目录,外部关卡和相对路径资源会集体找不到。命令行启动能直接看到 Qt 的警告输出,比如QRC资源缺失、图片格式插件没加载、OpenGL 上下文创建失败。Windows 下如果提示缺 DLL,用 Qt 自带的部署工具补齐运行库;Linux 下用ldd看可执行文件依赖;这些动作放在第一次运行失败之后做,不要提前折腾。
3. Qt 场景里接 RPG 地图与棋盘:坐标、分层与信号槽
RPG 形式连连看最容易写乱的地方,是地图和棋盘抢同一个事件循环。角色要移动,棋盘要响应点击,NPC 要触发对话,倒计时还要走。全塞进一个 QWidget 的mousePressEvent里,短期能跑,长期加一个道具就会互相踩。常见做法是用QGraphicsScene做分层:地形一层、棋盘一层、角色和特效一层,点击先落到场景,再由场景发信号给控制器。
3.1 为什么用 QGraphicsScene 承载地图层
QWidget 自绘也能画地图,但命中测试、图元移动、层叠顺序都要自己维护。QGraphicsScene自带图元管理、坐标变换和zValue层叠,角色走一格、棋盘高亮、飘字特效可以分别挂在不同的QGraphicsItemGroup上。对这类小游戏,性能不是瓶颈,维护边界才是。分层之后,角色移动只动 actor 层,消除只动 board 层,地形层基本不动,排查问题时能快速判断是哪一层画错了。
// mapscene.h 片段:地图层与棋盘层分开,避免角色移动时误触消除 class MapScene : public QGraphicsScene { Q_OBJECT public: explicit MapScene(QObject *parent = nullptr); signals: void playerMovedTo(const QPoint &mapCell); void tileClicked(const QPoint &boardCell); public slots: void loadLevel(const LevelData &level); private: QGraphicsItemGroup *terrainLayer = nullptr; QGraphicsItemGroup *boardLayer = nullptr; QGraphicsItemGroup *actorLayer = nullptr; QPoint mapOrigin{0, 0}; // 棋盘左上角在地图场景中的格坐标 int cellSize = 48; // 一个格子的像素边长,按素材改 };逻辑说明:场景只负责“显示”和“发事件”,不直接改棋盘数据;playerMovedTo和tileClicked把两类输入分开,控制器可以根据当前状态决定是否响应。参数说明:mapOrigin是棋盘在地图中的偏移,改成别的值就能把棋盘挪到不同房间;cellSize必须和素材像素对齐,否则点击会偏半格;三个层组用zValue控制前后关系,角色层通常高于棋盘层,特效层再高一层。
3.2 地图格、棋盘格、屏幕坐标的三层映射
这类项目翻车最多的地方是坐标。地图有自己的行列,棋盘有自己的行列,屏幕还有像素坐标。三层混着用,表现就是“点 A 消 B”“角色走到墙里”“高 DPI 下点击偏移”。我一般只保留两个转换函数:地图格到场景像素,场景像素到棋盘格。所有点击先转成场景坐标,再减棋盘原点,最后才进消除判定。
QPoint MapScene::mapCellToScene(const QPoint &cell) const { return QPoint(cell.x() * cellSize, cell.y() * cellSize); } QPoint MapScene::sceneToBoardCell(const QPointF &scenePos) const { const QPoint mapCell(qFloor(scenePos.x() / cellSize), qFloor(scenePos.y() / cellSize)); return mapCell - mapOrigin; // 负值说明点到棋盘外 }逻辑说明:qFloor不能用整数强转替代,负坐标或高 DPI 小数坐标下强转会往零截断,导致边缘格判定错位;减mapOrigin把地图格转成棋盘格,返回值小于零或超过棋盘行列就丢弃。参数说明:cellSize同时影响绘制和命中,改素材尺寸时两处必须一起改;scenePos来自mapFromScene之后的场景坐标,不要直接拿 QWidget 的鼠标坐标。
提示:高 DPI 屏幕上先确认
QGraphicsView的变换和场景矩形,再调cellSize。点击偏移不一定是算法错,可能只是视图缩放没对齐。
3.3 信号槽解耦:角色移动不直接改棋盘
角色移动和棋盘消除应该由控制器协调,而不是场景里互相持有指针。场景发“角色到了哪一格”,控制器判断这一格是不是 NPC、是不是宝箱、是不是棋盘触发点;棋盘发“点了哪个格”,控制器判断能不能连、要不要扣回合。这样加新玩法时只改控制器,场景和棋盘视图不用动。
connect(scene, &MapScene::playerMovedTo, controller, &GameController::onPlayerMoved); connect(scene, &MapScene::tileClicked, controller, &GameController::onTileClicked); connect(controller, &GameController::boardChanged, boardView, &BoardView::refresh);逻辑说明:场景到控制器是输入信号,控制器到视图是状态刷新信号;棋盘视图不直接读控制器内部容器,只按boardChanged重绘。参数说明:默认Qt::AutoConnection在同线程下是直连,游戏逻辑一般都在主线程,够用;如果以后把寻路放到工作线程,跨线程连接要显式用Qt::QueuedConnection,并且不要在槽里碰视图对象。
3.4 用 QTimer 管回合与倒计时
RPG 元素一进来,时间就不再只是连连看的倒计时,还可能是角色行动点、NPC 刷新、buff 持续回合。不要用sleep或忙循环,QTimer是 Qt 事件循环里最稳的做法。倒计时建议存“剩余秒数”,不要存绝对时间差;系统时间一跳,绝对时间差会瞬间扣完或者卡住。
QTimer *turnTimer = new QTimer(this); turnTimer->setInterval(1000); connect(turnTimer, &QTimer::timeout, controller, &GameController::tickTurn); turnTimer->start();逻辑说明:每秒触发一次tickTurn,控制器里扣剩余秒数、推进 buff 回合、检查失败条件;定时器归控制器或主窗口管理,不要挂在临时对象上。参数说明:setInterval(1000)是秒级回合,想要更细的动画可以另开一个 16ms 渲染定时器,但规则计时不要跟着渲染帧走;暂停时stop(),恢复时重新start(),不要用标志位空转。
4. Qt 消除逻辑拆解:路径判定、道具与角色属性怎么挂钩
连连看的核心不是图案,而是“两个相同图块能不能用不超过两次拐弯的路径连起来”。RPG 形式又加了一层:角色属性可能影响得分、障碍破坏、回合数,甚至影响某些图块能不能点。这里最容易犯的错,是把属性判断塞进路径判定里,导致算法既算连通又算数值,后面加道具就改不动。我的做法是路径判定只回答“通不通”,属性和道具在控制器外层做门槛和结算。
4.1 连通判定的三种写法与选型
常见写法有三种:递归 DFS 按拐弯数展开、BFS 按状态搜最少拐弯、直线预判加分情况枚举。递归写起来短,但棋盘大时栈深和重复访问不好控;分情况枚举要对直连、一折、两折分别写,边界容易漏;BFS 把“格子 + 进入方向 + 拐弯数”作为状态,逻辑统一,调试时也能打印状态。这个项目棋盘不会太大,BFS 的可读性更值钱。
bool GameController::canLink(const QPoint &a, const QPoint &b) const { if (a == b) return false; if (!board.isInside(a) || !board.isInside(b)) return false; if (board.at(a) == Empty || board.at(a) != board.at(b)) return false; const int W = board.width() + 2, H = board.height() + 2; auto idx = [W](int x, int y) { return y * W + x; }; // 含一圈外边界 std::vector<int> best(W * H, 4); // 到该格最少拐弯数,4 表示不可达 struct Node { int x, y, dir, turns; }; std::queue<Node> q; const int dx[4] = {1, -1, 0, 0}; const int dy[4] = {0, 0, 1, -1}; for (int d = 0; d < 4; ++d) { int nx = a.x() + 1 + dx[d], ny = a.y() + 1 + dy[d]; if (nx < 0 || nx >= W || ny < 0 || ny >= H) continue; if (board.at(nx - 1, ny - 1) != Empty && QPoint(nx - 1, ny - 1) != b) continue; q.push({nx, ny, d, 0}); best[idx(nx, ny)] = 0; } while (!q.empty()) { Node cur = q.front(); q.pop(); if (cur.x == b.x() + 1 && cur.y == b.y() + 1) return true; for (int d = 0; d < 4; ++d) { int nx = cur.x + dx[d], ny = cur.y + dy[d]; if (nx < 0 || nx >= W || ny < 0 || ny >= H) continue; int px = nx - 1, py = ny - 1; if (board.at(px, py) != Empty && QPoint(px, py) != b) continue; int turns = cur.turns + (d != cur.dir ? 1 : 0); if (turns > 2) continue; if (best[idx(nx, ny)] <= turns) continue; best[idx(nx, ny)] = turns; q.push({nx, ny, d, turns}); } } return false; }逻辑说明:棋盘外扩一圈虚拟空格,让路径可以绕边界走;best记录到每个格子的最少拐弯数,同一格用更少拐弯到达才继续扩展;目标格允许通过,其他非空格不允许通过。参数说明:turns > 2是标准连连看上限,改成 3 就变成更宽松的玩法;board.at(x, y)要能把越界坐标当Empty处理,否则外扩边界会越界;起点四个方向先入队,避免把起点本身算成拐弯。
4.2 把角色属性接进消除规则
属性不要写死在canLink里。路径判定只回答连通,得分、障碍门槛、连击加成放在clearScore和canClearBlockedTile这类函数里。这样同一套路径算法可以服务普通模式、挑战模式、剧情模式,改数值不用碰算法。
int GameController::clearScore(const QPoint &a, const QPoint &b) const { int base = 100; // 角色感知越高,同色配对额外加成;力量影响障碍门槛 int bonus = player.perception * 5; if (player.hasTalent("连击")) bonus += combo * 20; return base + bonus; } bool GameController::canClearBlockedTile(const QPoint &cell) const { return player.strength >= level.blockStrength(cell); }逻辑说明:clearScore只算分,不改棋盘;canClearBlockedTile只做门槛判断,真正移除图块仍由控制器统一提交。参数说明:base是基础分,perception和strength字段必须和关卡 JSON 对齐;blockStrength是格子门槛,不是全局常量,不同关卡可以不同;天赋字符串建议集中定义,不要散落在多个文件里。
4.3 关卡 JSON 字段与存档边界
关卡数据用 JSON 最省事,字段少、可读、改起来不用重编译。关键是字段职责要清楚:地图尺寸归map,棋盘位置和行列归board,角色初始属性归player,规则开关归rules。存档只存“变化量”,比如当前关卡、角色属性、已消除格子、剩余回合,不要把整张地图快照反复写进存档,版本一改就对不上。
{ "levelId": "rpg_link_01", "map": { "rows": 12, "cols": 16, "tileSize": 48 }, "board": { "origin": [4, 3], "rows": 8, "cols": 10, "types": 12 }, "player": { "hp": 100, "strength": 3, "perception": 2, "movePoints": 6 }, "rules": { "allowTurns": 2, "timeLimitSec": 180, "blockStrength": 5 } }逻辑说明:origin是棋盘左上角在地图格中的位置,和MapScene::mapOrigin对应;tileSize和cellSize对应;types控制图案种类,种类越多越难凑对;blockStrength和角色力量做门槛比较。参数说明:timeLimitSec是秒级倒计时,不要和回合数混用;movePoints是角色每回合可走格数,和连连看倒计时是两套资源,扣哪个由控制器决定。
| 字段 | 作用 | 常见误用 |
|---|---|---|
board.origin | 棋盘在地图中的偏移 | 直接写屏幕像素,换分辨率就错位 |
board.types | 图案种类数 | 超过素材数量,运行时报缺图 |
player.strength | 障碍门槛 | 和blockStrength单位不一致 |
rules.allowTurns | 路径拐弯上限 | 和算法里的硬编码 2 冲突 |
4.4 道具和技能不要直接改棋盘容器
道具一多,最怕“炸弹”“锤子”“刷新”各自直接改棋盘数组。短期能跑,撤销和回放就变成黑匣子。常见做法是让道具提交命令,控制器统一改棋盘并记录历史。这样“后悔药”只需要回滚快照,自动化回放也能按命令序列重放。
// 道具只提交命令,由控制器统一改棋盘,方便撤销和回放 struct ClearPairCommand { QPoint a, b; }; void GameController::apply(const ClearPairCommand &cmd) { history.push(board.snapshot()); board.take(cmd.a); board.take(cmd.b); emit boardChanged(); }逻辑说明:命令对象只描述“要做什么”,不直接碰视图;控制器在apply里先存快照,再改数据,最后发刷新信号。参数说明:history要有上限,不然长时间游玩内存会涨;快照可以只存被改的格子,不必整盘复制;回放时按命令序列执行,不要依赖鼠标事件。
5. 避坑与排查:RPG 连连看最容易翻车的五处
这类 Qt 小游戏包的血泪经验是:编译错误反而好查,运行时的“看起来没报错”最磨人。下面五处是我拆类似工程时反复遇到的,按现象、原因、解决拆开写,照着顺序排查,基本能少走一半弯路。
5.1 现象:编译通过但运行黑屏
现象:程序启动后窗口出现,地图和棋盘全是黑的,控制台没有明显错误。原因通常是资源前缀不对或工作目录不对,QPixmap加载失败但代码没检查isNull();也可能是场景矩形没设置,图元画在可视区域外。解决:先在启动时打印关键资源路径和加载结果,再从构建输出目录运行,确认.qrc前缀和外部关卡路径。把scene->setSceneRect和视图fitInView的调用顺序检查一遍,别让图元画到场景外。
# 从构建输出目录启动,观察 Qt 的资源警告 cd "$HOME/work/qt-rpg-link/build-release" 2>/dev/null || cd "$HOME/work/qt-rpg-link" ./可执行文件名 2>&1 | tee run.log逻辑说明:把标准错误和标准输出一起落到日志,Qt 的资源警告、图片插件警告都会出现;先切构建目录,保证相对路径按预期解析。参数说明:可执行文件名按构建输出替换,不要照抄;tee只是留痕,不影响程序运行。解决阶段优先修资源路径,不要先改绘制代码。
5.2 现象:消除后棋盘不刷新或残影
现象:两个图块逻辑上已经消掉,画面上还留着,或者下一次点击命中已消失的格子。原因多半是数据层改了但视图没收到刷新信号,或者图元删了但命中容器没同步。解决:让棋盘数据只通过一个入口修改,修改后统一发boardChanged;视图刷新时按数据重建图元,不要只改颜色。命中测试以数据层为准,不要以图元是否存在为准。
注意:不要在图元点击回调里直接
delete图元再改数组,顺序一乱就会出现悬空指针或重复删除。
5.3 现象:切换关卡后角色状态丢失
现象:第一关角色属性正常,切到第二关后力量、感知、天赋全回到默认值,或者血量继承了上一关的临时 buff。原因通常是关卡加载时重新构造了角色对象,存档字段和关卡字段混在一起。解决:把“角色基础属性”“关卡临时状态”“存档持久状态”分成三层,读关卡时只重置临时状态,持久状态从存档恢复;角色对象不要在切关时重建,改为重置字段。
5.4 现象:路径判定偶发误判
现象:明明能连的两个格子偶尔判不通,或者隔墙也能连。原因常见于边界处理不一致:有的地方把越界当墙,有的地方当空格;qFloor和整数强转混用;目标格在 BFS 里被提前挡掉。解决:统一board.at的越界语义,路径判定只认一种坐标体系;把canLink的输入限制在棋盘内;给 BFS 加调试输出,打印起点、终点、拐弯数和路径,别靠肉眼猜。
5.5 现象:打包后资源找不到
现象:开发目录能跑,换一台机器或换一个目录就缺图、缺关卡。原因通常是资源只存在于源码目录,没有进.qrc,或者运行时依赖当前工作目录。解决:需要随二进制走的图块和关卡编进.qrc;需要外部可改的关卡保留相对路径,并在启动时用QCoreApplication::applicationDirPath()拼绝对路径,不要依赖用户从哪里双击。部署时把 Qt 运行库补齐,再在干净目录跑一遍。
6. 进阶验证:用回放脚本和耗时打点卡住性能瓶颈
改完玩法后,我一般不复肉眼点一局就收工,而是录一组操作序列做回放:固定关卡、固定随机种子、固定点击顺序,断言消除数量、剩余回合、得分。这样改路径判定或属性公式时,能立刻知道有没有把旧关卡打穿。回放不需要复杂框架,一个 JSON 操作列表加一个控制器入口就够。
// 回放入口:按操作序列驱动控制器,不经过真实鼠标事件 void GameController::replay(const QVector<QPoint> &clicks) { for (const QPoint &cell : clicks) { onTileClicked(cell); if (isLevelFinished() || isFailed()) break; } }逻辑说明:回放绕过视图层,直接调用控制器,能稳定复现规则问题;每次点击后检查结束条件,避免无效操作污染结果。参数说明:clicks的顺序必须和录制时一致;随机种子要在加载关卡前固定,否则图案分布一变,断言全废。
卡顿用QElapsedTimer打点,不要凭感觉。重点看三个位置:关卡加载、棋盘刷新、路径判定批量调用。把耗时打到日志里,超过阈值再查图元数量和图集大小。
| 打点位置 | 观察值 | 常见瓶颈 |
|---|---|---|
| 关卡加载 | 首次加载毫秒数 | 大图未缩放、JSON 重复解析 |
| 棋盘刷新 | 单次刷新毫秒数 | 每次重建全部图元 |
| 路径判定 | 单次判定微秒数 | BFS 重复扩展、越界判断过多 |
具体技巧是给地图格和棋盘格做双索引:地图
本文还有配套的精品资源,点击获取