news 2026/10/21 5:27:18

纯C++手写FlappyBird:SDL2游戏开发实战与架构解析

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
纯C++手写FlappyBird:SDL2游戏开发实战与架构解析

1. 为什么选择纯C++手写一款FlappyBird

很多人第一次接触游戏开发,都是从"抄一个FlappyBird"开始的。但绝大多数教程走的是Unity、Godot或者Python+Pygame的路线,真正用纯C++从零手写、不依赖任何游戏引擎的完整案例其实并不多。原因很简单:纯C++做游戏,意味着窗口创建、渲染循环、输入处理、碰撞检测、资源管理、音效播放这些环节全部要自己搭,工作量比调引擎API大得多。

但反过来看,这恰恰是它最大的价值。当你用纯C++把一款FlappyBird跑通之后,你会真正理解一个游戏在底层是怎么"转"起来的——帧循环是怎么驱动的、精灵是怎么被绘制到屏幕上的、碰撞盒是怎么算的、状态机是怎么切换的。这些认知是调引擎API永远给不了你的。

这篇文章面向的读者很明确:有C++基础语法(类、指针、STL容器),但没做过图形编程,想通过一个完整小项目把"游戏是怎么跑起来的"这件事搞明白的人。整篇内容会围绕一个可编译、可运行、可扩展的纯C++ FlappyBird实现展开,从环境选型、架构设计、核心模块拆解,到实际编码中的坑和调优技巧,全部讲透。

我自己的实现路线是C++17 + SDL2 + CMake,这也是目前纯C++做2D小游戏最主流、最省心的组合。下面所有代码和思路都基于这套技术栈,但其中的架构思想换成SFML、raylib甚至Win32 GDI同样适用。

2. 技术选型:SDL2、SFML还是裸Win32

2.1 三个候选方案的横向对比

在动手之前,选哪套底层库是第一个要拍板的事。纯C++做2D游戏,绕不开的选择基本就三个:SDL2、SFML、裸Win32 API。我把它们的实际体验整理成了一张表:

维度SDL2SFML裸Win32
学习曲线平缓,C风格API平缓,C++面向对象陡峭,消息机制繁琐
渲染方式默认软件渲染,可接OpenGL默认硬件加速GDI,性能差
音效支持SDL_mixer,成熟内置,简单需自己接
跨平台Windows/Mac/Linux/移动端同左仅Windows
社区资料极多,游戏开发老牌多,偏教学少,偏系统编程
包体大小小中无依赖

我最终选SDL2,核心原因是三点:第一,它的API是纯C风格,和C++混用没有心理负担,不会像SFML那样引入一堆C++对象生命周期问题;第二,SDL2的文档和示例极其丰富,遇到问题基本一搜就有答案;第三,SDL2的扩展库生态完整,SDL_image管图片、SDL_ttf管字体、SDL_mixer管音效,各司其职,不会互相耦合。

2.2 为什么不用游戏引擎

有人会问:既然有Unity这种"拖拖拽拽就能做游戏"的工具,为什么还要用纯C++手写?我的回答是:目的不同。如果你是要快速上线一款商业游戏,那当然用引擎;但如果你是要理解游戏运行的底层机制,引擎反而会把你和真相隔开。引擎帮你封装了渲染循环、资源加载、碰撞检测,你调个API就完事了,但你不知道它背后做了什么。纯C++手写,逼着你去面对每一个细节,这个过程本身就是最大的收获。

2.3 环境搭建的具体步骤

以Windows + Visual Studio为例,SDL2的接入流程如下:

  1. 从SDL2官网下载Development Libraries(VC版本)
  2. 解压后得到include、lib、bin三个目录
  3. 在VS项目属性里配置:
    • C/C++ → 常规 → 附加包含目录:指向SDL2/include
    • 链接器 → 常规 → 附加库目录:指向SDL2/lib/x64
    • 链接器 → 输入 → 附加依赖项:填入SDL2.lib;SDL2main.lib;SDL2_image.lib;SDL2_ttf.lib;SDL2_mixer.lib
  4. 把SDL2/bin/x64下的所有dll复制到exe输出目录

