用Qt和C++复刻FC坦克大战:从QGraphicsView到碰撞检测实践
简介一款基于 Qt 与 C 开发的 FC 坦克大战复刻项目面向具备基础 C 语法、希望上手 Qt 游戏开发的读者。项目界面简洁还原度高实现了地图碰撞、敌方坦克 AI、子弹发射、基地防守等坦克大战的核心玩法代码逻辑清晰注释详细工程内对游戏场景、坦克角色、子弹对象等模块做了明确划分便于按模块学习或二次扩展。压缩包采用 zip 格式大小 4.72MB目前资源文件明细暂未提供因此内部文件数量与类型清单暂无法列出。已有 1729 人浏览该资源适合用于 Qt 图形界面编程练习、经典游戏逻辑复现以及课程设计参考。通过源码可以拆解窗口渲染、事件循环、碰撞检测、状态管理等关键实现也能够在此基础上继续加入音效、关卡编辑或双人模式让项目更加完整。1. 用 Qt 和 C 复刻 FC 坦克大战为什么值得亲手做一遍QT FC坦克大战C这个标题说白了就是拿 Qt 框架配合 C 把红白机上的坦克大战在 PC 窗口里重新实现一遍。第一次听到这个选题很多人第一反应是“一辆坦克、一个基地能有多难”真把它拆成代码需求时你要同时处理键盘事件、帧刷新、子弹移动、碰撞检测、敌人 AI、关卡数据、资源加载。这正好是 Qt 开发者最容易缺的一段会写对话框、会连数据库但没有把 QGraphicsView 的 item 体系用完整过。这个项目对三类人合适刚学 Qt 想要完成度高的练手C 基础还行但从没写过完整 2D 游戏课程设计或面试需要拿得出手的工程。下面按实现顺序拆先选渲染框架再写坦克与子弹接着做地图资源最后聊踩坑和验证。2. 把 FC 坦克大战拆成 Qt 能懂的对象QGraphicsView 选型与最小场景搭建先别急着写坦克类。整个游戏能不能长期迭代取决于第一层画布选什么。Qt 里做 2D 游戏最常见的两条路是 QWidget 自绘和 QGraphicsView/QGraphicsScene。前者适合做控件和轻量动画后者是 Qt 为大量可交互图元准备的 2D 场景框架。2.1 为什么把渲染层放在 QGraphicsView 而不是 QWidget 绘图直接对比一下两种做法方案优点代价QWidget::paintEvent拿到 QPainter 就能画逻辑路径短所有图元的状态、碰撞、事件都要自己维护QGraphicsView QGraphicsScene内置图元树、碰撞检测、事件分发、坐标变换概念多上手时要多理解几个类坦克大战的地图是几十个瓦片坦克和子弹是飞快移动的对象天然适合用“场景 图元”来描述。如果用 QWidget 自绘每个 tick 你都要遍历所有对象、判断碰撞、按顺序绘制这些工作 QGraphicsScene 已经做掉了。还有一个原因是 QGraphicsItem 自带 setPos、moveBy、rotate子弹和坦克只需要更新坐标不需要每次刷新重算像素。如果你以前主要用 Qt Designer 界面设计拖控件写表单第一次接触 QGraphicsView 会不习惯控件是 QWidget 树图元是 QGraphicsItem 树但图元树里的对象更像游戏实体。坦克、砖墙、子弹都继承 QGraphicsItem各自管自己的绘制和形状scene 负责把它们组织起来。这个模型和 FC 坦克大战的实体划分几乎一一对应。2.2 最小工程场景、视图、一帧可动方块先跑通最小的场景再往里面填坦克。下面这段代码创建场景、视图并放一个黄色方块模拟坦克#include QApplication #include QGraphicsScene #include QGraphicsView #include QGraphicsRectItem int main(int argc, char *argv[]) { QApplication app(argc, argv); // 场景尺寸对应地图逻辑网格26*24 宽、26*24 高 QGraphicsScene scene; scene.setSceneRect(0, 0, 26 * 24, 26 * 24); // 一个临时坦克22*22留 2 像素间隙便于看边界 QGraphicsRectItem tank(nullptr); tank.setRect(0, 0, 22, 22); tank.setBrush(Qt::yellow); tank.setPos(scene.sceneRect().center()); scene.addItem(tank); QGraphicsView view(scene); view.resize(800, 640); view.show(); return app.exec(); }这段代码把 QGraphicsScene 当作“世界坐标系”QGraphicsView 只是看这个世界的视口。scene.setSceneRect设置世界的边界之后坦克的移动范围都按这个矩形约束。注意视图大小 800×640 不代表场景大小它只是窗口尺寸场景里的内容如果超出视口可以通过滚动或缩放查看。QGraphicsRectItem是矩形图元坐标是局部坐标setPos把它放到场景坐标的某个点这个“局部/全局”分离是后面所有碰撞的基础。我一般会在这里就引入 QTimer否则画面是死的。提示如果你在 Qt Creator 里用的是 QMainWindow 模板记得把 view 放进 centralWidget否则窗口里看不到场景内容。2.3 主循环还是事件循环QTimer 驱动 60 FPS 的取舍游戏需要持续刷新但 C 语言风格里while (1)死循环在 Qt 里不能直接写它会卡死事件循环按钮、键盘、重绘全都断掉。常见做法是用 QTimer 周期触发更新把逻辑放进 timeout 回调#include QTimer QTimer timer; QObject::connect(timer, QTimer::timeout, []() { // 每帧驱动坦克和子弹的 tick() tank.moveBy(0.5, 0); // 临时演示每帧向右挪半像素 }); timer.start(1000 / 60);1000 / 60是 16 毫秒一帧对应大约 60 FPS。注意 QTimer 不是硬实时定时器Windows 上系统定时器分辨率通常到 15 毫秒左右所以这个值是一个“近似 60 帧”的目标。坦克大战是像素级移动不需要做帧间插值如果以后做赛车游戏再用 QElapsedTimer 做修正。子弹和 AI 也可以用独立 QTimer 降频。比如敌人 AI 不需要每帧都换方向用一个aiTimer.start(50)每 50 毫秒跑一次 AI 逻辑整体帧率压力更小。这里的原则是刷新率管玩家手感AI 频率管行为合理性两者不要混在同一个循环里。2.4 用 qmake 还是 CMake 拉起这个最小工程Qt Creator 里最简单的是 Qt Widgets Application 模板生成的 .pro。最小内容QT widgets CONFIG c17 TEMPLATE app TARGET TankBattle SOURCES main.cpp RESOURCES res.qrcqmake 能直接从 .pro 识别 Qt 模块、源文件、资源文件新建项目时选择构建套件MinGW 或 MSVC即可。如果团队习惯 CMake也可以把场景代码放进find_package(Qt5 COMPONENTS Widgets)和target_link_libraries(TankBattle PRIVATE Qt5::Widgets)但坦克大战这种中小项目qmake 的学习成本更低。注意 .pro 里QT widgets必须写漏掉它会导致找不到 QGraphicsView 头文件。另外用 Qt Creator 的 kit 选择器时MinGW 和 MSVC 两套 kit 不要混用编译目录分别清理。3. 坦克大战的核心动作坦克移动、开炮命中与碰撞检测的 C 实现场景跑通后把黄色方块换成真正的坦克类。坦克是游戏里唯一直接受玩家控制的对象它要能响应键盘、沿四个方向移动、开炮还要被边界和墙壁挡住。3.1 TankItem封装键盘响应与速度边界TankItem 继承 QGraphicsObject而不是 QGraphicsRectItem因为 QGraphicsObject 支持信号槽paint 也完全由自己控制。先定义基本骨架class TankItem : public QGraphicsObject { Q_OBJECT public: enum Direction { Up, Down, Left, Right }; explicit TankItem(QGraphicsScene *scene); QRectF boundingRect() const override; void paint(QPainter *painter, const QStyleOptionGraphicsItem *option, QWidget *widget) override; void tick(); // 由 QTimer 每帧调用一次 void clearKeys(); bool isKeyDown(int key) const; private: Direction m_dir Up; qreal m_step 2.0; // 每帧移动像素数 };实现文件里paint 先用 QPainter 画一个矩形坦克再用炮管方向区分朝向。boundingRect 返回整个坦克占用的矩形QGraphicsView 用它决定哪些区域需要重绘。tick 做移动和边界钳制void TankItem::tick() { QPointF pos this-pos(); if (m_dir Up) pos.ry() - m_step; else if (m_dir Down) pos.ry() m_step; else if (m_dir Left) pos.rx() - m_step; else if (m_dir Right) pos.rx() m_step; QRectF bounds scene()-sceneRect(); if (pos.x() bounds.left()) pos.setX(bounds.left()); if (pos.y() bounds.top()) pos.setY(bounds.top()); if (pos.x() boundingRect().width() bounds.right()) pos.setX(bounds.right() - boundingRect().width()); if (pos.y() boundingRect().height() bounds.bottom()) pos.setY(bounds.bottom() - boundingRect().height()); setPos(pos); }这里没有直接调用moveBy而是先算出新坐标再钳制避免坦克一半开出边界。m_step是速度参数调大游戏变快调小更精细2.0 在 60 FPS 下大约每秒 120 像素适合 24 像素一格的地图。注意boundingRect返回的是局部坐标矩形而pos是场景坐标所以边界计算要把两者相加。键盘响应不写在 TankItem 里而是写在 QGraphicsView 子类中这样焦点问题最少class GameView : public QGraphicsView { public: explicit GameView(QGraphicsScene *scene) : QGraphicsView(scene) { setFocusPolicy(Qt::StrongFocus); } void keyPressEvent(QKeyEvent *e) override { m_keys.insert(e-key()); QGraphicsView::keyPressEvent(e); } void keyReleaseEvent(QKeyEvent *e) override { m_keys.remove(e-key()); QGraphicsView::keyReleaseEvent(e); } bool isKeyDown(int key) const { return m_keys.contains(key); } void clearKeys() { m_keys.clear(); } private: QSetint m_keys; };setFocusPolicy(Qt::StrongFocus)是关键否则键盘事件可能直接落到父窗口。每次按键按下把键码插入 QSet抬起时移除。主循环里每帧查询isKeyDown决定方向而不是在 keyPressEvent 里移动。这样键按住时坦克持续走不会出现按一下走一步的情况。松开键后如果还有方向键按住坦克会马上按剩余方向继续走这是老玩家习惯的手感。3.2 子弹与碰撞检测用 scene 的 collidingItems 代替手写矩形相交子弹本质也是一个 QGraphicsObject只是它没有键盘输入只有方向、速度和伤害。子弹每帧往前移动一次然后调用QGraphicsScene::collidingItems问场景“我压到了谁”。这是 Qt 内置的空间检测比手写矩形相交省很多事。void BulletItem::tick() { setPos(pos() m_dirUnit * m_speed); // 返回与当前 item 形状相交的所有图元 QListQGraphicsItem * hits scene()-collidingItems(this); for (QGraphicsItem *hit : hits) { if (hit m_owner) continue; // 不撞发射者 if (hit-data(0) Wall) { // 砖墙 scene()-removeItem(hit); delete hit; delete this; // 子弹也消失 return; // 防止继续用已释放的 this } if (hit-data(0) Enemy) { // 敌人扣血或直接删除这里略过 delete this; return; } if (hit-data(0) Base) { // 基地被击中通知主界面游戏结束 delete this; return; } } }collidingItems默认按 item 的 shape() 做相交判断。矩形子弹和矩形砖块没问题如果以后做圆形坦克图案重写 shape() 返回 QPainterPath 圆形碰撞就更精确。使用data(0)给图元打标签是常见做法避免写一堆qgraphicsitem_cast。这里有个后悔药值得记删除图元时先scene()-removeItem(hit)再delete hit不要直接 delete。直接 delete 在 scene 的索引里可能留下悬空指针下一次碰撞或遍历就崩。代码里delete this后必须立刻return因为 hits 列表此时可能包含已失效指针继续遍历属于未定义行为。子弹速度也是碰撞手感的关键。m_speed 5.0在 60 FPS 下单帧移动 5 像素如果速度超过砖块尺寸很多子弹可能一帧内穿过砖块出现“穿墙”。砖块 24 像素时5 像素是安全的。想调到 8 以上就要做子弹轨迹检测例如每帧分成几步走每步都检测。3.3 敌人 AI 的简单写法随机转向 间隔射击不用状态机也能跑坦克大战的敌人不需要聪明只要会沿当前方向走、碰到障碍转向、隔一段时间开炮玩家就会觉得“有压力”。给 EnemyTank 一个 tickvoid EnemyTank::tick() { // 撞到墙或边界就走不了立刻换个方向 if (!canMoveForward()) { m_dir static_castDirection( QRandomGenerator::global()-bounded(4)); } // 每 800ms 开一炮制造节奏压力 if (m_fireTimer.elapsed() 800) { fire(); m_fireTimer.restart(); } moveOneStep(); }随机数这里用QRandomGenerator::global()-bounded(4)不用rand() % 4。Qt 的随机数生成器是线程安全的也省去到处传 seed 的麻烦bounded(4)得到 0 到 3 的整数正好映射四个方向。C 新人容易在这里踩坑先srand(time(0))再rand()在 Qt 事件循环里看起来没问题但一到多窗口或多线程就乱套。canMoveForward()的实现可以复用 3.1 的边界钳制再加一层墙面判断预计算前进一格后的矩形和场景里的 Wall item 做collidingItems有 Wall 就返回 false。和玩家坦克碰撞也一样两辆敌方坦克互相顶住时各自动换向这个效果配合场景碰撞检测很容易做。注意 AI 的转向频率要加一点“耐心”不要每帧都随机换向否则敌人会原地抽搐常见做法是记录本次方向至少保持 400ms 再允许更换。4. 关卡地图的数据结构和资源加载从 13×13 数组到可换关卡坦克大战的核心玩法是“守住基地 清光敌人”所以地图不只是背景它是和子弹实时交互的障碍。地图数据结构选得好关卡切换就是一次数组替换。4.1 瓦片地图用二维数组还是 QVector最简单可靠的是二维数组但要可换关卡用QVectorQVectorint更顺手// 0 空地, 1 砖墙, 2 钢墙, 3 水, 4 树, 5 基地 QVectorQVectorint map { {1, 1, 0, 0, 3, 0, 0, 0, 0, 3, 0, 0, 1}, {1, 1, 0, 0, 3, 0, 0, 0, 0, 3, 0, 0, 1}, // ... 共 13 行 };为什么用 QVector 而不是 C 数组地图尺寸在运行时可能变化QVector 能在加载关卡时替换整个二维结构遍历时map[row][col]的写法和数组一样但内存和拷贝由 Qt 管理。实际项目中我习惯用 13×13 的粗数组表达关卡布局渲染时每格放大到 24×24。原版 FC 的砖块更细复刻时可以在粗格内部再拆成 2×2 半砖这样子弹打掉半格的效果更还原但初版先用整格也能玩。把数组转成图元的代码for (int row 0; row map.size(); row) { for (int col 0; col map[row].size(); col) { int type map[row][col]; if (type 0) continue; auto *tile new QGraphicsRectItem(0, 0, 24, 24); tile-setPos(col * 24, row * 24); tile-setBrush(wallBrush(type)); tile-setData(0, Wall); tile-setData(1, type 2); // 钢墙不可被普通子弹打穿 scene()-addItem(tile); } }wallBrush(type)按类型返回颜色或图片砖墙黄色、钢墙银色、水蓝色、树绿色。这里每个砖块都是独立 item好处是被击中后能只删一格坏处是瓦片数量多了以后 scene 要管理的对象多但坦克大战 26×26 甚至 52×52 都远不到性能瓶颈不需要做瓦片合并。数据存type 2的钢墙属性碰撞逻辑里先判断钢墙就不删 item只删子弹。4.2 资源文件的路径陷阱qrc 和相对路径到底该用哪个很多人的第一个翻车点是图片加载不出来。Qt Creator 里直接写images/wall.png运行时当前目录是构建目录不是源码目录所以路径找不到。处理这个问题资源系统 .qrc 是正道RCC qresource prefix/ file aliaswall.pngassets/wall.png/file file aliassteel.pngassets/steel.png/file file aliastank_up.pngassets/tank_up.png/file /qresource /RCC在 .pro 文件里添加RESOURCES res.qrc或者在 CMake 里set(RESOURCES res.qrc)。代码里加载时用:/wall.pngQPixmap pix(:/wall.png); if (pix.isNull()) { // 资源没编进去检查 .qrc 的路径和 alias 是否写错 }注意 .qrc 文件里的路径是相对 .qrc 文件所在目录的不是相对源码根目录alias 只是给你在代码里用的名字。如果你之前主要用 Qt Designer 界面设计在设计器里添加过资源它生成的也是同一个 .qrc 机制两种入口共用一套规则。发布时资源已经编进二进制不需要额外拷贝图片文件夹这也是 qrc 比相对路径省心的原因。图片很多时.qrc会变得很长可以在qresource里用fileassets/wall.png/file多个 file还支持目录批量加入但初版手写几个就够了。顺便说一句如果要用 Windows 下最常见的 Microsoft Visual C 编译工具链不要以为相对路径能靠编译器“自动找到”编译器只管代码不管运行路径。踩过一次之后我所有游戏资源都走 qrc没有再遇到黑图。4.3 爆炸、音效与 UI 分数把 QGraphicsTextItem 当 HUD 用地图之上还要有 HUD。分数、剩余敌人数、关卡号直接加 QGraphicsTextItem 进 scene比单独做一个窗口覆盖层简单auto *scoreText new QGraphicsTextItem(QStringLiteral(SCORE: 0)); scoreText-setPos(20, 20); scoreText-setZValue(100); // 高于所有瓦片和坦克 scene()-addItem(scoreText);更新时调用scoreText-setPlainText(...)不用担心重绘顺序Z 值控制遮挡。爆炸动画也是常见需求我不建议用一堆 if 手写帧计数。简单做法是给爆炸动画单独一个 QTimer每 100ms 换一张 QPixmap连续换 4 次后删除自己QTimer *animTimer new QTimer(this); animTimer-setInterval(100); connect(animTimer, QTimer::timeout, this, []() { m_frame; if (m_frame 4) { animTimer-stop(); scene()-removeItem(this); delete this; return; } setPixmap(m_frames[m_frame]); }); animTimer-start();注意这里this是 QGraphicsPixmapItemlambda 捕获this后在动画结束时会删除自己删除前先 stop 定时器避免定时器再触发一次。这个模式在坦克大战里到处都会用到子弹消失、敌人重生、基地爆炸本质都是“延迟执行的临时对象”。先把 HUD 和爆炸做出来游戏就有完成度的味道了。5. 坦克大战排查手记Qt 编译、乱码、按键冲突与版本不一致的 5 个坑复刻坦克大战的过程中真正耗时间的往往不是游戏逻辑而是环境问题。下面几条是最常碰到的按现象、原因、解决列出。5.1 现象cannot mix incompatible Qt library (version ex50601) with this librar现象启动时弹窗或终端输出fatal: cannot mix incompatible Qt library (version ex50601) with this librar...程序直接退出。原因编译程序时用的 Qt 头文件版本和运行时加载的 Qt DLL 版本不是同一套。最常见的场景机器上装了 Qt 5.15.2 和 Qt 6PATH 里后者的 bin 目录排在前面运行 5.15.2 编译出来的 exe 就去加载了 Qt 6 的 Qt6Core.dll。解决打开命令行工具运行qmake -v看当前 PATH 里的 qmake 是哪个版本在 Qt Creator 里重建项目前先清空 build 目录并执行 qmake确保编译和运行用的是同一个 Qt 套件如果用 VSCode 配置 C/C 环境手动编译特别注意 include 路径和 DLL 路径不要跨版本混用。还有一个小概率是 Debug 程序用了 Release 的 DLLQt 的 Debug/Release 后缀不同但混用时同样报这个错统一成相同构建类型就好。5.2 现象qt.qpa.plugin: could not find the qt platform plugin linuxfb in ...现象在嵌入式设备或 Linux 无显示环境里跑 Qt 程序经常一启动就报qt.qpa.plugin: could not find the qt platform plugin linuxfb in ...。原因Qt 启动时需要一个平台插件告诉它“怎么把窗口画到屏幕上”在 ARM 板上通常用 linuxfb但 Qt 的 plugins/platforms 目录下没有这个动态库或者程序不知道去哪里找。解决分两步第一步确认编译出的 Qt 包含 linuxfb 平台插件一般在lib/plugins/platforms/libqlinuxfb.so第二步运行时设置环境变量export QT_QPA_PLATFORMlinuxfb并让 Qt 知道插件目录export QT_QPA_PLATFORM_PLUGIN_PATH/opt/Qt/plugins/platforms。在 Windows 桌面机上对应的问题是找不到qwindows.dll处理思路一样把平台插件目录放到可执行文件的相对路径下。很多发布脚本翻车都是因为这个。5.3 现象按住方向键不连续响应 / 一按就重复现象按住方向键坦克只动一格或者松开键之后坦克还在自己走。原因第一个现象的根因是你在 keyPressEvent 里直接调用了移动而系统默认开启按键自动重复所以逻辑只在“按下”的瞬间触发第二个现象的根因是 keyReleaseEvent 丢失常见于窗口失去焦点或弹窗打断。解决把“按键状态”和“每帧移动”分开。在 GameView 里维护QSetint m_keyskeyPressEvent 只负责 addkeyReleaseEvent 只负责 removeQTimer 的 timeout 里根据 m_keys 决定坦克方向并调用 tick。这样持续按键就是持续移动。窗口失焦时要多补一步重写leaveEvent或在主窗口失焦信号里调用clearKeys()避免回到窗口时坦克自动跑。这是一条典型的“用状态驱动替代事件驱动”的经验后面做玩家操作记录也用同一套状态。5.4 现象中文注释或字符串在 Windows 下编译乱码/警告现象代码里写QStringLiteral(基地)在 Windows 上用 MSVC 编译运行显示乱码编译时还经常带 C4819 警告。原因Qt 源文件默认 UTF-8而 MSVC 在中文 Windows 下默认按本地代码页 GBK 读取源码字面量被按错误编码解释。这和 C 标准里的执行字符集还不太一样属于 Qt 开发特有的坑。解决在 .pro 文件里加一行QMAKE_CXXFLAGS /utf-8告诉 MSVC 源文件按 UTF-8 读取用 CMake 就在 CMakeLists.txt 里写add_compile_options(/utf-8)。还有一个更省心的做法在 Qt Creator 里把源文件另存为“UTF-8 with BOM”BOM 会强制 MSVC 识别编码不需要额外传参。MinGW 编译器一般没有这个问题但如果切换工具链后出现乱码先看文件编码再调参数。中文 HUD 字符串建议统一走 QStringLiteral少用窄字符串 char* 转 QString减少编码转换步骤。5.5 现象release 后运行提示缺少 DLL / 换机器跑不了现象在开发机跑得好好的把 Release 目录拷给别人双击没反应或提示缺少 Qt5Core.dll。原因Qt 程序不像普通 C 程序一个 exe 就能跑它需要一堆 Qt DLL 和编译器运行时。解决常见做法是用 Qt 自带的 windeployqt 工具部署。在 Qt 命令行工具里进入 Release 输出目录执行C:\Qt\5.15.2\mingw81_64\bin\windeployqt.exe TankBattle.exewindeployqt 会扫描 exe 依赖把需要的 Qt 模块 DLL、platforms 插件、翻译文件拷到同目录。注意一定要用和你编译套件匹配的 windeployqtMinGW 编译的 exe 用 MinGW 版本的 windeployqtMSVC 编译的用 MSVC 版本。如果是 MSVC 编译器还要额外带 Microsoft Visual C Redistributable因为目标机器不一定装了运行库。发布前我习惯把整个发布目录压成一个 zip在一台干净的虚拟机里跑一遍能跑通才算真发布完成。6. 坦克大战的进阶验证回放日志与自定义关卡文件基础玩法做完后不要急着加华丽特效先做两个能帮你持续迭代的验证工具。6.1 回放日志记录按键自动跑回归在 GameView 里把每帧的按键状态快照记录成QVectorQPairint, int或者更简单在 keyReleaseEvent 里记(frame, key)QVectorQPairint, int record; // 主循环 frame 计数keyReleaseEvent 里 record.append({frame, e-key()});回放时把记录按 frame 喂给 GameView 的clearKeys和m_keys就能复现一整局输入。frame 是主循环的帧计数器key 是 Qt::Key 枚举值。这个功能不仅能帮你验证“改了 AI 后是否引入回归”也能用来录一段很难的关卡反复调数值和 Qt 的模拟鼠标点击事件思路不同这里不需要向系统发事件直接把键码注入状态集干净且不受窗口焦点影响。6.2 自定义关卡 JSON手写关卡快速验证边界做一个关卡加载函数从 JSON 读二维数组替换默认 map[ [1,1,0,0,2], [0,1,0,5,0] ]QFile file(:/levels/level1.json); file.open(QIODevice::ReadOnly); QJsonParseError err; QJsonDocument doc QJsonDocument::fromJson(file.readAll(), err); if (err.error ! QJsonParseError::NoError) return false; QJsonArray rows doc.array(); // 然后按 4.1 的方式生成瓦片验证方法启动游戏后打印map.size()和第一行长度确认 JSON 解析成的地图和预期一致。我的习惯是每次改完地图格式先用一个只包含基地和一面墙的极短 JSON 跑通再上完整关卡这样一旦碰撞或加载出错问题范围能缩到最小。回想整个项目最大的教训是先让“玩家能开炮打碎砖墙”跑通再回头调敌人 AI 和爆炸动画一次只改一个变量改坏了也容易退回去。希望帮到你。本文还有配套的精品资源点击获取
上一篇/下一篇内容由系统自动关联
返回资讯列表 →