资讯详情 用C语言和easyX实现Flappy Bird:从游戏循环到碰撞检测的完整实践
📅 2026/10/15 13:25:09
简介C语言结合easyX图形库实现的Flappy Bird像素鸟游戏完整源码面向C/C初学者、游戏开发入门者以及希望学习双缓冲绘图与简单物理模拟的编程爱好者。源码采用双缓冲技术避免闪屏包含鸟的飞行物理模拟、管道碰撞检测、游戏状态管理等核心逻辑注释清晰结构易读。压缩包共49个文件约17.09MB主要包含.cpp/.h源码文件、Visual Studio工程文件.sln/.vcxproj、调试生成文件.exe/.pdb/.ilk、游戏美术资源jpg图片与mp3背景音乐以及ReadMe说明文档便于直接打开工程运行或查看代码。已有1394人学习下载。通过阅读这份源码可掌握easyX绘图、动画循环、碰撞检测与音频播放的整合方法还能学习如何组织多状态游戏流程与资源文件适合作为课程设计或入门游戏开发的实战参考。1. 用 C 语言写 flappy birdeasyX 把像素鸟搬进 Windows 控制台第一次在纯 C 环境里做出能玩的 flappy bird那种感觉和用现成引擎拖拽是完全不一样的。C 语言学完指针和结构体之后很多人会卡在「我到底能做什么」这一步控制台黑框里跑不出视觉效果而 easyX 就是那个把窗口、绘图、键盘响应直接塞进 C 语言手里的图形库。这份源码不是教学 Demo而是一个完整可运行的像素鸟游戏麻雀虽小但游戏循环、碰撞检测、计分、渲染、状态切换全都有。适合刚学完 C 语言基础、想搞懂「游戏到底怎么跑起来」的人也适合想把面向过程编程落到一个真实项目里的读者。你不需要懂 Windows API不需要装庞大的引擎只需要一个 easyX 头文件和一份能编译的源码就能看到一只像素鸟在管道之间挣扎。这篇文章会把源码拆开告诉你每一块代码在干什么参数怎么调以及那些不跑一遍根本发现不了的坑。2. 把窗口和游戏循环搭起来帧率、计时器与坐标初始化的玩法2.1 easyX 的初始化与窗口参数怎么定拿到源码第一件事不是看游戏逻辑而是看窗口怎么建的。flappy bird 这个游戏对窗口尺寸很敏感窗口太大鸟的移动速度会显得慢窗口太小管道之间的距离又会让难度陡增。源码里用的是 800x600 的窗口这个尺寸在 easyX 里算是一个比较折中的选择既能看清像素细节又不会让鸟的飞行轨迹显得过于空旷。看 initgraph 这个函数它是整个 easyX 程序的入口相当于在内存里开了一块画布。参数分别是宽、高和窗口模式标志。多数源码会写成initgraph(800, 600)默认是普通窗口如果要全屏可以加SHOWCONSOLE或者NOCLOSE这些标志但做像素鸟这种小游戏普通窗口就够了全屏反而会让像素颗粒被拉伸得很难看。#include graphics.h #include conio.h int width 800; int height 600; void init_game_window() { initgraph(width, height); setbkcolor(RGB(135, 206, 235)); // 天空蓝背景 cleardevice(); }逻辑说明initgraph 创建窗口setbkcolor 设置背景色cleardevice 用背景色填充整个画布。RGB 宏的三个参数分别是红绿蓝分量135, 206, 235 是天空蓝这个颜色值在 flappy bird 里比纯白背景舒服眼睛不累。如果发现鸟和背景区分不开可以改成 RGB(255, 255, 255) 纯白但管道颜色也要相应调深。参数说明窗口宽高不要随意放大easyX 的绘图函数是纯 CPU 绘制窗口越大每帧重绘的开销越大。800x600 在普通笔记本上跑 60 帧没问题如果拉到 1920x1080帧率会掉到 40 左右鸟的移动手感就变了。想改尺寸的话记得同步调整鸟的初始坐标、管道的间距和地面高度这三个值是绑定的。2.2 游戏循环和帧率控制while 循环不是越空越好游戏循环是整个 flappy bird 的心脏。控制台程序写惯了很容易把循环写成while(1) { 处理逻辑; }不带任何节流。这在纯计算程序里没问题但在图形程序里CPU 会疯狂空转窗口会闪烁鸟飞行的速度会因为机器性能不同而时快时慢。源码里用的是经典的计时器控制方案。先记录当前时间处理完一帧逻辑后计算耗时如果不满目标帧间隔就睡一会儿。目标帧率一般定在 60 帧每秒也就是每帧 16 毫秒左右。用getTickCount()获取系统毫秒数或者用clock()函数两者在 easyX 里都能用。#include time.h int frame_interval 16; // 目标帧间隔毫秒 void game_loop() { while (1) { DWORD start_time GetTickCount(); handle_input(); update_logic(); render(); DWORD cost GetTickCount() - start_time; if (cost frame_interval) { Sleep(frame_interval - cost); } } }逻辑说明每一帧开始先记录时间逻辑处理完算一下花了多久如果比 16 毫秒快就睡掉剩余时间保证每帧节奏均匀。Sleep 的单位是毫秒这是 Windows 系统 APIeasyX 的运行环境是 Windows所以可以直接用。这样做的目的是把游戏速度从 CPU 性能中解耦否则同一份代码在不同电脑上跑难度完全不同。参数说明frame_interval 设 16 是 60 帧设 33 是 30 帧。frame 值越小鸟的位移更新越频繁看起来更流畅但如果你把管道的移动速度设得很快小帧率下会有穿越隧道的眩晕感。游戏手感出现「鸟偏沉」或者「鸟偏飘」的时候优先调这个值而不是去调重力加速度。2.3 鸟类、管道、背景的对象建模与坐标约定easyX 是面向过程的绘图库不要求你用面向对象但这份源码里还是把游戏元素拆成了结构体。鸟是一个圆形管道是上下两个矩形地面是一条横线。这样做的好处是碰撞检测和绘制代码都变得非常直接。鸟的位置用两个坐标记录x 和 y。x 基本不变固定在某一个值上因为鸟不左右移动游戏是背景和管道向左移动来制造前进感。y 才是变量受重力和点击影响。管道用两个结构体数组管理每一个管道元素包含上管道矩形区域的下边缘和下管道矩形区域的上边缘。这个设计非常关键因为小鸟实际上是在两个矩形之间飞只要 x 落在管道区域内就得检查 y 是否在洞里。typedef struct { int x; int y; int radius; int velocity; } Bird; typedef struct { int x; int top_rect_bottom; // 上管道底部 y 坐标 int bottom_rect_top; // 下管道顶部 y 坐标 int passed; // 是否已计分 } Pipe; void init_bird(Bird* bird) { bird-x 200; bird-y 300; bird-radius 15; bird-velocity 0; }逻辑说明结构体把鸟和管道的所有属性集中在了一起。Bird 里的 velocity 是垂直速度每帧都会加上重力加速度的值。Pipe 里的 passed 是用来判断是否已经给这组管道计过分的标志位防止同一根管道穿过两次加两分。init_bird 把鸟放在窗口左三分之一的位置这是 flappy bird 的标准出发点太靠左会让玩家来不及反应太靠右视野空间又不足。参数说明bird-radius 是绘制小鸟的半径改成 12 会变小碰撞判定区域也变小容错率会增加改成 18 会更难因为鸟的像素面积大更容易碰到管道边缘。新手想试水可以把 radius 稍微调小一点手感会友好很多。pipe 数组的长度一般是 3 到 4 个太少屏幕会空太多逻辑处理量变大3 个在 800x600 窗口下刚好覆盖整个屏幕宽度。3. 核心机制拆解重力、点击升降与管道生成的三个关键参数3.1 重力加速度像素鸟漂浮感的唯一来源flappy bird 最核心的手感来自重力模型。不是简单的 y 坐标每帧减一个固定值那样鸟飞起来像纸片落下去像电梯。真实的像素鸟用的是「加速度累积速度速度累积位移」的双层模型。每帧更新时velocity 先加上一个重力常数然后 y 坐标再加上 velocity。这个过程用代码写出来就是三行但手感差异全靠重力常数的数值决定。源码里常见的重力值是 0.5 或者 0.6配合 16 毫秒帧间隔刚好让鸟的飞行轨迹呈现出一道好看的抛物线。#define GRAVITY 0.5 #define JUMP_VELOCITY -8.0 void update_bird(Bird* bird) { bird-velocity GRAVITY; bird-y (int)bird-velocity; }逻辑说明GRAVITY 是每帧给 velocity 增加的量。velocity 是浮点数累加多次后再取整赋给 y 坐标。这就是为什么鸟的下落看起来越来越快——速度在累积不是匀速运动。JUMP_VELOCITY 是负数因为屏幕坐标系里 y 轴向下为正负值意味着向上移动。玩家按下空格键时velocity 直接被重置为 -8.0之后重力再逐步把鸟往下拉。参数说明GRAVITY 调到 0.8 时鸟会明显变重下落速度快需要频繁点击调到 0.3 时鸟轻飘飘的飞行轨迹很飘新手反而不好控制。JUMP_VELOCITY 是上跳初速度-8 时鸟每次点击能上升大约 80 像素距离如果改成 -10鸟跳得更高管道之间的容错窗口变大难度会显著降低。这两个值建议成对调想降低难度就同时降低 GRAVITY 和 JUMP_VELOCITY 的绝对值。3.2 管道生成逻辑这几行代码决定了「运气」成分管道生成是这份源码里最值得逐行读的部分。核心逻辑是当最右侧的管道移出屏幕后在屏幕右侧生成一个新的管道新管道的缝隙位置是随机的但缝隙高度要保持在一个合理范围内。源码里的管道缝隙高度一般固定在 150 像素这是一个经过检验的数值。150 像素意味着占窗口高度的四分之一配合鸟的 15 像素半径体型刚好能通过但容错空间有限。缝隙的 y 坐标随机范围是 100 到 400这个范围要保证上管道不会太高玩家看不到顶下管道不会太低鸟落地前没有反应时间。#define PIPE_GAP 150 #define PIPE_SPACING 250 void spawn_pipe(Pipe* pipe, int right_edge_x) { pipe-x right_edge_x; int min_y 100; int max_y height - PIPE_GAP - 100; int gap_center min_y rand() % (max_y - min_y); pipe-top_rect_bottom gap_center - PIPE_GAP / 2; pipe-bottom_rect_top gap_center PIPE_GAP / 2; pipe-passed 0; }逻辑说明gap_center 是缝隙中心点的 y 坐标管道上下边缘根据中心点和缝隙高度推导出来。min_y 和 max_y 的设定避免了管道贴边的情况——如果上管道顶部低于 100玩家完全看不到管道边缘视觉上很突兀如果下管道底部接近地面玩家没有反应空间。rand() 的实现在 easyX 项目里可以直接用标准库的但记得在 main 函数里 srand(time(NULL)) 初始化随机种子否则每次启动游戏的管道位置序列都一样玩十次就背板背出经验了。参数说明PIPE_GAP 是缝隙高度这是整个游戏难度最敏感的指标。150 是原版的经典值改成 170 就明显简单改成 130 就非常难。PIPE_SPACING 是相邻两组管道的水平间隔250 像素在 800 窗口下意味着同一时刻屏幕上有 3 组管道改成 200 的话屏幕上会有 4 组压力陡增改成 300 的话游戏节奏明显变慢适合做教学演示版。想调整难度优先调 PIPE_GAP其次是 PIPE_SPACING最后才碰重力参数。3.3 碰撞检测的判定范围是个细节活碰撞检测代码在 flappy bird 里看起来很简单就是「鸟的圆形区域是否和矩形区域相交」但实际实现时有个容易忽略的问题鸟的绘制是圆形碰撞检测区域如果用矩形会出现鸟的左上角和右下角明明没碰到管道却被判定死亡的情况。通常的做法是做一个缩写版的碰撞框。鸟的实际碰撞区域是圆心周围的一个正方形边长略小于直径。比如鸟的半径是 15绘制时用 30x30 的矩形区域来画但做碰撞判定时可能只取 24x24 的区域相当于给玩家留了一些视觉错觉容错。bool check_collision(Bird* bird, Pipe* pipe) { int hitbox_size bird-radius * 2 - 6; // 略微缩小碰撞区 int bird_left bird-x - hitbox_size / 2; int bird_right bird-x hitbox_size / 2; int bird_top bird-y - hitbox_size / 2; int bird_bottom bird-y hitbox_size / 2; if (bird_right pipe-x || bird_left pipe-x 60) { return false; // 不在管道横坐标范围 } if (bird_top pipe-top_rect_bottom bird_bottom pipe-bottom_rect_top) { return false; // 在缝隙中 } return true; }逻辑说明第一步先检查鸟的碰撞框是否落在管道的 x 轴范围内不在就直接放行不做复杂判定。第二步检查鸟是否完全在上下管道之间的缝隙里如果是也放行。剩下的情况就是撞上了。这段代码有一个隐含前提管道宽度是固定的 60 像素这是源码里定义好的常量。参数说明hitbox_size 的- 6就是让碰撞判定比视觉区域小一圈的技巧。这个数值不固定你觉得游戏太苛刻就改成 -8 或者 -10太宽松就改成 -2。管道宽度 60 是像素值改成 70 的话视觉上管道变粗碰撞判定面积也变大游戏更难。碰撞判定这里不要追求绝对的像素级精确游戏是靠手感吃饭的给玩家一点「以为自己没撞上」的错觉反而更容易上瘾。4. 渲染与游戏状态让 bird 的每一帧都有「像素感」4.1 绘制函数怎么写出像素风格easyX 的绘图函数不是很多像素鸟这种游戏用到的基本上就是fillcircle、solidrectangle、outtextxy这几个。像素风格不是说要用像素画素材这里说的像素感是一种视觉风格——大色块、清晰的边缘、没有渐变和模糊效果。绘制鸟的时候源码里一般会用 fillcircle 先画身体然后在眼睛的位置画一个白色的小圆点。如果想让鸟更生动可以加一个简单的翅膀摆动效果用setlinecolor配合line函数画一条斜线。但是这种细节是加分项不是必须项先跑通再美化。void draw_bird(Bird* bird) { setfillcolor(RGB(255, 220, 50)); // 鸟身颜色 fillcircle(bird-x, bird-y, bird-radius); setfillcolor(RGB(255, 255, 255)); // 眼睛 fillcircle(bird-x 8, bird-y - 5, 4); setfillcolor(RGB(0, 0, 0)); // 瞳孔 fillcircle(bird-x 10, bird-y - 4, 2); }逻辑说明fillcircle 的三个参数是圆心坐标和半径。鸟头的朝向是右侧所以眼睛画在 x 正方向偏一点的位置。颜分量 RGB 直接写死会让代码可读性变差建议定义成宏常量。整个绘制过程每一帧都会执行画面看起来是「闪」出来的但因为有 60 帧刷新人眼察觉不到闪烁。参数说明setfillcolor 设置的是之后绘制图形的填充色一旦设置后续所有图形绘制都会用这个颜色直到再次调用 setfillcolor。所以绘制完鸟之后绘制管道之前一定要重新调用 setfillcolor 设置管道颜色否则管道会变成鸟的颜色。这是 easyX 项目里最容易犯的连续性错误代码看起来没错但画面就是不对。4.2 游戏状态机的转换开始、运行、死亡、重开flappy bird 看起来是个简单的游戏但状态转换的处理决定了代码的清晰度。源码里一般会用一个枚举类型定义游戏状态READY、RUNNING、DEAD。状态不同逻辑处理也不同。READY 状态下鸟停在一个固定的高度上下浮动此时点空格键不跳跃而是把状态切换到 RUNNING。RUNNING 状态下鸟受重力影响点空格才跳。DEAD 状态下鸟直线坠落管道的移动和碰撞检测全部停止通常在鸟落地后或者玩家按任意键后回到 READY。typedef enum { READY, RUNNING, DEAD } GameState; void switch_state(GameState* state, GameState new_state) { *state new_state; if (new_state READY) { // 重置所有游戏数据 } }逻辑说明状态机的好处是每一帧的逻辑处理可以用 switch 分支代码结构不会随着游戏逻辑增多而混乱。很多控制台程序之所以写着写着就乱成一锅粥就是因为没有状态划分所有逻辑都堆在主循环里。READY 状态的浮动效果可以这样实现让鸟的 y 坐标在初始位置附近做正弦摆动公式是y base_y sin(time * speed) * amplitude。参数说明DEAD 状态下不要立刻跳出循环给一个堕落的动画时间。常见做法是设置一个dead_timer变量死亡后继续循环 60 帧再做重开处理期间 bird-velocity 恢复为 0鸟会因为重力自然下落视觉上更连贯。这个细节很多初版写的人会漏掉直接弹回 READY玩家会觉得很突兀。4.3 计分逻辑和碰撞死亡后的收尾处理计分看起来是最简单的模块但有一个特别容易被忽略的边界条件什么时候算「通过了一组管道」最粗略的写法是鸟的 x 坐标大于管道的 x 坐标就算通过但这种写法在管道移动速度快的帧里会出现漏判——上一帧还没过下一帧已经移过去了没有经过「正中间」那个时刻。正确做法是利用Pipe.passed标志位。每一帧检查如果管道还没有被标记为已通过并且鸟的 x 坐标已经大于管道的右边缘才加分并标记。void update_score(Bird* bird, Pipe pipes[], int* score) { for (int i 0; i MAX_PIPES; i) { if (!pipes[i].passed bird-x pipes[i].x PIPE_WIDTH) { pipes[i].passed 1; (*score); } } }逻辑说明PIPE_WIDTH 是管道宽度管道右边缘才是真正的通过判定线。passed 标志确保同一组管道永远不会被重复计分。分数变量用指针传递是为了在主循环里也能读取到最新值避免函数返回和拷贝带来的同步问题。5. 避坑指南easyX 项目最常见的五个翻车现场5.1 编译报错头文件找不到或者链接失败现象把源码拿过来代码看起来没有明显错误但编译时提示fatal error: graphics.h: No such file or directory或者链接阶段报error: undefined reference to initgraph。原因easyX 不是一个标准库头文件它需要单独安装到编译环境的包含路径里。很多人只下载了源码没有装 easyX 库或者装了但 IDE 没有配置好头文件目录。解决去 easyX 官网下载对应版本的安装包。安装包会往 VS 的 include 和 lib 目录里写入文件和库文件。如果你用的是 Dev-C 或者 Code::Blocks需要手动把 easyX 的头文件和库文件路径加到编译器设置里。这一步配置不对后面所有操作都白费先确认能跑通一个空窗口项目再开始搬游戏代码。5.2 中文路径导致运行直接崩溃现象项目文件放在带中文或空格名的目录下点击运行后窗口一闪而过或者弹出一个奇怪的路径错误提示框。原因easyX 的某些版本对中文路径支持不好尤其是图片加载函数loadimage这类涉及到文件操作的接口。游戏源码如果不涉及外部素材文件理论上不受影响但很多人习惯把项目放在「D:\C语言项目\」这类目录里链接阶段就可能出问题。解决把整个项目工程移到纯英文路径下再编译。这个坑非常隐蔽因为编译能通过运行却崩会让你以为是代码问题花几个小时排查逻辑代码最后发现只是路径问题。5.3 窗口关闭后进程仍然残留现象游戏运行正常关掉窗口后IDE 里显示程序还在运行任务管理器里有残留进程再次编译会提示「无法打开程序数据库」。原因easyX 的程序如果直接关掉窗口进程不一定会自动退出。game_loop 是一个死循环窗口关闭事件没有主动终止循环进程就一直在后台挂着。解决在游戏主循环里检测窗口关闭事件。easyX 提供的getchar()或kbhit()可以读取按键输入但窗口的 X 按钮不会触发输入事件。需要在循环里判断GetAsyncKeyState(VK_ESCAPE)有专门的接口监听窗口消息检测到关闭就弹出循环再调用 closegraph 清理资源。5.4 字体乱码outtextxy 输不了中文字符串现象想要在屏幕上显示「游戏结束」「得分」这种中文提示用 outtextxy 打印后显示为乱码。原因easyX 的 outtextxy 默认使用的字符编码是简体中文但 IDE 源文件的编码如果不是 GBK编译器会把字符串常量转成 UTF-8 存到程序里运行时就被 easyX 错误解码了于是显示为乱码。解决源码文件和编译器的字符集必须匹配。VS 里有两种处理办法一是把源文件存成 GB2312 编码二是在代码里用#include locale.h配合setlocale(LC_ALL, )。如果你用的是中文字符串最好在 C 语言层面就不碰中文游戏提示用英文GAME OVER和SCORE代替省掉一整个编码问题。5.5 图片加载不出来loadimage 和当前目录的绑定关系现象用 loadimage 加载像素鸟的素材图片运行时弹窗提示找不到文件但图片明明就在项目目录里。原因loadimage 的文件路径是相对当前工作目录的而 IDE 运行程序时的工作目录不一定是源代码所在目录可能是项目的 Debug 文件夹也可能是工程根目录。解决不依赖外部图片文件所有图形都用 easyX 的绘图函数直接画。这个项目叫「像素鸟」但本质上是用代码画出来的像素风不是贴图流。如果你想换素材图片就把图片放到程序运行的目录下或者用绝对路径先测试通路。6. 调试技巧帧数统计和坐标可视化帮你快速定位手感问题游戏写完了能跑但总觉得哪里不对说不出是鸟太重还是管道太密。这时候不要凭感觉瞎猜参数直接开启调试模式把关键数据画到窗口上。最常见的做法是在窗口左上角用 outtextxy 实时显示当前帧率和鸟的坐标。void debug_info(Bird bird, int fps, int score) { char debug_str[64]; sprintf(debug_str, FPS: %d Score: %d Y: %d V: %.1f, fps, score, bird.y, bird.velocity); setfillcolor(RGB(0, 0, 0, 100)); solidrectangle(10, 10, 400, 30); settextcolor(RGB(255, 255, 255)); outtextxy(15, 15, debug_str); }调试数据比手感判断更客观。比如你觉得鸟下坠太快看 Y 坐标每帧的变化量就知道是不是重力太大了你觉得管道生成位置不自然把每根管道的top_rect_bottom和bottom_rect_top打印出来能看到随机分布是否均匀。这种调试信息的价值在于它让你把所有「感觉」变成「数据」调参不再是玄学而是对照数值做判断。帧率统计尤其关键。easyX 项目最常见的隐性问题是帧率波动——机器性能好时 60 帧性能差时 30 帧游戏速度完全不一样。我一般会在循环开头记录时间戳循环结尾再算一次得到这一帧的耗时然后做一个滑动平均。如果帧率低于 55先不急着调游戏参数先优化渲染代码——比如把每帧重新绘制全窗口改成只绘制变化的区域或者把不必要的 fillcircle 改成更简单的putpixel像素级绘制。从这个角度说帧率是整个项目的一等公民不把帧率稳住后面调什么都白搭。最后一个技巧是「一键降难度」调试开关。在代码里加一个#define DEBUG_EASY 1开启后管道间隙自动加宽 20 像素、鸟的碰撞框缩小 4 像素、重力减半。这样别管手感参数怎么调玩家想体验流程就能直接切简单模式想挑战就关掉开关用默认值。这种设计不是游戏功能纯粹是给测试者和自己用的但实际效果特别好——你让一个没接触过这游戏的人帮你测试他开着简单模式能玩到 30 分关了简单模式玩到 5 分就死你立刻就能判断出手感平衡是否合理。从那以后我每次做这种 C 语言小游戏都会强制走一遍「帧率可视化 难度开关」这套流程省掉了大量反复编译测试的等待时间。希望帮到你。本文还有配套的精品资源点击获取