提示:SDL2main.lib是必须的,它负责把Windows的WinMain入口转换成标准的main函数,少了它链接会报"unresolved external symbol WinMain"。

如果用CMake管理项目,配置会更清爽:

cmake_minimum_required(VERSION 3.15) project(FlappyBird CXX) set(CMAKE_CXX_STANDARD 17) find_package(SDL2 REQUIRED) find_package(SDL2_image REQUIRED) find_package(SDL2_ttf REQUIRED) find_package(SDL2_mixer REQUIRED) add_executable(FlappyBird src/main.cpp src/Game.cpp src/Bird.cpp src/Pipe.cpp src/ResourceManager.cpp ) target_link_libraries(FlappyBird SDL2::SDL2 SDL2::SDL2main SDL2_image::SDL2_image SDL2_ttf::SDL2_ttf SDL2_mixer::SDL2_mixer )

CMake的好处是跨平台,同一份配置在Linux和Mac上稍作调整就能编译,不用手动改项目属性。

3. 游戏架构:一个主循环撑起整个游戏

3.1 游戏循环的本质

所有实时游戏的核心都是一个"死循环",业内叫Game Loop。它的逻辑极其简单:

while (游戏没退出) { 处理输入(); 更新逻辑(deltaTime); 渲染画面(); }

这三步每秒执行几十次(通常60次),就形成了我们看到的"动起来"的画面。FlappyBird也不例外,鸟的翅膀扇动、管道的移动、分数的跳动,全都是这个循环在背后驱动。

但这里有个关键细节:更新逻辑必须基于时间,而不是基于帧数。什么意思?如果直接写bird.y += 5,那在60帧的机器上鸟每秒下降300像素,在144帧的机器上就变成720像素,游戏难度完全不一样。正确做法是传入一个deltaTime(上一帧到这一帧的秒数),写成bird.y += speed * deltaTime,这样无论帧率多少,鸟的下降速度都是一致的。

3.2 状态机的设计

FlappyBird虽然简单,但至少有四个状态:准备中(Ready)、游戏中(Playing)、碰撞后坠落(Falling)、游戏结束(GameOver)。用状态机管理是最清晰的:

enum class GameState { Ready, Playing, Falling, GameOver };

每个状态下的行为不同:Ready状态下鸟原地上下浮动,点击后进入Playing;Playing状态下鸟受重力影响,管道开始移动;碰撞后进入Falling,鸟快速下坠;落地后进入GameOver,显示分数和重开按钮。

我见过不少新手把所有逻辑塞在一个update()里,用一堆if-else判断,代码很快就乱成一团。状态机的好处是每个状态只关心自己的逻辑,切换时做一次性的初始化,代码可读性和可维护性都高得多。

3.3 类的职责划分

我的实现里主要有这么几个类:

  • Game:持有窗口、渲染器、状态机,负责主循环调度
  • Bird:鸟的位置、速度、重力、跳跃、碰撞盒
  • Pipe:单根管道的上下两部分、位置、移动、碰撞盒
  • PipeManager:管理所有管道,负责生成、回收、碰撞检测
  • ResourceManager:统一加载和缓存纹理、字体、音效
  • ScoreBoard:分数显示和最高分记录

这种划分的核心原则是单一职责:Bird只管鸟自己,Pipe只管管道自己,谁都不越界。碰撞检测放在PipeManager里,因为它需要同时访问鸟和管道。

4. 核心模块拆解:从窗口到碰撞

4.1 窗口与渲染器的初始化

SDL2的初始化分两步:先初始化SDL本身,再创建窗口和渲染器。

if (SDL_Init(SDL_INIT_VIDEO | SDL_INIT_AUDIO) < 0) { SDL_Log("SDL_Init failed: %s", SDL_GetError()); return -1; } SDL_Window* window = SDL_CreateWindow( "FlappyBird", SDL_WINDOWPOS_CENTERED, SDL_WINDOWPOS_CENTERED, 288, 512, SDL_WINDOW_SHOWN ); SDL_Renderer* renderer = SDL_CreateRenderer( window, -1, SDL_RENDERER_ACCELERATED | SDL_RENDERER_PRESENTVSYNC );

