news 2026/10/21 5:27:15

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

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
纯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 Studio,SFML的配置需要手动链接库文件。建议用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 2022,SFML版本是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::Sprite,Sprite引用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并安装OpenAL

4.5 性能优化的小技巧

FlappyBird本身不重,但在低端设备上还是可以做些优化。第一,减少绘制调用。背景、地面、管道、小鸟的绘制顺序要固定,避免频繁切换纹理。第二,复用对象。管道不要频繁创建销毁,用对象池管理,移出屏幕的管道重置位置后重新使用。第三,避免每帧加载资源。所有纹理和音效在游戏初始化时一次性加载,运行期间只读取不加载。

我实测下来,优化前在低端笔记本上帧率在45左右波动,优化后稳定在60帧。对于这种小游戏来说,60帧和45帧的体验差距其实不大,但稳定比高低更重要。

4.6 从“能玩”到“精美”的细节打磨

最后分享几个让游戏看起来更精致的技巧。屏幕震动:小鸟撞击管道时,让整个画面短暂震动几帧,增强打击感。实现方式是渲染时给所有物体加一个随机偏移,持续0.1秒后恢复。得分动画:得分时分数数字放大再缩回,给玩家正反馈。渐变背景:用两个颜色的矩形叠加,上面深蓝下面浅蓝,比纯色背景有层次感。地面纹理滚动:地面纹理以比管道更快的速度向左移动,强化速度感。

这些细节单独看都很小,但加在一起,游戏的质感会有明显提升。我做完这些之后,给几个朋友试玩,他们的第一反应是“这真的是你自己写的?看起来像商店里下载的”。


这个项目我从开始到完成大概花了三个周末。第一个周末搭框架和游戏循环,第二个周末做小鸟物理和管道生成,第三个周末调碰撞和打磨细节。中间踩了不少坑,但每个坑填完之后,对游戏开发的理解都深了一层。如果你也在做类似的项目,我的建议是:先把核心循环跑通,哪怕画面只有几个方块,然后再逐步替换成纹理和动画。不要一开始就追求完美,迭代比一次性设计更靠谱。

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/10/21 5:26:36

从激光雷达到占用栅格地图:机器人导航地图模块设计与实践

做机器人的人应该都有同感&#xff1a;底盘能动起来只是开始&#xff0c;真正决定机器人是不是“靠谱”的&#xff0c;是它能不能回答三个问题——我在哪、我要去哪、路上有没有墙。这三个问题绕不开一张地图。我最近在梳理一个叫lingbot-map的地图模块&#xff0c;它来自一个代…

作者头像 李华
网站建设 2026/10/21 5:26:25

SolidWorks到Unity模型导入管线:从CAD数据到实时交互场景的完整指南

1. 从一张图纸到可交互场景&#xff1a;这条管线到底在解决什么问题做过机械设计的朋友大概率都经历过这样的场景&#xff1a;在 SolidWorks 里把一套机构画得清清楚楚&#xff0c;装配体干涉检查全绿&#xff0c;工程图也出完了&#xff0c;结果领导或者客户来一句——“能不能…

作者头像 李华
网站建设 2026/10/21 5:25:56

React企业级应用实战:技术选型、工程化搭建与性能优化

接手一个代号叫“rea”的项目时&#xff0c;我第一反应是这缩写也太随意了。但做着做着就发现&#xff0c;项目全称其实是React Enterprise Application&#xff0c;一个从零搭建的企业级前端应用&#xff0c;里边有商品展示、购物流程、后台配置、权限控制&#xff0c;还有一堆…

作者头像 李华
网站建设 2026/10/21 5:25:31

OpenClaw 命令行完全指南:从安装、配置到调试的常用命令详解

最近被好几个朋友追着问&#xff1a;你天天挂在嘴边的那个开源 AI 助手控制中心 OpenClaw&#xff0c;到底怎么上手&#xff1f;它的常用命令到底有哪些&#xff1f;OpenClaw 的核心定位很直接&#xff1a;把你的 AI 助理从网页对话框里解放出来&#xff0c;接到各种即时通讯渠…

作者头像 李华
网站建设 2026/10/21 5:24:50

Agent Skills实战:让大模型从陪聊变成专家

不用急着囤一堆课程。让大模型真正帮你干活的钥匙&#xff0c;其实藏在一个叫 Agent Skills 的概念里。这一篇不整虚的&#xff0c;直接拆开揉碎讲清楚&#xff1a;它到底是什么&#xff0c;为什么能把“陪聊”变成“专家”&#xff0c;以及小白程序员怎么一步步上手落地。我一…

作者头像 李华