ARTICLE DETAIL

资讯详情

深耕网站视觉设计与运营推广的一线实战洞察。

用C++与Qt重写FC坦克大战:从画布绘制到碰撞检测全攻略

用C++与Qt重写FC坦克大战:从画布绘制到碰撞检测全攻略 简介一款基于 Qt 和 C 开发的 FC 坦克大战复刻项目从经典红白机玩法中取材定位为面向 Qt 学习者和 C 游戏开发初学者的源码参考适合用于课程设计、兴趣开发或巩固 GUI 编程知识。压缩包约 4.72MB以项目源码及工程配置为主交付内容清晰紧凑整体界面简洁、还原度高并实现角色移动、子弹开火、敌方 AI、障碍物交互和过关判定等大部分功能注释划分明确便于按模块阅读和二次扩展。已有 1729 人学习浏览说明该主题具备不错的参考价值。通过该工程可学习游戏循环组织、定时器事件、信号槽通信与界面绘制刷新等 Qt 关键机制也能对照源码拆解坦克移动、子弹发射、碰撞检测、敌人生成和关卡推进等经典设计清晰的注释更加方便后续增加双人模式、道具系统或自定义关卡。对刚接触 Qt 的开发者而言整个工程直观演示了如何用定时器驱动画面更新、通过按键事件控制角色移动并在信号槽中处理游戏逻辑是理想的入门范例。1. 从FC到Qt为什么选C重写坦克大战而不是用游戏引擎如果你和我一样是80后大概率在红白机上玩过那款“90坦克大战”红色老鹰、钢砖、吃星星变强、敌人从出生点源源不断钻出来。现在用QT FC坦克大战C这个标题重写它绝不是为了怀旧而是因为用Qt Widgets做2D游戏能让你把C对象设计、碰撞检测、事件循环、绘图体系一次摸透比直接用Unity或Godot学到的东西扎实得多。引擎帮你封装了场景和渲染而Qt只给你画布和定时器坦克的每一帧都要你自己算。这篇文章面向两类人学过C基础、想拿一个完整项目练手的在校生以及刚接触Qt、想知道怎么组织一个非业务型应用代码的Windows/Linux开发者。我会把从建工程到能跑出第一颗子弹的完整路径写清楚参数、坑、改法都放出来。2. 搭建Qt坦克大战的最小工程环境、资源与项目骨架2.1 Qt版本选择与编译套件避开QT版本冲突的那条报错写Qt程序第一个决定是装哪个版本。打开搜索引擎相关热搜里有“qt安装”“qt download”“qt designer下载”很容易被新版本晃花眼。我的建议是不要追新认准一个长期支持版比如Qt 5.15.2或者Qt 6.2之后的某个LTS。5.15.2最大的好处是资料多网上能搜到的QPainter、QTimer示例绝大部分直接兼容Qt 6对C17要求更严格类库变化不大但遇到“cannot mix incompatible Qt library”这类报错时排查成本会高一点。所谓“cannot mix incompatible Qt library (version ex50601) with this library”的核心原因是你的程序在运行时加载了多个不同小版本的Qt动态库。最常见的是用Qt 5.12的编译器工具链编译但系统PATH里有一个Qt 5.6的DLL或者项目里同时引用了Qt5Core.dll和Qt6Core.dll。解决方案只有一个保证编译期头文件、链接期.lib、运行期DLL三者版本完全一致。我一般在项目文件里显式写死# 坦克大战.pro QT core gui greaterThan(QT_MAJOR_VERSION, 4): QT widgets TARGET tank_battle TEMPLATE app CONFIG c11 DEFINES QT_DEPRECATED_WARNINGS这里把QT和CONFIG写死能在qmake阶段就发现你想用Qt6却把5的库放进来的问题。如果你用CMake则在CMakeLists里写find_package(QT NAMES Qt6 Qt5 REQUIRED COMPONENTS Widgets) find_package(Qt${QT_VERSION_MAJOR} REQUIRED COMPONENTS Widgets) target_link_libraries(tank_battle PRIVATE Qt${QT_VERSION_MAJOR}::Widgets)参数说明greaterThan宏在Qt6里会自动跳过Qt4分支CONFIG c11保证后面用std::shared_ptr管理实体时的语法安全。另一个常见问题是安装时忘记勾选对应编译套件。用Qt安装工具勾选MSVC或MinGW同时必须勾选“Qt Debug Symbols”和“Qt Designer”模块。设计师Qt Designer虽然不能直接画游戏地图但可以用来设计菜单界面所以顺手装上不吃亏。2.2 用QWidget做游戏画布重写paintEvent的要点FC坦克大战的画面是纯2D用QGraphicsView有点“重”——它的场景结构适合大量可选中元素但我们的砖块和坦克是高频变化对象每次刷新都要重绘。我用得最多的是自定义一个QWidget作为画布重写paintEvent和keyPressEvent这样所有游戏对象都在一个坐标系里画逻辑最直观。// gamewidget.h #ifndef GAMEWIDGET_H #define GAMEWIDGET_H #include QWidget #include QTimer #include QVector class Tank; class GameMap; class GameWidget : public QWidget { Q_OBJECT public: explicit GameWidget(QWidget *parent nullptr); protected: void paintEvent(QPaintEvent *event) override; void keyPressEvent(QKeyEvent *event) override; void keyReleaseEvent(QKeyEvent *event) override; private: QTimer m_timer; Tank *m_player nullptr; GameMap *m_map nullptr; }; #endif// gamewidget.cpp #include gamewidget.h #include QPainter #include QKeyEvent GameWidget::GameWidget(QWidget *parent) : QWidget(parent) { setFocusPolicy(Qt::StrongFocus); // 必须设置否则按键不响应 m_timer.setInterval(16); // 约60帧 connect(m_timer, QTimer::timeout, this, [this](){ update(); // 触发重绘 }); m_timer.start(); } void GameWidget::paintEvent(QPaintEvent *event) { Q_UNUSED(event); QPainter painter(this); painter.fillRect(rect(), Qt::black); // 地图、坦克、子弹的绘制函数都接收同一个painter if (m_map) m_map-draw(painter); if (m_player) m_player-draw(painter); painter.end(); }这里有几个关键点。第一setFocusPolicy(Qt::StrongFocus)必须放在构造函数里否则你在键盘上按方向键窗口根本收不到事件。第二定时器驱动update()而不是直接调paintEvent因为update()会合并连续的重绘请求省CPU。第三所有绘制函数都接收QPainter这样能把背景、地图、坦克、子弹的绘制顺序固定下来——先画地图再画坦克子弹画在最上层。2.3 项目文件结构把地图、精灵、逻辑分开不要把所有代码都塞进GameWidget否则写不到两千行你就不想维护了。我一般把工程拆成这几个类Tank玩家和敌方坦克的公共基类管理方向、坐标、移动速度、生命值。PlayerTank继承Tank处理吃到星星后的双发逻辑。EnemyTank继承Tank有简单AI向下、转向、随机开火。Bullet记录坐标、方向、速度以及是谁发射的区分敌我。GameMap存储地图二维数组提供碰撞查询和绘制接口。GameWidget作为控制器处理键盘输入、碰撞检测、胜负判定。目录结构如下tank_battle/ ├── tank_battle.pro ├── main.cpp ├── gamewidget.h / .cpp ├── tank.h / .cpp ├── player_tank.h / .cpp ├── enemy_tank.h / .cpp ├── bullet.h / .cpp └── gamemap.h / .cppmain.cpp里只做三件事创建QApplication创建GameWidget并设置窗口标题和默认尺寸显示窗口。窗口大小我设成原版的分辨率每格像素数 × 地图宽度。这样后面做界面缩放时坐标系不会乱。3. 让坦克动起来按键输入、定时器与碰撞检测的核心逻辑3.1 按键处理与方向状态别把QKeyEvent当QMouseEvent用FC坦克大战的坦克移动特点是按住方向键持续移动松开停止不能斜向移动。所以我们要用keyPressEvent和keyReleaseEvent维护一个“四方向按键状态”的结构。// 在GameWidget私有成员中定义 bool m_keyUp false; bool m_keyDown false; bool m_keyLeft false; bool m_keyRight false; bool m_keyFireDown false; void GameWidget::keyPressEvent(QKeyEvent *event) { switch (event-key()) { case Qt::Key_Up: m_keyUp true; break; case Qt::Key_Down: m_keyDown true; break; case Qt::Key_Left: m_keyLeft true; break; case Qt::Key_Right: m_keyRight true; break; case Qt::Key_Space: m_keyFireDown true; break; default: QWidget::keyPressEvent(event); } } void GameWidget::keyReleaseEvent(QKeyEvent *event) { switch (event-key()) { case Qt::Key_Up: m_keyUp false; break; case Qt::Key_Down: m_keyDown false; break; case Qt::Key_Left: m_keyLeft false; break; case Qt::Key_Right: m_keyRight false; break; case Qt::Key_Space: m_keyFireDown false; break; default: QWidget::keyReleaseEvent(event); } }这个写的核心价值是把按键事件从“一次性触发”变成“持续状态”。游戏循环里每帧根据这四个bool决定坦克是否移动、朝哪个方向移动。注意不要在keyPressEvent里直接调用move()因为键盘事件只会触反一次按住不松不会连续触发而游戏循环每帧都在跑才能实现连续移动。参数说明支持按住Space连发如果游戏里要做“单发/连发切换”可以在事件里判别一下event-isAutoRepeat()只有非自动重复才算真正按下一次。这个细节很多新手会漏掉导致按住空格时开出三发子弹但那是keyPressEvent自动重复导致的和游戏逻辑无关。3.2 QTimer驱动游戏循环帧率与移动像素的换算FC原版坦克移动是“一格一格”的但我们用像素移动更顺滑。我设置定时器16ms即60fps玩家坦克移动速度是每帧1像素那么每秒60像素。原版地图格子是16×16像素也就是说1秒正好能走过接近4格这个手感偏慢实际调试时我喜欢用每帧1.5像素也就是每200ms走18像素配合碰撞检测的取整逻辑手感刚刚好。void GameWidget::gameLoop() { // 根据按键状态处理玩家移动 if (m_player) { int step m_player-speed() * (m_timer.interval() / 16.0); if (m_keyUp) m_player-moveUp(step); if (m_keyDown) m_player-moveDown(step); if (m_keyLeft) m_player-moveLeft(step); if (m_keyRight) m_player-moveRight(step); if (m_keyFireDown) m_player-shoot(); // 碰撞检测修正位置 resolveCollisions(m_player, m_map); } // 移动所有子弹并检测碰撞 ... update(); }step的计算要注意如果定时器interval不是固定16ms比如换成了20ms那么每次移动的像素必须按实际时间放大或缩小否则游戏速度会受系统定时器误差影响。我一般把16作为基准值这样做出来的游戏在60Hz、120Hz屏幕上的速度感接近。移动后必须立刻做碰撞检测并且在检测后“把坦克推出去”否则会嵌进砖墙里。3.3 碰撞检测矩形相交与Map格子判断的两种方案坦克和砖墙的碰撞有两种做法。第一种是“矩形相交检测”坦克是QRect地图里的每一块砖也是一个QRect遍历所有砖块如果intersects就回退位置。第二种是“格子检测”把坦克覆盖的像素范围映射到地图数组的格子坐标读取格子类型如果是砖或钢就禁止移动。我推荐第二种因为地图数组本身就是二维的查询效率高而且能精确到16×16像素格子。核心代码如下bool GameMap::isBlocked(int x, int y, int width, int height) { // 将像素坐标换算成格子坐标 int left x / TILE_SIZE; int right (x width - 1) / TILE_SIZE; int top y / TILE_SIZE; int bottom (y height - 1) / TILE_SIZE; for (int r top; r bottom; r) { for (int c left; c right; c) { int type m_cells[r][c]; if (type TILE_BRICK || type TILE_STEEL || type TILE_WATER) { return true; } } } return false; }参数说明TILE_SIZE定义为16。出水域TILE_WATER的标志是坦克不能过但子弹能过。所以如果要区分“坦克碰撞”和“子弹碰撞”写两个函数isBlockedForTank和isBlockedForBullet前者返回上述类型后者只检查TILE_BRICK和TILE_STEEL。钢砖可以提高后让子弹无法摧毁普通砖可以被子弹打碎这些也会在碰撞函数里处理。3.4 子弹与坦克的生命周期管理子弹是典型的“短生命周期”对象我不用new裸指针而是用QListstd::shared_ptrBullet。每帧移动所有子弹检查是否出界、撞墙、撞坦克。撞墙时如果是普通砖把对应格子置为TILE_EMPTY然后删除子弹撞到敌方坦克给坦克减生命值生命为0时从列表里移除。void GameWidget::updateBullets() { for (auto it m_bullets.begin(); it ! m_bullets.end(); ) { Bullet *bullet it-get(); bullet-move(); // 出界 if (!this-rect().contains(bullet-rect())) { it m_bullets.erase(it); continue; } // 撞砖 int cellX bullet-x() / GameMap::TILE_SIZE; int cellY bullet-y() / GameMap::TILE_SIZE; if (m_map-isBlockedForBullet(cellX, cellY)) { m_map-destroyCell(cellX, cellY); it m_bullets.erase(it); continue; } it; } }这里有个血泪经验删除列表元素时如果直接erase当前的迭代器必须把循环写对。我用it erase(it)这样迭代器自动指向下一个元素逻辑就对了。还有子弹和坦克的碰撞要在“坦克移动之后、地图修正之前”检测否则会出现子弹穿墙打中贴墙坦克的错觉。4. 复现FC经典地图关卡数据、贴图绘制与渲染优化4.1 用二维数组定义关卡把FC原版地图数据转成字节数组FC坦克大战的地图是13×13的格子每个格子有固定含义。原版数据存在ROM里我们自然拿不到但根据游戏画面能画出等价的二维数组。我用枚举定义格子类型enum TileType { TILE_EMPTY 0, TILE_BRICK 1, TILE_STEEL 2, TILE_WATER 3, TILE_TREES 4, TILE_BASE 5, TILE_SPAWN 6 };关卡数据用嵌套数组最直观比如第一关前两行可以这样写const int level1[13][13] { {0,0,0,0,1,0,0,0,1,0,0,0,0}, {0,0,0,1,1,0,0,0,1,1,0,0,0}, ... };我一般把这些数据放在一个levels.h头文件里每关一个静态数组。这里要注意不要盲目复制网上流传的数据有些是错误的。我调试时习惯把地图打印成字符画每两个数组值对应一个字符一眼就能看出出生点、基地位置对不对。参数说明TILE_SPAWN表示敌方坦克出生点TILE_BASE是你守卫的老鹰。这两个值不参与碰撞绘制但参与逻辑判断。敌方出生点在地图四个角我方出生点在下方中央老鹰在最后一行中央。游戏开始时先根据出生点数组生成对应的实体。4.2 绘制砖块、钢墙、水和基地QPixmap缓存与绘图顺序不要在paintEvent里直接painter-drawRect画每一块砖那样每帧都要重复计算几百个格子会卡。常见做法是用QPixmap缓存每种格子的贴图然后painter-drawPixmap按坐标贴上去。void GameMap::loadTextures() { m_brickPixmap QPixmap(:/images/brick.png); m_steelPixmap QPixmap(:/images/steel.png); m_waterPixmap QPixmap(:/images/water.png); m_treesPixmap QPixmap(:/images/trees.png); m_basePixmap QPixmap(:/images/base.png); } void GameMap::draw(QPainter *painter) { for (int row 0; row ROWS; row) { for (int col 0; col COLS; col) { int type m_cells[row][col]; QPixmap *pix nullptr; switch (type) { case TILE_BRICK: pix m_brickPixmap; break; case TILE_STEEL: pix m_steelPixmap; break; case TILE_WATER: pix m_waterPixmap; break; case TILE_TREES: pix m_treesPixmap; break; case TILE_BASE: pix m_basePixmap; break; default: continue; } painter-drawPixmap(col * TILE_SIZE, row * TILE_SIZE, *pix); } } }这里注意QPixmap应该在构造函数里加载一次不要每帧在paintEvent里重复加载否则文件I/O会成为性能瓶颈。如果你不想自己做素材可以先画纯色矩形代替painter-fillRect(rect, QColor(193, 91, 91)); // 砖砖块颜色但用真正的贴图观感好很多。网上有很多“坦克大战素材包”下载后注意版权个人学习无所谓商业发布就要小心了。绘图顺序很关键。我要求的绘制顺序是先画所有地图块包括地面再画树再画坦克和子弹。为什么树要放在坦克后面因为FC原版里树是半透明的坦克在树丛下面看起来坦克若隐若现有遮挡感。如果先画坦克再画树坦克就完全被树盖住了那没法玩。所以正确顺序是map-drawGround(painter) 画除了树之外的所有格子画坦克和子弹map-drawTree(painter) 最后画树树用带透明通道的PNG4.3 界面缩放与坐标换算让640×480原版画面适配窗口FC原版分辨率是256×240用16×16像素画格一共13×13格加上边框和基地区域实际画面高度是240像素。缩放之后才能适合窗口显示。我在GameWidget里设置一个缩放因子const int SCALE 3; // 放大3倍窗口分辨率变成 768x720然后所有绘制和输入坐标都要乘这个因子。最简单的做法是设置QPainter::scale(SCALE, SCALE)这样所有坐标都还在原来的游戏逻辑坐标系里窗口自动放大void GameWidget::paintEvent(QPaintEvent *) { QPainter painter(this); painter.scale(m_scale, m_scale); // 后续所有draw()都按256x240的逻辑坐标画 ... }鼠标或触摸输入如果是映射到屏幕要除以缩放因子。键盘输入不需要因为它和坐标无关。这里有个坑如果窗口可拖拽拉伸SCALE固定会导致画面拉伸变形。我一般把窗口设为固定大小或者用resizeEvent里重新计算缩放因子void GameWidget::resizeEvent(QResizeEvent *event) { m_scale qMin(width() / LOGICAL_WIDTH, height() / LOGICAL_HEIGHT); QWidget::resizeEvent(event); }4.4 动画与音效用QSoundEffect还是QMediaPlayer子弹击中砖墙会有爆炸动画坦克移动有履带声。Qt里播放音效有两个常见模块QSoundEffect适合放WAV格式的短音效资源占用低适合子弹射击这种高频事件QMediaPlayer适合放MP3背景音乐但初始化和解码耗时较长要在游戏启动时预加载否则第一次开枪会卡顿。我一般这样用#include QSoundEffect QSoundEffect *shotSound new QSoundEffect(this); shotSound-setSource(QUrl(qrc:/sounds/shot.wav)); shotSound-setVolume(0.4f); // 在坦克发射子弹的代码里 shotSound-play();注意QSoundEffect不要每次发射都new一个那样会有明显爆音。只创建一个反复调用play()即可。另外setVolume参数范围是0到10.3左右比较合适Qt的Audio系统还会受系统音量影响调太低会听不见太高又刺耳建议留出音量选项。5. 避坑手册从编译报错到运行黑屏的5个典型问题5.1 编译报错fatal: cannot mix incompatible Qt library现象编译链接都通过一运行就弹出一个对话框标题是“无法定位程序输入点”或者控制台输出fatal: cannot mix incompatible Qt library (version ex50601) with this library。原因程序在运行时加载的Qt5Core.dll和编译时的版本不是同一套。最常见于你电脑上同时装了MinGW版和MSVC版Qt并且把两个bin目录都加入了PATH。也有情况是Qt安装包修复后小版本对不上。解决第一步在项目构建设置里查看“运行环境”确保使用的可达DLL目录是当前Kit对应的bin目录第二步把其他不相干Qt版本的bin目录从PATH里移除。如果你用Qt Creator可以在“项目→构建环境”中给PATH最前面插入正确的Qt bin路径。如果还不行用where.exe Qt5Core.dll看看实际加载了哪个位置的DLL把那多出来的删掉。5.2 运行时崩溃qt.qpa.plugin: could not find the Qt platform plugin linuxfb现象在Linux开发板上运行出现qt.qpa.plugin: could not find the Qt platform plugin linuxfb in然后程序退出。原因Qt的图形界面需要一个平台插件桥接到底层显示系统。你编译时选择的是linuxfb平台但系统中没有对应的插件库或者没有把插件的路径告诉Qt。解决第一个办法是把Qt安装目录下plugins/platforms路径添加到环境变量QT_QPA_PLATFORM_PLUGIN_PATH。第二个办法是改用offscreen平台让程序先不报错跑起来命令是export QT_QPA_PLATFORMoffscreen但这没有窗口只能用来做单元测试。对于嵌入式场景你需要在交叉编译时把linuxfb相关模块编进去而不是用-no-feature-linuxfb关掉。检查qt5/qtbase/src/plugins/platforms下有没有libqlinuxfb.so没有就回过去配置并编译安装。5.3 黑屏但程序在跑检查paintEvent里是否忘了调用QPainter::end现象启动后窗口是黑的但进程还挂着没崩溃也没有报错。原因大多数情况下是paintEvent里创建了QPainter但没有正确结束绘制导致后续的界面线程阻塞。或者你用了QPainter painter(this)之后中途return了painter析构时才自动end但某些草率代码会提前返回把后面的绘制逻辑跳过了。解决养成好习惯在paintEvent末尾显式调用painter.end()或者把绘制逻辑拆到单独的函数里保证painter生命周期完整。还有一个隐蔽原因你重写了paintEvent但最后没调用基类QWidget::paintEvent(event)同时又不是每个分支都绘制了背景。解决方法是第一行就painter.fillRect(rect(), Qt::black)铺底色。5.4 按键无效焦点问题与事件过滤现象窗口显示出来坦克死也不动鼠标点击窗口后按键才有反应。原因QWidget默认强焦点是关闭的。你虽然设置了setFocusPolicy(Qt::StrongFocus)但窗口在启动时焦点可能落在子控件上子控件接收了按键游戏窗口没有收到。尤其是你加了菜单栏或按钮后键盘事件优先传给它们。解决在showEvent里强制把焦点给游戏区域this-activateWindow(); this-setFocus();。如果是多个控件用installEventFilter在父窗口拦截所有按键事件。我常用后者因为它在任何情况下都能保证方向键被游戏吃掉bool GameWidget::eventFilter(QObject *obj, QEvent *event) { if (event-type() QEvent::KeyPress || event-type() QEvent::KeyRelease) { keyEvent(event); return true; // 不再下发 } return QWidget::eventFilter(obj, event); }注意过滤后你还需要手动转发给需要的控件比如空格用来开火如果不往QLineEdit上转发会导致输入不了用户名。5.5 内存与性能QGraphicsItem的误用现象游戏卡顿占用CPU 30%以上越玩越卡。原因如果你用了QGraphicsView和QGraphicsItem并且每次子弹飞行都新建一个QGraphicsItem到场景里删除时又只是removeItem而没有delete场景里的废弃对象越积越多图形项还会继续参与碰撞和绘制。这是典型的“把自己绕进框架里”的问题。解决尽量用QWidget自绘我整篇文章的示例都基于QWidget。如果你实在要用QGraphicsView确保删除对象时立刻delete item并且用QGraphicsScene::items()做全量遍历时注意对象类型转换。另一个性能坑是每帧都调用QGraphicsScene::update()刷新整个场景这会把所有图和动画重新遍历一遍。正确做法是只update发生变化的区域或者直接计算膨胀矩形。6. 让坦克大战从“能跑”变成“像游戏”状态机、难度曲线与声音节奏一个能跑的坦克大战和“能玩”之间隔着几样东西游戏状态管理、敌人AI、分数和生命、以及合理的调试手段。首先给游戏加一个GameState变量用来区分菜单、游戏中、关卡过渡、游戏结束四种状态。我习惯用枚举加一个状态机函数enum GameState { MENU, PLAYING, LEVEL_CLEAR, GAME_OVER }; void GameWidget::updateState() { ... }状态机控制了定时器的启停、绘制内容、按键响应。比如在GAME_OVER状态下坦克不能动空格跳回菜单。这个状态机写得越早后面加暂停、加设置菜单就越容易否则所有逻辑都堆在按键处理和主循环里改一次崩三处。其次是敌人AI。FC原版的AI并不聪明很多是靠地图砖块限制路线再加一点随机方向选择。我给敌人坦克加一个“方向计时器”每隔2秒随机换一个方向如果前方有障碍就立刻左转或右转。这样既简单又有原版的“倔劲”。同时不同关卡敌人速度和射击间隔要调整比如每过一关敌人速度因子从1.0涨到1.15射击间隔从3000ms缩短到2000ms。这些数值我都会写在配置表里改起来轻松。然后是音效节奏。射击声、爆炸声、加命声是游戏的动脉。我自己的习惯是先用一个QMediaPlayer放冒险岛风格的循环BGM再用QSoundEffect放短音效音量固定在0.3。调试声音时最容易犯的错误是直接测试时音量开到100%被吓一跳后降低系统音量结果忘了改低Qt音量最后发布出去玩家听到的是爆音。我一般把音量初始化写成配置文件音量默认15%。最后是我自己的调试习惯写一个#ifdef DEBUG的按键按F1直接开启无敌F2显示碰撞框把每个物体的QRect画成红色半透明框。有了它碰撞检测出问题时一眼能看出来不用猜。这套方法帮我省了无数时间也成了我传给新人的“习惯项”。希望这篇长文能帮你绕开大部分Qt游戏开发的坑把小时候的记忆变成自己写的程序。游戏做到能玩只是第一步把它调成“想要的感觉”才是有趣的开始。本文还有配套的精品资源点击获取
返回列表