这里有两个参数值得说:288x512是FlappyBird原版的逻辑分辨率,用这个尺寸能最大程度还原原版手感;SDL_RENDERER_PRESENTVSYNC开启垂直同步,能防止画面撕裂,同时把帧率锁在显示器刷新率上,避免CPU空转。

4.2 纹理加载与资源管理

游戏里用到的图片有:背景、地面、鸟的三帧动画、上下管道、各种UI元素。如果每次用的时候都去加载一遍,性能会很差。正确做法是启动时一次性加载,用std::unordered_map<std::string, SDL_Texture*>缓存起来:

SDL_Texture* ResourceManager::loadTexture(const std::string& path) { auto it = cache.find(path); if (it != cache.end()) return it->second; SDL_Surface* surface = IMG_Load(path.c_str()); if (!surface) { SDL_Log("IMG_Load failed: %s", IMG_GetError()); return nullptr; } SDL_Texture* texture = SDL_CreateTextureFromSurface(renderer, surface); SDL_FreeSurface(surface); cache[path] = texture; return texture; }

注意:SDL_Surface是CPU内存里的像素数据,SDL_Texture是GPU显存里的纹理。加载完Surface后一定要转成Texture并释放Surface,否则会内存泄漏。

4.3 鸟的物理:重力、跳跃与旋转

鸟的运动是FlappyBird的灵魂。核心就两个变量:垂直速度velocityY和重力加速度gravity。每帧更新:

velocityY += gravity * deltaTime; y += velocityY * deltaTime;

点击时给velocityY一个负值(向上),比如velocityY = -400,鸟就往上飞。重力gravity一般取1200~1500像素/秒²,这个值需要反复调,太小鸟飘得像气球,太大鸟掉得像石头。

原版FlappyBird还有一个细节:鸟会根据垂直速度旋转。上升时头朝上,下降时头朝下。实现方式是:

double angle = std::clamp(velocityY * 0.1, -30.0, 90.0);

然后用SDL_RenderCopyEx带角度渲染。这个细节看起来小,但对"手感"的影响非常大,加上之后整个游戏立刻就有原版那味了。

4.4 管道的生成与回收

管道是FlappyBird的难度来源。每根管道由上下两部分组成,中间留一个缺口。生成逻辑是:

  1. 每隔固定距离(比如180像素)生成一对新管道
  2. 缺口中心Y坐标在合理范围内随机
  3. 缺口高度固定(比如120像素)
int gapCenter = 100 + rand() % 200; int gapHeight = 120; topPipe.rect = {x, 0, PIPE_W, gapCenter - gapHeight/2}; bottomPipe.rect = {x, gapCenter + gapHeight/2, PIPE_W, SCREEN_H - (gapCenter + gapHeight/2)};

管道向左移动,移出屏幕左侧后要回收,否则内存会一直涨。我用一个std::vector<Pipe>配合erase-remove惯用法:

pipes.erase( std::remove_if(pipes.begin(), pipes.end(), [](const Pipe& p) { return p.x + PIPE_W < 0; }), pipes.end() );

4.5 碰撞检测:AABB就够了

FlappyBird的碰撞检测用**AABB(轴对齐包围盒)**完全够用,不需要像素级检测。AABB的判断逻辑就是两个矩形是否重叠:

bool checkCollision(const SDL_Rect& a, const SDL_Rect& b) { return a.x < b.x + b.w && a.x + a.w > b.x && a.y < b.y + b.h && a.y + a.h > b.y; }

但这里有个手感优化的关键点:鸟的碰撞盒不能等于它的贴图大小,否则玩家会觉得"明明没碰到怎么就死了"。正确做法是把碰撞盒缩小一圈,比如贴图是34x24,碰撞盒用28x20,四周各留几像素的宽容度。这个细节是无数玩家吐槽"这游戏判定有问题"的根源,也是原版FlappyBird手感好的秘密之一。

5. 那些教程不会告诉你的实操坑

5.1 帧率不稳定导致的"瞬移"

我第一版实现的时候,鸟偶尔会突然穿过管道,明明前一帧还在缺口上方,下一帧就跑到下面去了。排查了半天才发现是deltaTime过大导致的。当窗口被拖动、系统卡顿的时候,某一帧的deltaTime可能达到0.5秒甚至更多,鸟一帧就移动了几百像素,直接穿过了管道。

