资讯详情

纯C++手写FlappyBird:从游戏循环到碰撞检测的完整实现

📅 2026/10/21 5:27:16 | 华诺云谱 👁 阅读
纯C++手写FlappyBird:从游戏循环到碰撞检测的完整实现
1. 项目缘起与整体设计思路1.1 为什么选择纯C手写一款FlappyBird很多人第一次接触游戏开发都是从“复刻经典小游戏”开始的。FlappyBird这个项目尤其典型规则简单到一句话就能说清——点击屏幕让小鸟穿过管道缝隙撞到就重来。但真正动手写的时候你会发现它几乎涵盖了2D游戏开发的全部基础要素游戏循环、渲染管线、碰撞检测、物理模拟、资源管理、状态机、输入处理。麻雀虽小五脏俱全。那为什么强调“纯C”因为现在市面上大多数教程都依赖Unity、Godot、EasyX甚至Python的Pygame。这些工具确实能快速出效果但也会把底层细节全部封装掉。你写完之后对“一帧是怎么画出来的”“碰撞检测到底在算什么”“游戏循环为什么不能写成死循环”这些问题依然一头雾水。用纯C从零搭建意味着你需要自己选图形库、自己管理窗口、自己控制帧率、自己处理坐标变换。这个过程很磨人但走完一遍之后再看任何游戏引擎的文档你都会有“原来它是在做这件事”的顿悟感。这个项目适合谁我认为有三类人值得动手第一类是有C基础但没做过图形编程的开发者想通过一个完整项目把语法知识转化成工程能力第二类是做过Web前端或移动端开发想了解底层渲染和游戏循环机制的人第三类是老玩家纯粹想体验“自己写一个能玩的小游戏”的成就感。不管你属于哪一类只要你能看懂指针、类、STL的基本用法这个项目就能跟下来。1.2 技术选型图形库到底选哪个纯C写游戏绕不开的第一个问题就是图形库。C标准库本身不包含任何图形或窗口功能所以必须借助第三方库。常见的选项有这么几个图形库上手难度跨平台性适合场景备注SFML低Windows/Linux/macOS2D小游戏快速开发API设计简洁文档友好SDL2中全平台需要更底层控制偏C风格但生态成熟raylib极低全平台教学和原型函数式风格开箱即用OpenGL高全平台需要自定义渲染管线学习曲线陡峭EasyX极低仅Windows国内教学常见非跨平台功能有限我最终选择的是SFML。理由很直接它的C接口设计得非常自然sf::RenderWindow、sf::Sprite、sf::Texture这些类一看名字就知道干什么不需要写大量样板代码。而且它自带窗口管理、纹理加载、音频播放、文字渲染基本上一个库就把2D游戏需要的东西全包了。相比之下SDL2虽然更底层更灵活但你需要自己处理纹理加载和文字渲染对于第一个完整项目来说负担太重。raylib确实更简单但它的函数式风格和C的面向对象思路有些割裂不利于练习类设计。提示如果你在Windows上使用Visual StudioSFML的配置需要手动链接库文件。建议用vcpkg或CMake来管理依赖能省掉大量配置时间。1.3 项目整体架构设计在动手写代码之前我先在纸上画了一个简单的架构图。整个游戏分成四个核心模块游戏主循环模块负责控制帧率、处理事件、更新逻辑、渲染画面。这是整个程序的骨架所有其他模块都挂在它上面。实体管理模块负责小鸟、管道、地面、背景这些游戏对象的创建、更新和销毁。碰撞检测模块独立出来因为它是游戏的核心判定逻辑需要精确控制。状态管理模块处理游戏的三种状态准备中、进行中、游戏结束。为什么要这样分因为如果把所有逻辑都塞进main函数代码会迅速膨胀到不可维护。我见过很多初学者写的FlappyBird一个main函数三百行改一个参数要在里面找半天。模块化之后每个文件职责清晰调试的时候也能快速定位问题。具体文件结构是这样的FlappyBird/ ├── main.cpp // 入口创建窗口和游戏循环 ├── Game.h/Game.cpp // 游戏主类管理状态和主循环 ├── Bird.h/Bird.cpp // 小鸟类处理物理和动画 ├── Pipe.h/Pipe.cpp // 管道类处理生成和移动 ├── Collision.h // 碰撞检测工具函数 └── assets/ // 纹理和音效资源这个结构不复杂但足够支撑一个完整的游戏。后面如果要做扩展比如加道具、加排行榜直接新增模块就行不用动现有代码。2. 核心细节解析与实操要点2.1 游戏循环固定时间步长的物理更新游戏循环是整个项目的心脏。最朴素的写法是一个while循环每帧处理输入、更新位置、绘制画面。但这里有个关键问题不同电脑的帧率不一样。如果你的游戏在60帧的机器上小鸟每秒下落100像素到了144帧的机器上就会变成每秒下落240像素游戏体验完全崩坏。解决方案是引入固定时间步长的概念。核心思路是物理更新使用固定的时间间隔比如1/60秒渲染则尽可能快。如果一帧的时间超过了固定步长就多跑几次物理更新来追上。const float FIXED_TIMESTEP 1.0f / 60.0f; float accumulator 0.0f; sf::Clock frameClock; while (window.isOpen()) { float frameTime frameClock.restart().asSeconds(); if (frameTime 0.25f) frameTime 0.25f; // 防止卡顿后爆炸 accumulator frameTime; while (accumulator FIXED_TIMESTEP) { handleInput(); update(FIXED_TIMESTEP); accumulator - FIXED_TIMESTEP; } render(); }这段代码里有个细节值得说frameTime 0.25f的钳制。如果不加这个判断当窗口被拖动或系统卡顿时frameTime可能变成好几秒accumulator会累积出几百次物理更新游戏直接卡死。0.25秒是我实测下来比较安全的阈值既能保证正常情况下的流畅又能防止极端情况下的雪崩。注意固定时间步长并不意味着渲染也要固定。渲染应该尽可能快让画面看起来顺滑。物理更新和渲染解耦是游戏开发中非常重要的设计原则。2.2 小鸟物理重力、跳跃与旋转的数学原理小鸟的运动逻辑看起来简单但要做到手感舒服参数调校非常关键。核心物理量有三个重力加速度、跳跃初速度、最大下落速度。重力加速度决定小鸟下落的快慢。如果设得太大小鸟会像石头一样往下砸设得太小又会像气球一样飘。我试过很多组参数最终定在每秒平方1200像素左右。跳跃初速度决定点击后小鸟往上冲的力度我设的是每秒负400像素屏幕坐标系y轴向下为正。最大下落速度用来限制小鸟不会越掉越快设的是每秒600像素。旋转角度是让画面“精美”的关键细节。小鸟在上升时应该微微仰头下落时逐渐低头。实现方式是根据当前垂直速度映射到一个角度范围float targetRotation 0.0f; if (velocityY 0) { targetRotation -25.0f; // 上升时仰头 } else { targetRotation std::min(velocityY / 600.0f * 90.0f, 90.0f); } birdSprite.setRotation(targetRotation);这里用了一个简单的线性映射下落速度越快低头角度越大最大90度垂直向下。上升时固定仰头25度因为上升过程很短不需要动态变化。实测下来这个角度变化让小鸟的动作看起来非常自然比那些不旋转的版本生动很多。2.3 管道生成随机性与难度的平衡管道的生成逻辑直接决定了游戏的可玩性。如果缝隙太大游戏毫无挑战缝隙太小又让人砸键盘。经过反复测试我把管道缝隙高度定为150像素管道宽度定为80像素水平间距定为250像素。生成算法是这样的每次生成一对管道时上管道的高度在80到320像素之间随机取值下管道的高度就是屏幕高度减去上管道高度再减去缝隙高度。这样能保证缝隙始终在屏幕范围内不会出现“缝隙跑到屏幕外”的尴尬情况。float minTopHeight 80.0f; float maxTopHeight 320.0f; float topHeight minTopHeight (rand() % (int)(maxTopHeight - minTopHeight)); float bottomHeight SCREEN_HEIGHT - topHeight - GAP_HEIGHT;但纯随机有个问题连续出现极端值时游戏会变得不可玩。比如连续三个管道都是最高位置小鸟根本来不及调整。所以我加了一个平滑约束新管道的顶部高度不能和上一个相差超过120像素。这个约束让难度曲线更平滑玩家有节奏感而不是被随机性戏弄。实操心得管道间距不要设得太小。我一开始设了200像素结果发现小鸟刚穿过一个管道下一个就已经到眼前了反应时间根本不够。250像素是我试了七八个值之后找到的甜点。2.4 碰撞检测从矩形到像素级碰撞检测是FlappyBird的核心判定逻辑。最直接的做法是用轴对齐包围盒也就是把小鸟和管道都当成矩形判断两个矩形是否相交。这种方法计算量小代码简单对于FlappyBird这种精度要求不高的游戏完全够用。bool checkCollision(const sf::FloatRect birdBounds, const sf::FloatRect pipeBounds) { return birdBounds.intersects(pipeBounds); }但矩形碰撞有个明显问题小鸟的纹理是圆形的矩形包围盒的四个角其实是透明的。当小鸟擦着管道边缘飞过时视觉上明明没碰到游戏却判定撞了玩家会觉得“这游戏在针对我”。为了解决这个问题我把小鸟的碰撞盒缩小了一圈用纹理尺寸的70%作为碰撞盒。这样虽然牺牲了一点精确度但玩家的体验反而更好——因为人眼对“擦边”的判断本来就不精确稍微宽容一点反而觉得手感好。地面的碰撞检测更简单只需要判断小鸟的y坐标是否超过了地面高度。但这里有个细节小鸟的旋转会导致包围盒变化如果直接用旋转后的包围盒碰撞判定会变得不稳定。我的做法是碰撞检测始终使用未旋转的包围盒旋转只影响渲染不影响逻辑。这样既保证了判定的稳定性又不会让玩家觉得“明明没碰到却死了”。3. 实操过程与核心环节实现3.1 环境搭建与SFML配置先说Windows下的配置。我用的开发环境是Visual Studio 2022SFML版本是2.6.1。下载SFML之后需要做三件事配置头文件路径、配置库文件路径、链接必要的库。头文件路径指向SFML的include目录库文件路径指向lib目录。然后在项目属性的链接器输入里加上这几个库sfml-graphics.lib sfml-window.lib sfml-system.lib sfml-audio.lib注意Debug和Release模式要分别配置因为SFML的库文件带-d后缀的是Debug版本。如果搞混了会出现链接错误。如果你用CMake配置会更简洁cmake_minimum_required(VERSION 3.10) project(FlappyBird) set(CMAKE_CXX_STANDARD 17) find_package(SFML 2.5 COMPONENTS graphics window system audio REQUIRED) add_executable(FlappyBird main.cpp Game.cpp Bird.cpp Pipe.cpp) target_link_libraries(FlappyBird sfml-graphics sfml-window sfml-system sfml-audio)CMake的好处是跨平台在Linux和macOS上也能直接编译。我在Linux上测试过只需要把SFML的开发包装上cmake一条命令就能跑起来。提示SFML的音频模块在有些Linux发行版上需要额外安装OpenAL。如果编译时报音频相关的链接错误先检查这个。3.2 游戏状态机的实现FlappyBird有三种状态准备中、进行中、游戏结束。状态机的作用是控制不同状态下哪些逻辑生效、哪些输入被响应。准备状态下小鸟在原地上下浮动管道不移动点击后进入进行中。进行中状态下小鸟受重力影响管道向左移动碰撞检测生效。游戏结束状态下所有物体停止移动显示得分和重开提示。enum class GameState { Ready, Playing, GameOver }; void Game::update(float deltaTime) { switch (currentState) { case GameState::Ready: bird.floatUpAndDown(deltaTime); break; case GameState::Playing: bird.updatePhysics(deltaTime); pipeManager.update(deltaTime); if (checkCollisions()) { currentState GameState::GameOver; } break; case GameState::GameOver: // 不更新任何逻辑 break; } }状态机的实现方式有很多种我用的是最简单的switch-case。对于只有三个状态的游戏来说这比状态模式更直观。但如果后面要加暂停、设置、排行榜等状态建议改成状态模式每个状态一个类避免switch越来越长。3.3 纹理加载与精灵渲染“精美”这个词对FlappyBird来说主要体现在视觉细节上。我准备了四张纹理小鸟、管道、地面、背景。小鸟纹理包含三帧动画用来实现翅膀扇动的效果。sf::Texture birdTexture; birdTexture.loadFromFile(assets/bird.png); sf::Sprite birdSprite(birdTexture); birdSprite.setTextureRect(sf::IntRect(0, 0, 34, 24));翅膀动画的实现方式是每隔0.1秒切换一次纹理区域float animationTimer 0.0f; int currentFrame 0; void Bird::updateAnimation(float deltaTime) { animationTimer deltaTime; if (animationTimer 0.1f) { animationTimer 0.0f; currentFrame (currentFrame 1) % 3; sprite.setTextureRect(sf::IntRect(currentFrame * 34, 0, 34, 24)); } }背景的滚动效果是另一个提升视觉质感的关键。我用了两层背景远景和近景以不同速度向左移动。当背景移出屏幕时把它重新放到右边形成无限滚动的效果。这种视差滚动让画面有了纵深感比静态背景好看很多。backgroundX - scrollSpeed * deltaTime; if (backgroundX -SCREEN_WIDTH) { backgroundX SCREEN_WIDTH; }实操心得纹理的加载路径最好用相对路径并且把assets文件夹放在可执行文件同级目录。如果用绝对路径换一台电脑就跑不起来了。另外纹理加载失败时SFML不会崩溃只是显示白色方块所以一定要检查loadFromFile的返回值。3.4 得分判定与难度递增得分逻辑很简单当小鸟的x坐标超过管道的右边缘时分数加一。但这里有个容易踩的坑每对管道只能计一次分。如果不加标记小鸟在管道附近上下浮动时会反复触发计分。struct PipePair { sf::Sprite topPipe; sf::Sprite bottomPipe; bool scored; float x; }; void PipeManager::updateScore(float birdX) { for (auto pipe : pipes) { if (!pipe.scored birdX pipe.x PIPE_WIDTH) { pipe.scored true; score; } } }难度递增方面我做了两个调整随着分数增加管道移动速度略微提升管道缝隙略微缩小。但幅度非常克制速度每分增加2像素每秒缝隙每分缩小1像素上限分别是基础值的1.5倍和0.8倍。这样前期轻松后期有挑战但不会突然变得不可能。4. 常见问题与排查技巧实录4.1 画面撕裂与垂直同步最开始跑起来的时候画面有明显的横向撕裂尤其是管道快速移动时。这是因为渲染和显示器刷新不同步。解决方案是开启垂直同步window.setVerticalSyncEnabled(true);开启后渲染会等待显示器刷新完成再交换缓冲区撕裂就消失了。但垂直同步有个副作用如果某一帧渲染时间超过了刷新周期帧率会直接掉到下一个刷新周期造成卡顿。对于FlappyBird这种轻量游戏垂直同步是利大于弊的。如果你不想用垂直同步也可以手动限制帧率window.setFramerateLimit(60);这两种方式选一个就行不要同时开。4.2 碰撞检测的“幽灵碰撞”前面提到过矩形碰撞盒比小鸟纹理大导致擦边时误判。我一开始把碰撞盒设成纹理的100%结果玩家反馈“明明没碰到就死了”。后来改成70%体验好了很多。但70%也不是随便定的我做了个简单的测试让小鸟以不同角度接近管道记录视觉上“看起来碰到了”和“实际判定碰到了”的差距。70%是视觉和判定最接近的比例。另一个幽灵碰撞的来源是管道纹理的透明区域。管道纹理的上下两端有圆角圆角部分是透明的但矩形碰撞盒把透明区域也算进去了。解决方案是把管道的碰撞盒也缩小只取中间实心部分。这样小鸟从圆角旁边飞过时就不会误判。4.3 内存泄漏与资源管理SFML的资源类Texture、SoundBuffer、Font都是不可拷贝的如果直接放在类里按值传递编译器会报错。正确的做法是用引用或指针持有它们并且确保生命周期覆盖整个使用期间。我踩过的坑是在Bird类里直接放了一个sf::Texture成员然后在构造函数里加载。结果每次创建Bird对象都会重新加载一次纹理浪费内存。后来改成把纹理放在Game类里统一管理Bird只持有一个sf::SpriteSprite引用Game的纹理。这样纹理只加载一次所有Sprite共享。class Game { sf::Texture birdTexture; sf::Texture pipeTexture; // ... }; class Bird { sf::Sprite sprite; // 引用Game的纹理 };注意SFML的Sprite不拥有纹理它只持有纹理的指针。所以必须确保纹理的生命周期比Sprite长。如果纹理先销毁了Sprite就会显示为白色方块。4.4 常见问题速查表问题现象可能原因排查方法解决方案画面撕裂未开启垂直同步观察快速移动物体边缘开启setVerticalSyncEnabled小鸟下落速度异常未使用固定时间步长在不同刷新率显示器上测试引入accumulator机制纹理显示为白色方块纹理加载失败或生命周期结束检查loadFromFile返回值确保纹理在Sprite之前创建、之后销毁碰撞误判碰撞盒过大打印碰撞盒坐标对比视觉位置缩小碰撞盒至纹理的70%管道生成位置不合理随机范围设置不当连续观察多局游戏加入平滑约束限制相邻管道高度差游戏卡顿后加速frameTime累积过多拖动窗口后观察钳制frameTime上限为0.25秒音频播放失败音频设备未初始化检查SFML音频模块链接确保链接sfml-audio并安装OpenAL4.5 性能优化的小技巧FlappyBird本身不重但在低端设备上还是可以做些优化。第一减少绘制调用。背景、地面、管道、小鸟的绘制顺序要固定避免频繁切换纹理。第二复用对象。管道不要频繁创建销毁用对象池管理移出屏幕的管道重置位置后重新使用。第三避免每帧加载资源。所有纹理和音效在游戏初始化时一次性加载运行期间只读取不加载。我实测下来优化前在低端笔记本上帧率在45左右波动优化后稳定在60帧。对于这种小游戏来说60帧和45帧的体验差距其实不大但稳定比高低更重要。4.6 从“能玩”到“精美”的细节打磨最后分享几个让游戏看起来更精致的技巧。屏幕震动小鸟撞击管道时让整个画面短暂震动几帧增强打击感。实现方式是渲染时给所有物体加一个随机偏移持续0.1秒后恢复。得分动画得分时分数数字放大再缩回给玩家正反馈。渐变背景用两个颜色的矩形叠加上面深蓝下面浅蓝比纯色背景有层次感。地面纹理滚动地面纹理以比管道更快的速度向左移动强化速度感。这些细节单独看都很小但加在一起游戏的质感会有明显提升。我做完这些之后给几个朋友试玩他们的第一反应是“这真的是你自己写的看起来像商店里下载的”。这个项目我从开始到完成大概花了三个周末。第一个周末搭框架和游戏循环第二个周末做小鸟物理和管道生成第三个周末调碰撞和打磨细节。中间踩了不少坑但每个坑填完之后对游戏开发的理解都深了一层。如果你也在做类似的项目我的建议是先把核心循环跑通哪怕画面只有几个方块然后再逐步替换成纹理和动画。不要一开始就追求完美迭代比一次性设计更靠谱。
📝

华诺云谱内容团队

资深建站顾问 · 行业研究员

10年+企业数字化服务经验,专注智能建站、SEO优化与品牌营销,持续输出建站技巧、行业洞察与营销干货,已帮助5000+企业实现数字化增长。

你可能需要的服务

订阅华诺云谱资讯周报

每周一封,精选建站技巧、SEO与营销干货,直达邮箱。已有 8,000+ 企业主订阅,助你少走弯路。

↑