简介:这是一份基于Qt框架实现的Flappy Bird游戏完整源码包,面向已掌握C++基础、想快速上手Qt游戏开发的学习者,也适合作为课程设计与开源项目拆解样例。项目以QGraphicsView/QGraphicsScene搭建2D渲染场景,通过QTimer控制鸟的飞行与管道滚动,利用信号槽机制处理点击上升、碰撞检测和计分逻辑,能清晰演示从图形绘制、交互响应到游戏状态管理的整套流程,覆盖了2D游戏开发中常见的渲染、事件与资源组织问题。压缩包共57个文件、约490KB,包含8个cpp源码、7个头文件、pro工程与qrc资源文件、28张png游戏素材、2个ico图标、5段mp3音效,以及ReadMe和Markdown说明;目录按地面、记分板、管道、小鸟等模块划分,便于对照学习Qt的资源加载、界面更新和跨平台编译要点。目前已有189人浏览/学习,适合借此掌握游戏循环、动画驱动和多媒体资源集成方法,并可迁移到其他Qt小游戏项目中。
1. 用 Qt 做 Flappy Bird:一份能跑通的完整 2D 游戏源码实战拆解
先说结论:这份 QtFlappyBird-master.zip 不是那种只有几个文件的半成品 Demo,而是一个用 Qt Widgets 模块(QGraphicsView/QGraphicsScene 体系)写完整了的 Flappy Bird 克隆。它把小鸟飞行、管道生成与移动、碰撞检测、计分、音效播放、开始/结束/计分板界面全串起来了,麻雀虽小五脏俱全。对于想学 Qt 游戏开发、又不想从零画轮子的人,这份源码正好当教科书用:它演示了 QTimer 驱动游戏循环、信号槽处理用户输入、QGraphicsPixmapItem 管理游戏对象、QSound 播放音效这一整套标准做法。我拆完这份代码后的直接感受是:它把“如何用 Qt 做 2D 游戏”这个问题,落成了一份可以直接编译运行、逐步断点调试的真实答案。适合三类人——刚学完 C++ 想找 Qt 练手项目的学生、要把 Qt 游戏开发引入团队的组长、以及想快速搞清 QGraphicsView 体系怎么组织游戏代码的转岗工程师。
2. QGraphicsView 与 QGraphicsScene:这份源码的地基选型
2.1 为什么是 QGraphicsView 而不是 QWidget 自绘
拿到这份源码,我第一个确认的就是它没用 QWidget::paintEvent 自绘,而是用了 QGraphicsView + QGraphicsScene + QGraphicsItem 这套图形视图框架。这个选型很关键:Flappy Bird 里有大量可移动的图形对象——小鸟、管道、地面、数字计分板,如果全用自绘,每次更新都得手动调用 update() 重绘整个界面,还要自己管理碰撞区域,代码量会翻倍。
QGraphicsView 体系的优势在于场景管理:QGraphicsScene 负责维护所有图形对象(Item)的增删改查,QGraphicsView 只负责把场景渲染到窗口上。每一帧的变化只需要修改对应 Item 的坐标或状态,视图自动重绘,不用关心局部刷新问题。这份源码里,地面、管道、小鸟都是独立的 Item 子类,各自封装自己的行为和贴图,主窗口只负责组装它们——这是标准的“组合优于继承+职责分离”写法。
在渲染性能上,QGraphicsView 默认使用 GraphicsView 的渲染管线,对 2D 游戏够用。源码里涉及动画表现的地方不多,所以没有激进地开启 ViewportUpdateMode,保持默认的 SmartViewportUpdate 模式即可:只有 Item 移动的区域会被重绘,其他区域不受影响。
// 主窗口构造函数中初始化场景与视图的典型写法 MainWindow::MainWindow(QWidget *parent) : QMainWindow(parent) { scene = new QGraphicsScene(this); scene->setSceneRect(0, 0, GAME_WIDTH, GAME_HEIGHT); // 固定游戏世界大小 view = new QGraphicsView(scene, this); view->setRenderHint(QPainter::Antialiasing); view->setFrameShape(QFrame::NoFrame); setCentralWidget(view); }这段代码把场景视口锁定为固定矩形,好处是后续所有对象的坐标都不用关心窗口尺寸变化,逻辑坐标和视觉坐标一一对应。注意 sceneRect 必须显式设置——如果让 Qt 自动计算,场景边界会随 Item 移动而膨胀,导致视口漂移,这是一处新手常见的坑。setRenderHint(QPainter::Antialiasing) 用于抗锯齿,Flappy Bird 这种纯像素贴图的游戏开不开视觉差异不大,但如果贴图里有曲线元素,建议保留。
2.2 Item 体系:从 bird 到 pipe 的统一抽象
再看源码里 gameobjects 的组织方式。bird、pipe、ground、scoreboard 各自继承 QGraphicsPixmapItem 或 QObject + QGraphicsPixmapItem 的多继承组合。QGraphicsPixmapItem 是最省事的 2D 图片 Item:构造时传入 QPixmap,setPos 控制位置,setRotation 控制旋转。
这份源码里鸟的旋转处理值得单独说:Flappy Bird 原版中鸟上下飞时身体会随速度方向倾斜,这里用 setRotation 实现。需要注意 QGraphicsItem 的旋转默认绕 Item 自身的原点 (0,0) 旋转,而贴图左上角是 (0,0),直接旋转会让鸟绕着左上角转而不是绕中心转——正确的做法是先 setTransformOriginPoint 到中心点。
// bird 类中处理旋转的代码示意 void Bird::updateRotation() { // 将旋转原点设置为图片中心,否则鸟会绕着左上角翻转 setTransformOriginPoint(boundingRect().center()); // 根据垂直速度映射旋转角度:上升时抬头,下降时低头 qreal angle = qBound<qreal>(-30.0, m_velocity * 0.4, 60.0); setRotation(angle); }qBound 把角度限制在 -30 到 60 度之间,避免鸟出现头朝下的诡异姿态。m_velocity 是鸟的垂直速度,正值向下——注意 Flappy Bird 的物理模型里“向上为正”还是“向下为正”一定要统一,这份源码里我确认用的是 Y 轴向下为正(屏幕坐标),所以速度为正表示下落。后续所有计算碰撞偏移、判定得分时都遵循这个约定,混用会导致碰撞检测时而灵敏时而失灵。
2.3 场景对象组织方式值得直接模仿
看完这份源码的类组织,我觉得它的分层很干净:场景只负责摆放和查询;Item 自己管自己的贴图、动画、逻辑;主窗口通过信号槽与 Item 通信。具体到这份源码:主窗口持有 scene、bird、pipe 容器(管道通常不止一根,用 QList 管理)、ground、scoreboard;每帧更新由 QTimer 驱动,槽函数里统一调用各 Item 的 update 逻辑。
这种组织方式的直接好处是:瓶颈好找。如果帧率低了,优先怀疑 pipe 的数量和坐标计算;如果碰撞失灵,只需要看 bird 的 shape() 与 pipe 的 shape() 的交集判断;如果要加新机关(比如移动平台),照 pipe 的写法加一个子类就是。它没有用 QML 的声明式写法,但对 C++ 开发者来说反而更直观——每个类对应一个游戏对象,打开头文件就能知道它干什么。
3. Flappy Bird 源码中的动画驱动与物理模拟逻辑
3.1 QTimer 驱动游戏循环:固定步长与帧率控制
剥开这份源码,游戏的核心循环不是 while(1) 死循环,而是 QTimer 的定时触发。主窗口里创建一个 QTimer,设置 interval 为 16ms(约 60fps),timeout 信号连接到 updateGame 槽函数。每个 tick 里依次执行:更新 bird 的垂直速度与位置、更新所有 pipe 的水平位置、检查碰撞、检查是否得分、更新地面滚动。
固定步长有个好处——逻辑确定性。每次 tick 的位移量是固定的,不会因为机器卡顿导致飞行距离突变。代价是低配机器上 60fps 撑不住时游戏会整体变慢,而不是像帧同步游戏那样“跳帧”。对于 Flappy Bird 这种操作频率低的游戏,固定步长完全够用。
// 主窗口中的定时器配置 gameTimer = new QTimer(this); gameTimer->setTimerType(Qt::PreciseTimer); gameTimer->setInterval(16); // 约 60 FPS connect(gameTimer, &QTimer::timeout, this, &MainWindow::updateGame);setTimerType(Qt::PreciseTimer) 值得说明:默认的 CoarseTimer 在系统负载高时可能合并触发,导致动画抖动;PreciseTimer 要求触发尽量准确,代价是略多的系统资源开销。2D 游戏里建议直接 PreciseTimer,避免排查“为什么动画一卡一卡”的玄学问题。
物理模型部分,这份源码实现的是简化重力模拟:鸟有一个垂直速度 m_velocity,每帧速度加上一个重力加速度常数,然后按新速度移动位置。点击屏幕时把速度重置为一个向上的固定值(比如 -5.0),实现“扇翅膀”效果。
// bird.cpp 中的物理更新 void Bird::updatePhysics() { m_velocity += GRAVITY * dt; // 重力加速度累积 m_velocity = qBound<qreal>(-MAX_FALL_SPEED, m_velocity, MAX_FALL_SPEED); setPos(x(), y() + m_velocity); // 按速度移动 }dt 是归一化的时间步长(1.0 表示一帧),便于调整帧率时保持物理一致性。GRAVITY 的值决定了游戏的手感:太大鸟掉得快、操作窗口短;太小鸟飘、没有坠落感。常见做法的初始值是 GRAVITY=0.25、点击速度=-5.0,这份源码在这个区间附近,具体手感可以按需微调。
3.2 管道对的生成与移动:从坐标计算到回收机制
管道是 Flappy Bird 的“关卡生成器”。这份源码里管道是成对出现的:一根上方管道(倒挂)和一根下方管道(正立),中间留出缺口让鸟穿过。管道间隔和缺口高度是控制难度的两个核心参数:间隔决定玩家操作的节奏(太密来不及反应,太疏游戏失去紧张感);缺口高度决定容错率。
管道移动是每帧把所有 pipe 的 x 坐标向左移固定步长(比如 2 像素),视觉上鸟在前进。当管道对完全移出屏幕左侧时,直接把它移回最右侧并重新随机生成缺口位置——这就是对象复用,而不是销毁重建,省去了频繁 new/delete 带来的内存碎片和性能抖动。
// 管道生成与回收示意 void PipeManager::spawnPipe(qreal x) { qreal gapY = QRandomGenerator::global()->bounded(MIN_GAP_Y, MAX_GAP_Y); // 上管道:倒挂,y 从顶部到 gapY 顶部 Pipe *topPipe = new Pipe(QPixmap(":/images/pipe1.png"), Pipe::Top); topPipe->setPos(x, gapY - topPipe->pixmap().height()); // 下管道:正立,y 从 gapY 底部到底部 Pipe *bottomPipe = new Pipe(QPixmap(":/images/pipe2.png"), Pipe::Bottom); bottomPipe->setPos(x, gapY + GAP_HEIGHT); scene->addItem(topPipe); scene->addItem(bottomPipe); }这里的坐标计算是常见误用重灾区:QPixmap 的 setPos 设的是 Item 左上角,不是中心。上管道要“倒挂”,图片本身就要预先倒置,或者用 setTransform(QTransform().scale(1, -1)) 翻转。如果图片没处理好,会出现管道错位半根的情况——调试时看两眼就能判断是图片方向问题还是坐标问题。
回收逻辑更简单:每帧检查 pipe 的 x 坐标是否小于-pipeWidth,如果是就把它移到屏幕右侧VIEW_WIDTH + pipeWidth的位置,重新随机 gapY。这样整个游戏过程只需要维护固定数量的管道 Item,性能不会随游戏时长下降。
3.3 地面滚动与视差效果:一个容易被忽略的细节
地面(ground)在这份源码里是单独一个 Item,持续向左移动,移出屏幕后立即把 x 坐标恢复为 0,形成无限滚动的视觉效果。这里需要注意地面移动速度必须和管道移动速度一致,否则会产生“地面和管道相对滑动”的违和感。
// ground 的滚动逻辑 void Ground::scroll() { setPos(x() - GROUND_SPEED, y()); if (x() <= -groundPixmap.width()) { setPos(0, y()); // 回绕到起点 } }回绕条件x() <= -width是精确的:地面图片宽度恰好是屏幕宽度时,移出整个图片宽度的一瞬间回绕,视觉上无缝衔接。如果地面图片宽度不是屏幕宽度的整数倍,回绕时会出现跳变——这是做视差背景时最容易翻车的细节。
4. 状态机与音效资源管理:从 ready 到 gameover 的完整闭环
4.1 用枚举状态机管理游戏阶段
这份源码没有用复杂的架构,而是用一个枚举状态变量来区分游戏阶段:READY(等待开始)、PLAYING(游戏中)、GAMEOVER(结束)。不同状态下同一输入事件走不同分支:READY 状态下点击开始游戏并给鸟一个初始速度;PLAYING 状态下点击让鸟向上飞;GAMEOVER 状态下点击重置游戏。
enum GameState { READY, PLAYING, GAMEOVER };这种写法直白、好调试、不易出错。比状态模式(State Pattern)更适合这种单一角色的小游戏。状态切换点有三个:READY→PLAYING(首次点击)、PLAYING→GAMEOVER(碰撞)、GAMEOVER→READY(点击重置)。所有状态切换都集中处理,不会出现“鸟死了还在动”“计分还在涨”的脏状态。
状态机带来的直接收益是 bug 容易定位:如果用户反馈“游戏结束了分数还在跳”,你只需要检查 PLAYING→GAMEOVER 的切换条件里是不是忘了把计分逻辑停掉。这是新手最容易漏的地方——QTimer 还在跑,但状态已经变了,槽函数里没有判断状态就继续执行了游戏逻辑。
4.2 信号槽处理点击输入与按键事件
用户输入的响应,这份源码用两种方式结合:鼠标点击和键盘空格键都有。Qt 里处理这种“即时操作”的事件,常见做法是重写 keyPressEvent 和 mousePressEvent,或者用事件过滤器。这套源码选择直接在主窗口事件函数里判断状态并调用对应方法。
// 主窗口中的事件处理 void MainWindow::mousePressEvent(QMouseEvent *event) { if (event->button() == Qt::LeftButton) { handleInput(); } } void MainWindow::keyPressEvent(QKeyEvent *event) { if (event->key() == Qt::Key_Space) { handleInput(); } } void MainWindow::handleInput() { switch (m_gameState) { case READY: startGame(); break; case PLAYING: bird->flap(); // 给鸟一个向上速度 break; case GAMEOVER: resetGame(); break; } }这里有个细节:mousePressEvent 要注意是否已经 setFocus,否则键盘事件可能不触发。这份源码在主窗口构造函数里调用 setFocusPolicy(Qt::StrongFocus),确保点击窗口后键盘事件也能收到。如果遇到“鼠标好用但键盘没反应”的情况,优先检查焦点策略。
4.3 QSound 播放音效:资源路径的两种写法
声音资源在这份源码里用的是 Qt 的资源系统(qrc 文件),5 个音效分别对应扇翅膀、得分、撞击、 swooshing、死亡。加载方式用的是 QSound::play(":/sounds/sfx_point.mp3")——注意这里用的是资源路径冒号开头,而不是相对路径。
// 音效播放封装 void playSound(const QString &name) { // 如果音效资源缺失,不要崩溃,静默跳过 if (QFile::exists(name)) { QSound::play(name); } }QSound 在 Windows 上用的是系统音频 API,在 Linux 上需要 PulseAudio 或 ALSA 后端;如果目标机器没有音频服务,QSound::play 可能不做任何事也不报错——这是“游戏没声音但不知道为什么”的常见原因。加上 QFile::exists 判断是防御式写法,避免资源缺失时播放空路径。
这种“资源路径写在代码字符串里”的做法在小项目里没问题,但资源文件改名或目录调整时,字符串忘了同步就会出现“图出不来、声音放不出”的问题。后面我会给一个更稳的封装方式。
5. 避坑指南:这份 Qt Flappy Bird 源码最容易踩的 6 个坑
5.1 中文乱码:ReadMe 文件打开就花屏
现象:解压后用记事本打开 ReadMe_gb2312.txt,中文显示为乱码或方块。 原因:这份源码的说明文件用了 GB2312 编码,而 Qt Creator 默认以 UTF-8 读取文件;Windows 记事本在新版系统上默认 UTF-8,直接打开 GB2312 文件必然乱码。 解决:用 VS Code 打开时选择“通过编码重新打开”,选 GB2312/GBK;或者在 Qt Creator 中先改编辑器编码为 System 再打开。如果要把这份说明并入自己的项目文档,建议转存为 UTF-8 并统一换行符为 LF。
5.2 MSVC 编译报错:找不到 QtWidgets 头文件
现象:用 VS2022 + Qt 插件打开 .pro 文件后编译,报错cannot open include file: 'QtWidgets/QApplication': No such file or directory,或者提示依赖项路径里出现error: dependent '..\..\..\..\..\..\qt\5.15.2\msvc2019_64\include\qtwidgets'。 原因:.pro 文件里QT += widgets没有正确传递到 VS 的 include 路径,或者 Qt 版本与 VS 工具集不匹配(msvc2019 的库用在 msvc2022 上)。 解决:右键项目 → Qt → Qt Project Settings,确认 Qt 版本选择正确;在 .pro 文件里显式声明QT += core gui widgets;如果用的是 Qt 5.15.2,VS2022 需要安装“用于 Visual Studio 的 Qt 工具”扩展并选择 msvc2019_64 的套件;实在不行改用 Qt 6.5+ 的 msvc2022_64 预编译包。注意:msvc2019 和 msvc2022 的二进制不兼容,不能混用。
5.3 小鸟碰撞判定“不真实”:明明是擦边却死了
现象:鸟的贴图边缘碰到管道一点点就算碰撞,玩家觉得判定太严;或者反过来,管道明显挡住了但没死。 原因:默认的 QGraphicsItem::collidesWithItem 用 boundingRect(外接矩形)做检测,而贴图本身有透明区域,矩形判定必然偏大;另外,碰撞发生在场景坐标系,而鸟和管道的坐标系如果旋转过(鸟设置了 Rotation),形状计算会套用变换,容易出偏差。 解决:在 Bird 类里重写QPainterPath shape() const,返回一个收缩过的 QPainterPath——用 QPainterPath 加一个略小于图片的圆角矩形或椭圆,让判定贴合视觉。同时把管道图片的透明边距裁剪掉,尽量让 boundingRect 接近实际像素。
5.4 屏幕宽高比不同导致管道错位
现象:在 16:9 屏幕上正常,切到 4:3 或竖屏模拟器上管道位置错乱、地面露出白边。 原因:游戏世界坐标是硬编码的固定数值(比如 480x800),没做自适应;View 缩放后物理像素和逻辑坐标对不上。 解决:最省事的做法是把游戏世界坐标固定为设计分辨率,View 开启setScaledContents效果不佳时改用view->fitInView(scene->sceneRect(), Qt::KeepAspectRatio),在窗口 resize 事件里重新调用一次。如果追求完美,可以用 QGraphicsScene 的宽度作为基准来计算管道间距。注意:fitInView 在窗口比例和目标比例不一致时会留黑边,可接受就用,不可接受就锁定窗口最小尺寸。
5.5 计分重复触发:一只鸟穿过两根管道加两次分
现象:鸟穿过管道缺口时,分数一次加了 2 分甚至 3 分。 原因:计分逻辑写在“鸟的 x 坐标超过管道 x 坐标”时触发,但管道对象没有标记“已经计过分”;如果同帧内多个管道同时满足条件,或者管道回收后状态没重置,就会重复计数。 解决:在 Pipe 里加一个bool scored标志,置为 false 表示未计分;检测到穿过时,如果!scored就加分并置为 true;管道回收到右侧重新生成时重置为 false。这样保证每个管道实例只计一次分。
5.6 导入源码包后资源文件找不到
现象:编译通过但运行时图片全空白、声音不响,控制台输出file not found。 原因:.qrc 文件里引用的资源路径是相对路径,源码包解压到中文目录或路径带空格时,资源收集阶段的相对路径失效;也可能是 qrc 文件没有在 .pro 里注册。 解决:在 .pro 文件里确认RESOURCES += flappy.qrc;用 Qt Creator 打开 flappy.qrc 检查里面的 alias 前缀是否与代码中的:/images/...一致;项目路径尽量不要包含中文和空格。如果资源必须外置(不编译进二进制),改用 QFile 读取相对路径并自行处理缺失场景,但这会失去 Qt 资源系统“打包即用”的好处。
6. 把这份源码改造成自己的项目:四项进阶优化
6.1 从魔法数字到配置文件
这份源码的难点参数(重力常数、管道间隔、管道速度、缺口大小)大概率直接写在 cpp 文件里,改起来要翻好几个文件。我拿到手第一件事就是把这些值统一收敛到一个配置结构体里。
// GameConfig.h struct GameConfig { qreal gravity = 0.25; qreal flapVelocity = -5.0; qreal maxFallSpeed = 12.0; int pipeInterval = 180; // 管道水平间距(像素) int pipeGap = 130; // 上下管道之间的缺口高度 qreal pipeMoveSpeed = 2.0; }; GameConfig loadConfig(const QString &path);然后用 QSettings 或者简单的 JSON 文件读取配置。好处是调难度不需要重新编译:策划或你自己想试手感,改数值文件重启游戏就行,不用动代码。我自己的习惯是默认参数写死在结构体里做兜底,配置文件缺失时用默认值,避免程序直接崩溃。
6.2 碰撞检测升级:shape() 重写实战
前面避坑提到了 shape(),这里给出具体写法。默认 shape() 返回 boundingRect(矩形),但 Flappy Bird 的鸟图片往往是圆形或椭圆,管道图片也是细长条。直接用 QPainterPath 构造贴合形状:
// Bird.cpp 中重写 shape() QPainterPath Bird::shape() const { QPainterPath path; // 椭圆中心在图片中心,半径取宽高的 40%,给玩家一定容错 QRectF rect = boundingRect(); qreal rw = rect.width() * 0.4; qreal rh = rect.height() * 0.4; path.addEllipse(rect.center(), rw, rh); return path; }注意:shape() 返回的是 Item 本地坐标系中的形状,碰撞检测时 Qt 会自动应用 Item 的 transform。所以鸟旋转后,椭圆的碰撞区域也会跟着转——这可能不是你想要的(鸟旋转 60 度时碰撞区域也应该变细长)。如果想要“无论鸟怎么转碰撞区域都不变”的效果,考虑在检测时用 collidesWithPath 手动传入场景坐标的形状,而不是依赖默认的 collidesWithItem。这是进阶调优点,新手阶段用 shape() 重写已经比默认矩形好很多。
6.3 用 QElapsedTimer 做真实帧率控制
QTimer::setInterval(16) 只是告诉系统“尽量 16ms 触发一次”,实际触发间隔受系统负载影响,可能 10ms 也可能 30ms。在低配机器上,固定步长会导致游戏变慢(因为一帧移动的像素是固定的,每秒帧数下降 = 每秒位移下降)。更稳的做法是用 QElapsedTimer 测量真实经过的时间,按真实时间缩放位移:
void MainWindow::updateGame() { qint64 elapsed = elapsedTimer.nsecsElapsed(); elapsedTimer.restart(); qreal dt = elapsed / 1000000.0 / 16.0; // 以 16ms 为基准时间的归一化步长 bird->updatePhysicsWithDt(dt); pipeManager->updateWithDt(dt); ground->scrollWithDt(dt); }dt 大于 1 说明这一帧耗时超过 16ms(卡顿),位移量变大;dt 小于 1 说明跑得比 60fps 快,位移量变小。这样游戏速度不会因为帧率波动而明显变化。注意:这个方案要求物理更新全部改成乘 dt 的形式,否则快速点击时鸟的飞行距离会飘。
6.4 用 QSoundEffect 替代 QSound
QSound 在 Qt 6 里已经废弃,而且 QSound 的播放是异步的,连续快速点击时可能丢音。换成 QSoundEffect(Qt 5.15+)或 QMediaPlayer(Qt 6),可以获得更稳定的播放控制和音量调节:
// 初始化时加载音效 QSoundEffect *wingSound = new QSoundEffect(this); wingSound->setSource(QUrl("qrc:/sounds/sfx_wing.mp3")); wingSound->setVolume(0.6f); // 播放 wingSound->play();QSoundEffect 的优点是同一个对象重复 play 不会反复创建播放线程,而且可以实时调音量。对 Flappy Bird 这种需要快速响应的音效场景,比 QSound 更合适。Qt 6 里 QSound 被移除,如果这份源码要移植到 Qt 6,这步是必做的。
我接手 Qt 项目有个习惯:拿到一份新源码,永远先编译跑通原版,然后再动刀改参数。跑通之前不评论代码好坏——很多“问题”其实是自己环境没配对。这份 Flappy Bird 源码就是这样:先原版跑起来,感受重力、看管道速度,然后按自己的手感调参数,再逐步加自己的功能。从那以后我每次做 Qt 小游戏都强制走一遍这个流程:原版跑通 → 参数配置化 → 碰撞 shape 精修 → 换现代音效接口。希望帮到你。
本文还有配套的精品资源,点击获取