解决方案是给deltaTime设上限:

if (deltaTime > 0.05) deltaTime = 0.05;

这样即使系统卡顿,单帧最多按50毫秒计算,鸟不会瞬移。这个技巧在所有实时游戏里都通用,是必加的一道保险。

5.2 音效延迟与声道占用

用SDL_mixer播放音效时,如果每次都用Mix_PlayChannel(-1, chunk, 0),短时间内连续播放同一个音效会出问题——要么被截断,要么延迟明显。正确做法是预分配声道:

Mix_AllocateChannels(16); int channel = Mix_PlayChannel(-1, jumpSound, 0);

或者干脆给每个音效固定一个声道,避免互相抢占。另外,音效文件建议用WAV格式,加载快、延迟低;背景音乐用OGG,体积小、可循环。

5.3 内存泄漏的隐蔽来源

SDL2里最容易泄漏的是Surface和Texture。每次IMG_Load出来的Surface必须SDL_FreeSurface,每次SDL_CreateTextureFromSurface出来的Texture在程序退出前必须SDL_DestroyTexture。我建议在ResourceManager的析构函数里统一清理:

ResourceManager::~ResourceManager() { for (auto& [path, tex] : cache) { SDL_DestroyTexture(tex); } cache.clear(); }

另外,TTF_Font、Mix_Chunk、Mix_Music也都要记得释放。用RAII思想封装成类,能省掉大量手动清理的麻烦。

5.4 最高分持久化

FlappyBird的乐趣很大一部分来自"刷新最高分"。最高分需要持久化到本地文件,否则每次重启就清零了。最简单的方案是写一个文本文件:

void saveHighScore(int score) { std::ofstream file("highscore.txt"); file << score; } int loadHighScore() { std::ifstream file("highscore.txt"); int score = 0; if (file.is_open()) file >> score; return score; }

注意:文件路径要用相对路径或者放到用户目录下,不要写死绝对路径,否则换台机器就找不到文件了。

6. 让游戏"精美"的几个视觉技巧

6.1 视差滚动背景

原版FlappyBird的背景是分层的:远处的城市、近处的树木、最前面的地面,三层以不同速度向左移动,形成视差滚动效果。实现很简单,每层维护自己的x坐标,每帧减去不同的速度:

bgFarX -= 20 * deltaTime; bgNearX -= 50 * deltaTime; groundX -= 100 * deltaTime;

当某一层移出屏幕后,把它重置到右侧,形成无缝循环。这个技巧能让2D画面产生纵深感,是"精美"感的重要来源。

6.2 鸟的帧动画

鸟的翅膀扇动是三帧循环。用一个计时器控制帧切换:

animTimer += deltaTime; if (animTimer > 0.1) { animTimer = 0; currentFrame = (currentFrame + 1) % 3; }

0.1秒一帧,也就是每秒10帧的动画速度,看起来自然又不突兀。这个值可以微调,快一点显得鸟很慌张,慢一点显得悠闲。

6.3 分数跳动的缓动效果

分数增加时如果直接跳变,会显得很生硬。加一个简单的缓动:分数显示值逐渐逼近真实值:

displayScore += (realScore - displayScore) * 10 * deltaTime;

这样分数会有一个"追上去"的动画,视觉上舒服很多。类似的缓动技巧可以用在按钮缩放、管道生成等很多地方,是提升"精致感"的通用手段。

6.4 屏幕震动与粒子效果

碰撞瞬间加一个轻微的屏幕震动,能极大增强打击感。实现方式是渲染时给所有元素加一个随机偏移:

if (shakeTimer > 0) { shakeTimer -= deltaTime; offsetX = (rand() % 10) - 5; offsetY = (rand() % 10) - 5; }

再配合几个羽毛粒子从鸟身上飞散,整个碰撞反馈就非常到位了。这些效果代码量都不大,但对"精美"的贡献是决定性的。

7. 从能跑到好玩:手感调优的实战经验

7.1 重力与跳跃力的黄金比例

FlappyBird的手感核心在于重力和跳跃力的配比。我试过很多组参数,最后稳定在:

