简介这是一份使用C语言与C编写的Flappy Bird像素鸟小游戏完整工程基于easyX图形库实现适合C/C初学者学习游戏开发入门。代码采用双缓冲绘图技术避免画面闪烁包含鸟类物理模拟、管道碰撞检测和游戏状态切换等核心逻辑且注释较清晰。压缩包共49个文件大小17.09MB主要包含cpp/h源码、sln/vcxproj工程文件、可直接运行的exe、17张按钮/小鸟/背景/标题图片素材、两首mp3背景音乐以及编译生成的pdb/obj等调试文件。目前已有1394人学习下载。通过该项目读者可以掌握easyX绘图流程、双缓冲动画原理及简单游戏循环设计并可将源码作为模板扩展玩法是一个结构完整、可编译运行的入门级游戏开发范例。1. 用C语言写一个能跑起来的像素鸟比想象中更能锻炼底层思维如果你已经在网页或者Python里写过游戏再回头看C语言版本的flappy bird最直观的感受往往是“原来游戏引擎帮你干了那么多活”。这个项目用easyX图形库在Windows下从零实现像素鸟没有现成的物理引擎没有精灵表加载器小鸟的下落、上升、管道移动全部都要自己算坐标、画矩形、做碰撞检测。把它跑起来并读完代码你对窗口坐标系、消息循环、贴图刷新这些概念的理解会比看十篇教程都扎实。项目适合两类人一类是刚学完C语言基础、想找个完整项目练手的在校同学一类是想理解简单2D游戏内部机制的开发者。前者能从中看到结构体怎么组织游戏状态、函数怎么划分模块后者能关注到一个非阻塞输入检测和定时刷新是如何配合的——这套东西在单片机和嵌入式上同样用得上。整个源码只有几个C文件依赖一个easyX库跑起来之后会发现麻雀虽小五脏俱全。2. 技术选型与工程结构为什么是easyX代码文件如何分工2.1 easyX图形库的定位与适用场景easyX是Windows平台下专为C/C教学设计的图形库它本质上是封装了Win32 API的绘图接口让开发者用类似Turbo C时代BGI库的方式操作窗口。选它而不是SDL或SFML最大的理由就是入门门槛低不需要配置复杂的链接库路径不需要理解渲染管线和事件循环你只需要调用initgraph(width, height)初始化窗口再用circle、fillrectangle这类函数画图即可。但要说明白的是easyX覆盖的是2D软件渲染的直接绘制它并不提供“精灵”、“场景”、“碰撞体”这类游戏对象概念。这意味着所有游戏逻辑都要自己从坐标层面实现小鸟的位置是一个float类型的变量管道是两组矩形坐标游戏状态用枚举或者int常量区分。这种“什么都要自己写”的模式恰恰是这个项目最有教学价值的地方。从开发调试的角度看easyX还有一个好处它的窗口可以像普通Windows窗口一样被拖动、关闭出错时会弹出标准的运行时错误提示对比裸写Win32程序要友好得多。有一点值得注意easyX只支持Windows MSVC环境用MinGW或者Linux运行会直接失败。这是它在教学场景之外最大的局限性。所以如果你手头的编译器是Dev-C的MinGW版本需要做额外配置甚至换用Visual Studio这在后面运行章节会详细展开。2.2 源文件与头文件的分工逻辑整个项目拆得很清晰基本延续C语言经典的多文件组织方式。常见的文件划分是一个入口文件负责程序启动和主循环一个文件专门做游戏初始化与资源加载一个文件处理用户输入还有一组工具函数负责碰撞检测和分数计算。如果工程写得规整你还会看到单独的struct.h定义所有数据结构。这种拆分的意义在于每个模块可以独立测试修改而不是在一个几百行的main.c里靠注释区分功能块。比如在典型的实现中game.h会集中声明如下内容#ifndef GAME_H #define GAME_H // 窗口与逻辑尺寸 #define WINDOW_WIDTH 600 #define WINDOW_HEIGHT 800 // 游戏状态 typedef enum { STATE_READY, // 等待开始 STATE_PLAYING, // 运行中 STATE_OVER // 游戏结束 } GameState; // 小鸟结构体坐标是float是为了做连续位移 typedef struct { float x; float y; float vy; // y轴速度正数代表向下 float gravity; // 每帧加速度 float jumpSpeed; // 上升瞬间初速度 } Bird; // 管道结构体上下两根共用一个x坐标 typedef struct { int x; int gapY; // 缺口中心y坐标 int gapHeight; // 缺口高度 } Pipe; extern Bird bird; extern Pipe pipes[3]; // 同时显示3根管道 void updateGame(); void renderGame(); void handleInput(); #endif从这段代码能读出几个关键设计取舍。一是窗口尺寸用了宏定义如果你想改成竖屏手机比例只需要改这两个数字而是游戏状态用枚举而不是魔法数字这样在switch分支里阅读性很强三是管道数组长度固定为3原因是屏幕宽度有限任何时刻最多能显示三根管子这样做不需要动态内存管理对C语言初学者极度友好。2.3 资源文件与依赖说明下载的源码里通常不包含任何图片或音频资源因为easyX可以直接用函数绘制像素风图形。小鸟的身体是一个画了眼睛的椭圆管道是绿色矩形加黑色描边背景是渐变天空蓝。这样做的好处是解压即可编译不存在资源路径问题缺点是视觉效果非常朴素后期如果想替换成图片素材需要自己研究loadimage函数和透明贴图——这实际上是一个很好的进阶方向。项目根目录下面一般就是一个.sln解决方案文件、几个.c和.h文件外加一个README.txt说明运行环境。没有额外的assets目录也没有数据库或配置文件。整个项目处于“复制到某目录就能开工”的干净状态。3. 核心代码拆解游戏循环、物理模拟与碰撞检测的完整链路3.1 主循环与帧率控制让游戏以稳定节奏运行游戏的主循环是整个程序的心脏它做三件事处理输入、更新逻辑、渲染画面。这个循环不能简单地用while(1)跑到底因为刷新太快会让游戏速度快慢取决于CPU性能。常见做法是配合Windows的时间函数做帧率限制让逻辑更新的节奏稳定在60帧左右。#include graphics.h #include conio.h #include time.h int main() { initgraph(WINDOW_WIDTH, WINDOW_HEIGHT); srand((unsigned)time(NULL)); initGame(); while (true) { // 当前时间单位毫秒 DWORD start GetTickCount(); handleInput(); updateGame(); renderGame(); // 每帧至少间隔16ms约60帧 while (GetTickCount() - start 16) { Sleep(1); } // 窗口关闭时主动退出 if (isWindowClosed()) break; } closegraph(); return 0; }这里的GetTickCount是Windows自带的毫秒级计时接口第一次调用记录帧开始时间渲染完成后计算耗时如果不足16毫秒则休眠补足差额。这种“忙等加休眠”的模式在游戏教学里是最直观的方案。注意Sleep(1)是把当前线程挂起至少1毫秒避免CPU空转到100%。如果你在别的代码里看到只调用Sleep(10)而没有统计帧耗时那其实是会吃满单核的双刃剑方案——画面不会闪烁但功耗很高。3.2 物理更新重力、跳跃速度与位移累计很多刚上手的人会把小鸟的移动写成x speed这种匀速直线运动那手感会很僵硬。正确做法是引入重力加速度概念每一帧先让速度叠加一个向下的加速度再把速度加到位置上。#define GRAVITY 0.4f #define JUMP_FORCE -6.5f void updateBird() { // 速度先受重力影响 bird.vy GRAVITY; // 速度上限防止下落过快穿模 if (bird.vy 9.0f) { bird.vy 9.0f; } // 位移 bird.y bird.vy; // 顶部碰撞不允许飞出屏幕上方 if (bird.y 0) { bird.y 0; bird.vy 0; } // 底部碰撞碰到地面算游戏结束 if (bird.y WINDOW_HEIGHT - FLOOR_HEIGHT) { bird.y WINDOW_HEIGHT - FLOOR_HEIGHT; gameState STATE_OVER; } }注意Jump_FORCE是负值因为在easyX的坐标系里y轴正方向是朝下的窗口左上角是原点。graphics库沿用了屏幕坐标系习惯这与数学笛卡尔坐标系相反。如果把这个符号搞反你的小鸟就会按下空格直接栽到地板下面。速度上限9.0f是防止帧间隔波动时位移过大——虽然我们已经限帧但在窗口被拖动或系统繁忙时仍可能出现帧间隔变长没有速度上限会出现管道瞬移穿模的情况。void onKeyPress() { // 只在按下空格或鼠标左键时触发跳跃 // 不用getch()阻塞等待改用非阻塞检测 if (GetAsyncKeyState( ) 0x8000 || GetAsyncKeyState(VK_LBUTTON) 0x8000) { if (gameState STATE_READY) { gameState STATE_PLAYING; } bird.vy JUMP_FORCE; } }这里的关键细节是GetAsyncKeyState而不是getch。如果用控制台的getch程序会阻塞在等待按键的状态主循环会卡住小鸟动作全部暂停。GetAsyncKeyState是Win32 API它能检测“此刻物理按键是否处于按下状态”不等用户输入调用立即返回。只需取返回值最高位0x8000就能判断该键是否被按下。用这个方法还有一个隐性收益你不需要处理EasyX窗口是否持有焦点的同步问题——不过它会在后台时继续监听按键所以玩到一半切出去再回来小鸟可能已经撞死了。3.3 管道生成与移动复用数组和随机缝隙管道是经典的“对象池”模式在C语言中的朴实践预先分配三根管道每帧把它们的x坐标向左移动移出屏幕就重新放置到最右侧并随机生成新的缝隙高度。注意我这里把帧率锁定在60FPS配合移动速度4像素每帧管道横移速度约240像素每秒从出现到消失需要约2.5秒这个节奏是合理的。#define PIPE_SPEED 4 #define PIPE_WIDTH 70 #define PIPE_GAP 180 void updatePipes() { for (int i 0; i 3; i) { pipes[i].x - PIPE_SPEED; // 管道完全离开屏幕左侧重置到最右侧 if (pipes[i].x -PIPE_WIDTH) { pipes[i].x WINDOW_WIDTH 100; // 缝隙中心在150到500之间随机避免过低或过高 pipes[i].gapY rand() % (WINDOW_HEIGHT - 300) 150; pipes[i].scored 0; } } }注意scored这个字段没有出现在前面的结构体中但完整的源码里通常会有——它的作用是防止同一根管子被重复加分。判断逻辑是鸟的x坐标每穿过一根管道的x坐标时如果scored为0就加1分并置为1防止再加。这比比较“当前管道索引是否变化”要简洁可靠得多。3.4 碰撞检测矩形相交的简单数学模型碰撞检测在这个项目里并不复杂因为所有物体都是轴对齐矩形AABB。判定两个矩形是否相交只需比较四条边的位置关系一个矩形的右边缘要大于另一个的左边缘同时上边缘小于另一个的下边缘。把鸟看成略小于绘制图的小矩形管道则分解为上管和下管两个矩形分别检测。bool isHit() { // 小鸟碰撞盒比视觉尺寸略小提供宽容度减少挫败感 int birdLeft (int)bird.x - 15; int birdRight (int)bird.x 15; int birdTop (int)bird.y - 12; int birdBottom (int)bird.y 12; for (int i 0; i 3; i) { Pipe p pipes[i]; // 管道左边缘和右边缘 int pLeft p.x; int pRight p.x PIPE_WIDTH; // 如果鸟在管道x范围内才需要进一步检测 if (birdRight pLeft birdLeft pRight) { // 上管从屏幕顶部到缝隙上沿 if (birdTop p.gapY - PIPE_GAP/2) return true; // 下管从缝隙下沿到屏幕底部 if (birdBottom p.gapY PIPE_GAP/2) return true; } } return false; }这里的小鸟碰撞盒每边缩了约3个像素在视觉上鸟和管道看似擦到边但不会判定死亡。这个设计不是一个bug而是故意的容错。玩过原版flappy bird的都知道缝隙判定是肉眼所见略宽的否则上手难度会陡增到劝退。PIPE_GAP是缝隙高度gapY是缝隙中心。所以gapY - PIPE_GAP/2是上管底边gapY PIPE_GAP/2是下管顶边。3.5 渲染顺序与双缓冲避免画面闪烁的关键easyX的BeginBatchDraw与EndBatchDraw是一对经常被忽视的函数它们起到双缓冲作用。如果不用它们每帧绘制小鸟、背景、管道时会直接写显存画面会出现连续闪烁看起来非常业余。使用批量绘制后所有绘制操作先写入内存缓冲最后一次性交换到屏幕。void renderGame() { BeginBatchDraw(); // 画天空背景纯色填充整个窗口 setfillcolor(RGB(135, 206, 235)); fillrectangle(0, 0, WINDOW_WIDTH, WINDOW_HEIGHT); // 画管道先画管道再画小鸟使小鸟在最上层 for (int i 0; i 3; i) { drawPipe(pipes[i]); } // 画小鸟 drawBird(); // 画分数 drawScore(); // 游戏结束遮罩 if (gameState STATE_OVER) { drawGameOver(); } EndBatchDraw(); }绘制顺序是一个细节性考点先背景、再管道、再小鸟、最后UI叠加。如果你把背景画在最后所有物体都会被背景色块盖掉。drawBird()内部用ellipse画身体solidcircle画眼睛嘴巴用fillpolygon画一个小三角形。这些绘制函数都是easyX内置的借助它们可以不再依赖任何美术资源。4. 本地编译与运行从源码到窗口程序的完整实操4.1 环境准备Visual Studio与easyX安装跑这个项目第一步是安装Visual Studio Community版本。安装时在“使用C的桌面开发”工作负荷前打勾这一步已经包含MSVC编译器与Windows SDK不再需要额外安装其他依赖。安装完成后去easyX官网下载对应VS版本的安装包运行后它会自动检测已安装的VS版本点击安装即完成集成。实质上是把头文件放入VS的include目录把库文件放入lib目录。一个常见的问题是easyX官网下载页有多个版本选错了会导致代码编译时提示找不到graphics.h。注意看安装包说明用最新版本基本兼容但如果你用VS2019却下载了Legacy版本路径对不上就会报错。安装成功后可以在VS里新建空项目右键源文件添加现有项把下载的.c文件全部加入。4.2 编译参数与解决方案配置对于这个项目编译时需要注意字符集设置。如果代码里使用了中文字符串比如游戏结束画面显示“游戏结束”而工程默认使用多字节字符集在某些系统区域设置下字体显示会乱码。我建议在项目属性中将字符集改为“使用Unicode字符集”并且在代码中统一用宽字符或者调用settextstyle时指定中文字体。不过仅有少量中文显示影响不算很大。另一个关键配置是链接器easyX的静态库需要在链接时被加入。安装程序通常已经配置好但如果你的平台工具集选择错误比如用LLVM工具集而不是MSVC会导致链接失败。此时需要手动在项目属性中指定附加依赖项EasyXa.lib静态库或者EasyXw.lib动态库。下面是完整的构建操作序列打开VS创建空项目C空项目语言选择C。把.c文件和.h文件全部复制进项目目录并在解决方案资源管理器中“添加现有项”。项目菜单 - 属性 - 常规 - 字符集 - 使用Unicode字符集。链接器 - 输入 - 附加依赖项填EasyXa.lib。按F7编译CtrlF5运行。4.3 常见编译错误的排查手段我在本地编译时遇到过setupapi报错或者找不到wingdi.h的情况这些通常不是源码问题而是VS组件缺失。重新运行Visual Studio Installer勾选“Windows 10 SDK”组件即可解决。如果你看到的是C4996警告像strcpy被标记不安全这不影响编译可以在预处理定义中加_CRT_SECURE_NO_WARNINGS消除噪音。最烦人的错误是无法解析的外部符号 _main——这通常是因为建项目时选了“Windows桌面应用程序”而不是“控制台应用程序”或者空项目导致入口函数类型不对。本项目用的main函数是控制台入口而窗口程序入口应该是WinMain两者不能混用。4.4 运行验证确认代码执行链路是否正常编译完成后窗口会以initgraph中定义的600x800尺寸打开。最开始的画面应该是小鸟静止在窗口左下方管道从右侧不断移入点击空格或鼠标左键后小鸟开始受重力影响下落。每穿过一根管道屏幕左上角分数加一。撞到管道或地面时画面中央弹出游戏结束覆盖层。若想重新开始按R键重置游戏状态。这套交互逻辑对应了代码中的STATE_READY、STATE_PLAYING、STATE_OVER三个枚举状态。如果按空格后小鸟没有上升多半是键盘扫描码问题把空格改为任意其他键位即可调试。观察代码运行状态最好的办法是使用VS的断点功能在updateBird一行打上断点按F5启动调试每按一次F10单步执行就能清楚看到vy如何先叠加重力再累加到y。这种观察方式比直接看打印日志直观得多也推荐你在阅读本项目时使用。5. 避坑记录跑这个项目最容易翻车的四个位置5.1 链接失败graphics.h找不到现象编译时报错“无法打开源文件graphics.h”或链接时报“无法解析的外部符号”。原因绝大多数情况是easyX没有正确安装或者VS版本与easyX版本不对应。安装后头文件应该出现在VS安装目录的VC/Auxiliary/VS/include下。有些用户安装时不勾选“将库目录添加到系统环境变量”导致后续手动配置困难。解决重新下载对应版本的easyX安装包并安装。打开VS开发人员命令提示符输入cl测试验证编译器环境再用dir确认头文件路径是否存在。如果库目录确实存在但VS还是找不到在项目属性中手动添加VC目录的包含目录和库目录。5.2 CtrlF5运行后窗口闪退现象双击生成的exe窗口一闪而过看不到游戏画面。原因initgraph创建的图形窗口是独立于控制台的如果主函数执行完或者发生运行时错误导致程序退出窗口会立刻关闭看起来就是“闪退”。这个项目通常是因为代码在某个时刻访问了空指针或数组越界。解决用VS的调试模式F5运行会定位到出错的源代码行。没有调试器时可以在怀疑的位置临时插入system(pause)让它停下查看。但注意system(pause)放在主循环里会阻塞游戏只能放在初始化刚结束或出错前的位置。5.3 画面闪烁严重现象小鸟和管道出现明显残影界面抖动刺眼。原因代码里没有调用BeginBatchDraw和EndBatchDraw每次fillrectangle都会立即刷新到屏幕造成多次局部重绘。这个现象在慢速机器上尤其明显。解决把整个渲染逻辑包在批绘制区间内。如果项目主循环里已经有这个函数注意检查EndBatchDraw是不是在帧末只调用了一次——如果调用了两次第二次会清空缓冲导致画面黑屏。5.4 游戏在窗口失焦后按键失灵现象先用鼠标点击窗口开始游戏玩到一半切换出去再回来发现按空格没反应。原因GetAsyncKeyState检测的是全系统按键状态但easyX窗口在失焦时可能会暂停消息循环导致画面卡顿而按键触发的跳跃可以执行但画面却没有刷新看起来像是失灵。事实上是因为窗口重绘被阻塞。解决在游戏中把鼠标点击窗口的行为当作特殊的“暂停”信号或者在WM_KILLFOCUS消息里主动暂停游戏。简易做法是检测到窗口标题栏变化时自动复位状态不过更推荐用在每次handleInput时判断当前窗口是否为前台窗口具体实现可以用GetForegroundWindow与自身窗口句柄对比。6. 二次开发进阶把它改造成“自己的游戏”的三个方向6.1 改造一增加难度曲线与计分逻辑原版的分数逻辑是每穿过一根管道加一分整局难度恒定。实际体验会让人觉得后期越来越无聊——不是难度高了而是没有变化。你可以加入“每5分管道移动速度1”或者“缝隙高度缩小10像素”的机制。需要修改的地方是updatePipes中的PIPE_SPEED和PIPE_GAP从常量改为变量并在分数增加时更新。改造思路很简单把常量改为全局变量在加分函数里判断。int score 0; int currentSpeed 4; int currentGap 180; void addScore() { score; if (score % 5 0) { currentSpeed; if (currentGap 120) { currentGap - 8; } } }这样改动之后前三根管道其实是“热身”真正挑战从第10根开始。一个更进阶的变体是“速度从4到8缝隙从180到130”的等差变化这种参数化调整让游戏有了可调性也为后续做“难度选择菜单”铺好了路。6.2 改造二用图片替换矢量绘制现在的视觉效果是函数画出来的虽然简洁但辨识度一般。想换成真正的像素鸟素材时有两种路径。一是用easyX的loadimage函数加载png格式然后用putimage贴图。注意easyX早期版本不支持png透明通道需要用loadimage配合putimage的透明参数或先将png转换成32位位图再贴。另一个选择是保留现在的几何绘制但给小鸟加上简单的“翅膀扇动”动画——通过切换眼睛大小、身体高度形成两帧动画。相比加载图片这个做法的工程量更小风格也更贴合“像素”的名字。void drawBirdFrame(int frame) { // frame为0时画圆润身体frame为1时画椭圆瘦长身体 if (frame % 2 0) { fillcircle((int)bird.x, (int)bird.y, 15); } else { fillellipse((int)bird.x, (int)bird.y, 18, 12); } }在主循环里通过帧计数变量每8帧切换一次frame值就能制造出扇动翅膀的错觉。若还想加背景滚动只需把云朵画成横向移动的椭圆阵列——颜色用RGB(255, 255, 255)填充透明度不支持的话就实心画。6.3 改造三本地最高分持久化这是最能提升“作品感”的小功能。用fopen写入系统临时目录下的一个文本文件就可以实现。需要注意区分“本次得分”和“历史最高分”写入文件的只是最高分。这里比较值钱的经验是写文件时用相对路径会找不到文件位置用GetTempPath或者程序所在目录的绝对路径更靠谱。写文件的时机要放在游戏结束后、渲染GameOver画面之前否则画面会卡住。void saveHighScore() { FILE *fp fopen(best.txt, w); if (fp ! NULL) { fprintf(fp, %d, bestScore); fclose(fp); } } void loadHighScore() { FILE *fp fopen(best.txt, r); if (fp ! NULL) { fscanf(fp, %d, bestScore); fclose(fp); } }这个功能逻辑简单但涉及文件读写、异常处理、初始化时机三个知识点。很多人在第一次写时会在loadHighScore里忘记判断fp NULL结果是第一次运行游戏时bestScore读到随机值界面显示一个巨大的分数。从那以后我每次加文件读写都会强制走一遍“文件不存在、文件为空、文件损坏”三个场景踩过一次坑就再也不敢省这个判断了。希望这个项目也能让你养成同样的习惯。本文还有配套的精品资源点击获取
阅读完成 · 觉得有帮助?