用C++和SDL2从零实现《飞翔的小鸟》游戏

1. 项目概述

《飞翔的小鸟》是一款经典的2D休闲游戏,玩家通过点击屏幕控制小鸟飞行高度,躲避随机生成的管道障碍物。这个用纯C++实现的版本完全从零开始构建,不依赖任何游戏引擎,展示了如何用最基础的图形库实现完整游戏循环。

我在大学计算机图形学课程中首次接触这个项目,后来在多个技术分享会上反复重构优化。这个实现特别适合想深入理解游戏底层原理的开发者,代码量控制在800行左右,但完整包含了碰撞检测、物理模拟、状态机等核心游戏机制。

2. 技术架构设计

2.1 跨平台图形方案

选用SDL2(Simple DirectMedia Layer)作为图形库,相比OpenGL更轻量,且完美支持Windows/Linux/macOS三平台。关键配置如下:

SDL_Window* window = SDL_CreateWindow("Flappy Bird", SDL_WINDOWPOS_CENTERED, SDL_WINDOWPOS_CENTERED, 288, 512, SDL_WINDOW_SHOWN); SDL_Renderer* renderer = SDL_CreateRenderer(window, -1, SDL_RENDERER_ACCELERATED | SDL_RENDERER_PRESENTVSYNC);

注意:必须检查SDL_Init()返回值,我在早期版本曾因忘记初始化音频子系统导致随机崩溃

2.2 游戏对象建模

采用面向对象设计,主要类结构:

class Bird { float y, velocity; // 垂直位置和速度 SDL_Texture* texture; public: void flap(); void update(); void render(SDL_Renderer*); }; class PipePair { int x, gapY; static const int GAP_HEIGHT = 100; public: bool isOffscreen() const; bool collidesWith(const Bird&) const; void update(); };

2.3 游戏主循环

标准游戏循环包含三个关键阶段:

while (running) { Uint32 frameStart = SDL_GetTicks(); processInput(); // 处理键盘/鼠标事件 updateGame(); // 更新所有对象状态 renderFrame(); // 绘制当前帧 // 控制帧率为60FPS Uint32 frameTime = SDL_GetTicks() - frameStart; if (frameTime < 16) SDL_Delay(16 - frameTime); }

3. 核心算法实现

3.1 物理系统简化模型

小鸟飞行采用简化的牛顿力学模型:

void Bird::update() { velocity += GRAVITY; // 重力加速度 y += velocity; // 角度旋转效果 if (velocity < 0) angle = -25; // 向上飞时抬头 else angle = min(90, angle + 3); // 下落时低头 }

参数调优经验:

  • 重力常数GRAVITY建议0.3~0.6之间
  • 点击后的初始速度推荐设为-8到-10
  • 旋转角度变化率影响手感,需要反复测试

3.2 碰撞检测优化

管道碰撞使用矩形相交检测,但对小鸟做了圆形近似处理:

bool PipePair::collidesWith(const Bird& bird) const { // 小鸟碰撞半径 const int BIRD_RADIUS = 12; // 上管道检测 if (bird.x + BIRD_RADIUS > x && bird.x - BIRD_RADIUS < x + PIPE_WIDTH) { if (bird.y - BIRD_RADIUS < gapY - GAP_HEIGHT/2) return true; } // 下管道检测同理... }

3.3 无限关卡生成

管道采用对象池模式循环利用:

vector<PipePair> pipes; void generatePipes() { if (pipes.back().x < 200) { // 最后一个管道快离开屏幕 int gapY = rand() % 200 + 150; // 随机间隙位置 pipes.emplace_back(288, gapY); // 新管道从右侧进入 } // 移除屏幕外的管道 pipes.erase(remove_if(pipes.begin(), pipes.end(), [](const PipePair& p) { return p.isOffscreen(); }), pipes.end()); }

4. 性能优化技巧

4.1 纹理加载最佳实践

所有纹理在游戏初始化时预加载:

SDL_Texture* loadTexture(const string& path) { SDL_Surface* surface = IMG_Load(path.c_str()); if (!surface) { cerr << "Failed to load image: " << IMG_GetError(); return nullptr; } SDL_Texture* tex = SDL_CreateTextureFromSurface(renderer, surface); SDL_FreeSurface(surface); return tex; }

关键点:使用SDL_image库支持PNG透明通道,记得调用IMG_Init(IMG_INIT_PNG)

4.2 渲染批处理

避免频繁切换渲染状态:

void Game::render() { SDL_RenderClear(renderer); // 1. 先绘制背景(最底层) SDL_RenderCopy(renderer, bgTexture, NULL, NULL); // 2. 批量绘制所有管道 for (auto& pipe : pipes) pipe.render(renderer); // 3. 最后绘制小鸟(在最上层) bird.render(renderer); SDL_RenderPresent(renderer); }

4.3 内存管理要点

使用RAII管理SDL资源:

struct TextureDeleter { void operator()(SDL_Texture* t) const { if (t) SDL_DestroyTexture(t); } }; unique_ptr<SDL_Texture, TextureDeleter> birdTexture;

5. 常见问题排查

5.1 画面撕裂问题

症状:快速移动时出现水平撕裂线 解决方案:

// 创建渲染器时启用垂直同步 SDL_CreateRenderer(window, -1, SDL_RENDERER_ACCELERATED | SDL_RENDERER_PRESENTVSYNC);

5.2 音频延迟问题

使用SDL_mixer的正确初始化方式:

if (Mix_OpenAudio(44100, MIX_DEFAULT_FORMAT, 2, 2048) < 0) { cerr << "SDL_mixer init error: " << Mix_GetError(); } // 预加载音效 Mix_Chunk* wingSound = Mix_LoadWAV("wing.wav");

5.3 跨平台编译问题

CMake配置示例:

find_package(SDL2 REQUIRED) find_package(SDL2_image REQUIRED) find_package(SDL2_mixer REQUIRED) add_executable(flappy_bird src/main.cpp src/Bird.cpp src/Pipe.cpp) target_link_libraries(flappy_bird PRIVATE SDL2::SDL2 SDL2::SDL2_image SDL2::SDL2_mixer)

6. 项目扩展方向

6.1 添加粒子特效

实现爆炸火花效果:

struct Particle { float x, y, vx, vy, lifetime; }; vector<Particle> particles; void createExplosion(float x, float y) { for (int i = 0; i < 50; ++i) { float angle = rand() % 360 * M_PI / 180; float speed = 1 + (rand() % 5); particles.push_back({ x, y, cos(angle) * speed, sin(angle) * speed, 60 // 60帧生命周期 }); } }

6.2 存档系统实现

使用JSON保存最高分:

#include <nlohmann/json.hpp> void saveHighScore(int score) { json j; j["high_score"] = score; ofstream file("save.json"); file << j.dump(); } int loadHighScore() { ifstream file("save.json"); if (!file) return 0; json j; file >> j; return j.value("high_score", 0); }

6.3 触摸屏适配

针对移动设备优化:

// 在SDL事件循环中处理触摸事件 case SDL_FINGERDOWN: bird.flap(); playSound(wingSound); break;

这个项目最让我有成就感的是看到完全从零构建的游戏能流畅运行。建议尝试给小鸟添加翅膀动画效果,只需要在Bird类中添加帧计时器即可实现简单的精灵动画。