参数数值说明
重力1400 px/s²太大鸟掉太快,太小飘
跳跃初速度-420 px/s约等于重力的0.3倍
最大下落速度600 px/s防止鸟掉太快穿模
管道移动速度120 px/s原版手感

这组参数下,鸟从最高点到最低点大约0.6秒,玩家有充足的反应时间,但又不至于太简单。关键是要限制最大下落速度,否则鸟掉到后期会越来越快,直接穿过管道。

7.2 管道间距的难度曲线

固定间距会让游戏很快变得无聊。更好的做法是随分数动态调整:分数越高,管道间距越小、缺口越小。但要设下限,否则会变成不可能完成的任务。

int gapHeight = std::max(100, 140 - score * 2); int pipeSpacing = std::max(140, 200 - score * 3);

这样前10分很轻松,20分开始有压力,30分之后就是高手区了。难度曲线平滑上升,玩家会有"再玩一把就能突破"的冲动。

7.3 输入响应的即时性

FlappyBird的点击必须零延迟。我见过一些实现把输入处理放在渲染之后,导致点击有一帧的延迟,手感立刻就差了。正确顺序是:先处理输入,再更新逻辑,最后渲染。而且输入处理要放在循环的最开头,确保这一帧的点击立刻反映到鸟的位置上。

另外,SDL2的SDL_PollEvent会一次性取出所有事件,如果一帧内玩家点了多次,要全部处理,不能只处理一次。否则快速连点时会丢输入。

7.4 死亡后的短暂停顿

碰撞后不要立刻切到GameOver界面,而是让鸟先坠落1秒左右,再显示结算。这个短暂的"坠落过程"给了玩家情绪缓冲,也让游戏节奏更自然。实现上就是Falling状态持续一段时间后再切GameOver。

8. 打包发布与后续扩展方向

8.1 静态链接与单文件发布

默认情况下SDL2是动态链接的,发布时要带一堆dll。如果想做成单exe,需要静态链接:

  1. 下载SDL2的静态库版本(或者自己用源码编译)
  2. 项目属性里改成链接SDL2-static.lib等静态库
  3. 确保运行时库也设为静态(/MT)

静态链接后exe体积会大一些(几MB),但用户双击就能玩,不用管dll,体验好很多。

8.2 用资源打包避免文件散落

游戏发布时如果图片、音效散落一地,显得很不专业。可以把所有资源打包成一个二进制文件,运行时从内存加载。SDL2提供了SDL_RWFromMem,配合自定义的资源包格式就能实现。这一步不是必须的,但能让项目看起来更完整。

8.3 后续可以扩展的方向

跑通基础版之后,这个项目还有很多可以深挖的地方:

  • 加入道具系统:护盾、磁铁、慢速,增加玩法深度
  • 多人在线排行榜:用简单的HTTP请求上传分数
  • 关卡编辑器:让玩家自己设计管道布局
  • 移植到移动端:SDL2本身支持Android和iOS,改改输入处理就能跑
  • 换成OpenGL渲染:把SDL_Renderer换成OpenGL,学习底层图形管线

我个人觉得最有价值的是移植到移动端这一步。触摸输入和键盘输入的逻辑差异、屏幕适配、性能优化,这些都是纯PC开发接触不到的实战经验,做完之后对整个游戏开发的理解会上一个台阶。

最后分享一个我在这个项目里踩过的坑:不要过早优化。我一开始就想着要做对象池、要做多线程资源加载,结果代码复杂度飙升,bug一堆,游戏反而跑不起来。后来全部推倒重来,先用最朴素的vector和单线程把游戏跑通,再逐步优化,效率反而高得多。先把东西做出来,再考虑做好,这个顺序不能反。

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

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

1. 项目缘起与整体设计思路1.1 为什么选择纯C手写一款FlappyBird很多人第一次接触游戏开发&#xff0c;都是从“复刻经典小游戏”开始的。FlappyBird这个项目尤其典型&#xff1a;规则简单到一句话就能说清——点击屏幕让小鸟穿过管道缝隙&#xff0c;撞到就重来。但真正动手写…

作者头像 李华
网站建设 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;接到各种即时通讯渠…

作者头像 李华