简介基于C语言与easyX图形库实现的Flappy Bird像素鸟小游戏源码面向对游戏编程感兴趣的C/C初学者适合用作课程设计、期末项目或自学练手。项目利用双缓冲绘图技术避免画面闪烁完整涵盖鸟的飞行物理模拟、管道移动、碰撞检测、游戏状态管理等核心逻辑代码注释详尽结构清晰便于逐模块研读。压缩包共49个文件约17.09MB包含cpp/h源码、jpg图片素材、mp3背景音效、可直接运行的exe及Visual Studio工程配置文件其中大量图片用于界面与动画音频用于游戏反馈。已有1391人学习下载读者可从中掌握easyX绘图、游戏循环、碰撞判定等基础设计思路并快速搭建可运行的Flappy Bird项目。 你是不是也这样C语言学到指针和结构体刷题刷到想吐翁凯老师的视频看完了但还是觉得“我到底能用C做什么”我曾经也一样直到某天我照着教程用 easyX 图形库写出了人生第一个能玩的小游戏——Flappy Bird那种“原来我写的代码真的能跑出一个游戏”的兴奋感比解出十道链表题都来得强烈。这篇文章我会把整个思路和源码结构完整拆开讲从游戏对象设计到碰撞检测、从双缓冲绘图到按键响应逐段说清楚。这个项目的核心价值在于它恰好覆盖了C语言最重要的几块硬骨头结构体、全局状态管理、循环与延时、键盘事件处理还有最关键的——把一个连续运动的世界转化成屏幕上逐帧画面的编程思维。适合学完C语言基础、想找第一个实战项目练手的初学者也适合想快速体验 easyX 图形库魅力的朋友。1. 先弄清楚游戏里到底有哪些对象和状态写代码之前我花了一个下午在白纸上画这个游戏这是我认为整个项目中最值得的一步。Flappy Bird 看起来简单但它的世界里有三种核心对象一只受重力影响的小鸟、一组不停向左移动的上下管道、一个不断累加的计分器。这三个对象不是孤立的它们之间存在若干交互关系——小鸟撞到管道会死、小鸟飞过管道间隙会得分、小鸟碰到窗口上下边界也会死。1.1 用结构体封装游戏对象而不是散落一堆变量我当时犯过的最大错误就是一开始把小鸟的x、y坐标、速度、管道数组、管道间距全部定义成零散的全局变量。写到碰撞检测时函数参数列表长得像蜈蚣腿自己都分不清哪个参数是管道的还是小鸟的。后来我重构成了结构体整个世界瞬间清爽了。// 小鸟结构体 typedef struct Bird { float x, y; // 位置坐标 float velocity; // 垂直速度正数向下 float radius; // 碰撞半径方便碰撞检测 } Bird; // 管道结构体 typedef struct Pipe { int x; // 管道x坐标 int gapY; // 上下管道之间的间隙中心y坐标 int gapHeight; // 间隙高度即小鸟可以飞过的通道宽度 int width; // 管道宽度 int upperHeight; // 上管道高度绘制时直接用 int scored; // 标记这根管道是否已经计过分 } Pipe;用结构体的好处不仅是代码整洁。当你用Bird bird;定义一个小鸟变量再想想“小鸟移动”这个动作本质上就是修改这个结构体里的字段你的思维模型就完全面向对象了——即使C语言本身不提供class你也可以通过结构体函数的方式实现封装。管道也是一样游戏中同时存在多根管道用结构体数组天然就能描述这个“一群同类物体”的概念比定义 pipe1、pipe2、pipe3 这种愚蠢的命名强一万倍。这里我故意把坐标和速度都定义成 float而不是 int。原因是游戏每帧的位移往往是小数比如每帧移动2.7个像素int会直接截断成2长时间运行后画面会明显卡顿不流畅。float 虽然多占几个字节但对于现代计算器来说完全不值一提。1.2 游戏状态机运行中、死亡、重新开始Flappy Bird 有一个特别容易忽略的设计点游戏不是只有一种状态的。小鸟停在起点待命、玩家点击后游戏开始、撞击管道后游戏结束、按任意键重新开局——这四种状态如果全靠 if-else 嵌套代码会变成一锅粥。我采用的是最简单的整数枚举法效果非常好enum GameState { STATE_READY, // 等待开始小鸟悬停在起点 STATE_RUNNING, // 游戏进行中 STATE_OVER // 游戏结束等待重开 };这个枚举在 C 语言中实际就是一组整型常量放在全局主循环里根据当前状态分派不同的逻辑。比如 STATE_READY 时小鸟只在原地上下浮动不响应重力STATE_RUNNING 时才执行重力模拟和管道移动STATE_OVER 时冻结所有移动只检测玩家的重开按键。这样划分后每个功能的代码都只需要关注自己的状态逻辑边界特别清晰改 bug 也好定位。2. easyX绘图的核心机制双缓冲不是可选项是必选项easyX 是基于 Windows GDI 的图形库很多人第一次接触时会直接用initgraph、cleardevice、circle这些函数然后发现画面疯狂闪烁甚至闪到眼睛疼。这不是你代码写错了而是没有理解图形绘制的底层逻辑。2.1 为什么必须用 BeginBatchDraw / EndBatchDraw回想一下你手动画画的流程先画一只鸟再画几根管道眼睛看到的是完整画面。但计算机屏幕的绘制是逐像素进行的如果你画完一只鸟就被用户看到接着又去画管道用户就会在几毫秒内看到“只有鸟没有管道”的残缺画面。下一帧再看到“有鸟没管道更新位置”的画面这种中间态连续出现就是闪屏的来源。BeginBatchDraw()的含义是告诉 easyX接下来的所有绘制命令先不要真正显示到屏幕上而是画到一个内存缓冲区里。当你把所有图形都画完后调用EndBatchDraw()一次性把整个缓冲区的内容刷新到屏幕。这个机制和 DMA 传输、数据库批量提交是同一个思路——减少中间态暴露的次数。BeginBatchDraw(); // 开始批量绘制 while (1) { // 1. 处理输入 // 2. 更新游戏逻辑 // 3. 绘制背景、管道、小鸟、分数 // 4. 刷新屏幕 cleardevice(); drawBackground(); drawPipes(); drawBird(); drawScore(); FlushBatchDraw(); // 将缓冲区内容一次性显示 Sleep(16); // 约60帧/秒 } EndBatchDraw(); // 结束批量绘制注意循环里我用的不是EndBatchDraw而是FlushBatchDraw。区别在于EndBatchDraw会关闭批量模式下一次循环开头又需要重新调用BeginBatchDraw而FlushBatchDraw只刷新屏幕但保持批量模式开启适合在主循环里持续使用。实测用EndBatchDraw也能跑但每帧都切换模式有微小开销习惯上FlushBatchDraw更干净。2.2 帧率控制与 Sleep 的取舍游戏循环不能无限制地全速跑否则画面更新速度会快到人眼根本无法感知而且 CPU 占用率会直接拉满。我用Sleep(16)把帧率控制在60 FPS左右这个数值不是拍脑袋定的——1000毫秒除以60帧约等于16.67毫秒取整16毫秒。这里有个小坑值得提Sleep(16)并不是精确的16毫秒Windows 系统默认的时间片精度大约是15.6毫秒所以实际睡醒时间可能是15.6或31.2毫秒导致帧率在31~60之间波动。如果你的游戏逻辑对时间敏感比如重力加速度严格的帧率控制还需要用QueryPerformanceCounter做高性能计时。但对于 Flappy Bird 这种休闲小游戏Sleep 带来的误差完全在可接受范围内反而因为代码简单、不容易引入 bug。我自己的做法是跟着手感调速度跑了几天没发现问题。3. 逐模块拆解源码实现从窗口初始化到碰撞检测前面铺垫完原理现在进入真正的代码实现。我会按照模块顺序拆解每个模块给出的代码都是可以直接复制运行的最小可用版本然后单独解释关键点。3.1 窗口初始化与全局参数第一步是定义窗口尺寸。我使用#define常量而不是魔法数字这个习惯建议你从现在开始就养成——过两周再看代码你不会记得 600 是什么意思但WINDOW_WIDTH一看就懂。#include graphics.h #include conio.h #include stdio.h #include stdlib.h #define WINDOW_WIDTH 600 #define WINDOW_HEIGHT 800 // 管道参数 #define PIPE_WIDTH 70 #define PIPE_GAP_HEIGHT 180 // 上下管道间隙 #define PIPE_SPACING 260 // 相邻管道的水平间距 #define PIPE_SPEED 3 // 每帧向左移动的像素数微调用 // 小鸟物理参数 #define GRAVITY 0.45f // 每帧下落的加速度增量 #define JUMP_VELOCITY -8.5f // 按空格后向上的初速度负数表示向上 #define MAX_FALL_SPEED 13.0f // 限制最大下落速度防止穿模 // 管道数组 #define MAX_PIPES 4 // 屏幕上同时出现的最多管道数关于窗口尺寸多说一句。很多教程喜欢用 480x640 之类的竖屏尺寸因为手机版 Flappy Bird 是竖屏的。但如果你是在电脑上练习竖屏窗口可能超出屏幕高度反而看不到效果。我测试下来 600x800 在 1080P 屏幕上刚刚好窗口不需要最大化游戏区域刚好完整呈现。全局参数集中定义还有一个额外好处游戏手感调试变得非常简单。你觉得小鸟下落太快改GRAVITY。管道的间隙难度太高改PIPE_GAP_HEIGHT。所有可调参数一目了然这也是我强烈建议你采用这种写法的原因。3.2 小鸟的物理模型与手感调优Flappy Bird 的操作逻辑一句话就能说清按一次空格给小鸟一个向上的冲量不按键的时候小鸟受重力持续加速下落。这个模型对应的物理公式是经典力学里的匀加速运动velocity_new velocity_old gravity * dt position_new position_old velocity_new * dt在固定帧率的简化模型里把 dt 隐含在常量中就变成每帧更新一次速度、再更新一次位置。贴上核心代码void updateBird(Bird* bird) { // 重力加速下落 bird-velocity GRAVITY; // 限幅防止下落速度无限增大 if (bird-velocity MAX_FALL_SPEED) { bird-velocity MAX_FALL_SPEED; } // 更新位置 bird-y bird-velocity; } void jumpBird(Bird* bird) { bird-velocity JUMP_VELOCITY; // 直接赋初速度不看当前速度 }这里有一个非常关键的手感细节jumpBird中我采用的是直接赋初速度而不是在现有速度上叠加负值。原因在于如果小鸟已经以12像素/帧的速度下落此时按空格若只是叠加一个 -8.5 的速度它仍然以 3.5 像素/帧的速度继续下落玩家会感觉按键“没反应”。直接赋初速度则保证每次按键都能给小鸟一个清晰的向上脉冲反馈非常明确这也是原版 Flappy Bird 的手感核心。另外建议给下落速度设置上限。没有MAX_FALL_SPEED时如果长时间不按键速度会无限累加导致小鸟一帧穿越好几根管道碰撞检测出现漏判。设置上限后速度封顶既保证碰撞检测的可靠性画面表现也更接近真实物理。3.3 管道生成与移动逻辑管道的核心难点在于如何让管道无限生成而不是生成三五根就没了。我的方案是固定管道数量为 MAX_PIPES4 根利用“屏幕外检测”实现循环利用。思路是这样预先在数组中生成4根管道初始位置分别为 400、660、920、1180。每帧所有管道向左移动 PIPE_SPEED 像素。当一根管道的 x 坐标移动出屏幕左边界x -PIPE_WIDTH时立即把这根管道“搬运”到最右边管道的后面并重新随机一个间隙位置。这样管道数量始终不变但视觉效果上是源源不断地出现。void initPipes(Pipe pipes[]) { for (int i 0; i MAX_PIPES; i) { pipes[i].x 400 i * PIPE_SPACING; pipes[i].gapY rand() % (400 - 200 1) 200; // 200~400之间随机 pipes[i].gapHeight PIPE_GAP_HEIGHT; pipes[i].width PIPE_WIDTH; pipes[i].upperHeight pipes[i].gapY - pipes[i].gapHeight / 2; pipes[i].scored 0; } } void updatePipes(Pipe pipes[]) { for (int i 0; i MAX_PIPES; i) { pipes[i].x - PIPE_SPEED; // 左移 // 移出屏幕左侧后重置于最右侧下方 if (pipes[i].x -PIPE_WIDTH) { pipes[i].x MAX_PIPES * PIPE_SPACING; // 搬到队列尾 pipes[i].gapY rand() % (400 - 200 1) 200; pipes[i].upperHeight pipes[i].gapY - pipes[i].gapHeight / 2; pipes[i].scored 0; // 重置计分标记 } } }注意 gapY 的随机范围。如果间隙中心太靠近窗口顶部或底部上下管道的长度会非常夸张玩家几乎没法操作。我限定在 200~400 之间加上间隙高度 180上管道最短也有 200-90110 像素下管道最短是 800-(40090)310 像素视觉上既合理又有挑战性。3.4 碰撞检测不止是判断矩形相交碰撞检测是这类游戏最核心的部分。Flappy Bird 的碰撞有两类第一类是小鸟与管道的碰撞第二类是小鸟飞出窗口上下边界。第一类碰撞起初我用的是圆形与矩形的标准相交检测代码逻辑复杂后来发现一个简化方案把小鸟近似成一个正方形检测鸟的正方形与管道矩形的相交。int checkHit(Bird* bird, Pipe pipes[]) { // 用正方形近似小鸟边长是直径2*radius float birdLeft bird-x - bird-radius; float birdRight bird-x bird-radius; float birdTop bird-y - bird-radius; float birdBottom bird-y bird-radius; for (int i 0; i MAX_PIPES; i) { // 只检测与小鸟x范围有重叠的管道 if (birdRight pipes[i].x birdLeft pipes[i].x pipes[i].width) { int upperBottom pipes[i].upperHeight; // 上管道底部y int lowerTop pipes[i].upperHeight pipes[i].gapHeight; // 下管道顶部y // 如果小鸟顶部高于上管道底部或者小鸟底部低于下管道顶部则相撞 if (birdTop upperBottom || birdBottom lowerTop) { return 1; // 撞了 } } } // 上下出界判定 if (bird-y bird-radius || bird-y WINDOW_HEIGHT - bird-radius) { return 1; } return 0; // 没撞 }这里最关键的是提前用 x 范围过滤。管道分布在水平不同位置如果小鸟和某根管道的 x 范围都不重叠那就不可能碰撞直接跳过矩形检测可以省掉大量无用计算。实际运行中一帧最多检测 2 根管道开销极小。3.5 计分逻辑scored标志位避免重复加分计分逻辑非常简单但有个非常隐蔽的 bug当小鸟飞过一根管道后如果管道还在屏幕上它继续向左移动小鸟继续向右经过如果不做任何标记每帧都会判定一次“小鸟通过管道”分数会疯狂累加。我的方案是给每根管道加一个scored字段。只有满足两个条件才加分第一小鸟的 x 坐标已经越过管道中心线说明确实飞过去了第二这根管道还没有被计过分。加完分立即把scored置1防止重复加。void updateScore(Bird* bird, Pipe pipes[], int* score) { for (int i 0; i MAX_PIPES; i) { if (!pipes[i].scored bird-x pipes[i].x pipes[i].width / 2) { (*score); pipes[i].scored 1; } } }3.6 主循环与按键响应主循环是整个游戏的心脏所有模块在这里协调工作。按键处理我用的是_kbhit()和_getch()组合这是conio.h提供的控制台输入函数在 easyX 生成的图形窗口中依然有效。int main() { initgraph(WINDOW_WIDTH, WINDOW_HEIGHT); BeginBatchDraw(); Bird bird {100, 300, 18, 0, 0}; Pipe pipes[MAX_PIPES]; initPipes(pipes); int score 0; enum GameState state STATE_READY; while (1) { // 按键检测 if (_kbhit()) { char ch _getch(); if (ch ) { if (state STATE_READY) { state STATE_RUNNING; bird.velocity JUMP_VELOCITY; } else if (state STATE_RUNNING) { jumpBird(bird); } else if (state STATE_OVER) { // 重开重置所有状态 bird.y 300; bird.velocity 0; score 0; state STATE_READY; initPipes(pipes); } } if (ch 27) { // ESC退出 break; } } // 按状态更新逻辑 if (state STATE_RUNNING) { updateBird(bird); updatePipes(pipes); if (checkHit(bird, pipes)) { state STATE_OVER; } updateScore(bird, pipes, score); } // 绘制 cleardevice(); drawBackground(); drawPipes(pipes); drawBird(bird, state); drawScore(score, state); FlushBatchDraw(); Sleep(16); } EndBatchDraw(); closegraph(); return 0; }注意STATE_OVER状态下我仍然在绘制分数和管道只是不更新移动逻辑这样玩家能看到自己撞在哪个位置理解为什么失败。如果状态切换时直接清空画面玩家会一头雾水。4. 绘制函数的具体实现与图像美化绘制是整个项目里视觉效果最强、也是新手最容易忽视的部分。很多人以为 easyX 只能画简单的圆和方块所以做出来的游戏丑得没法看。其实 easyX 的IMAGE对象支持从文件加载图片你完全可以自己做素材或者用简单的几何图形搭配出像素风格。4.1 用几何图形画出像素小鸟我的小鸟没有用图片而是用三个图形拼出来的身体是黄色圆形眼睛是白色圆形加黑色瞳仁嘴巴是红色三角形。这样画出来的效果虽然谈不上精美但很有“像素游戏”的复古感也避免了图片素材的版权问题。void drawBird(Bird* bird, enum GameState state) { // 身体 setfillcolor(YELLOW); solidcircle((int)bird-x, (int)bird-y, (int)bird-radius); // 眼睛 - 画在右前侧让小鸟看起来朝右飞 setfillcolor(WHITE); solidcircle((int)bird-x 6, (int)bird-y - 8, 7); setfillcolor(BLACK); solidcircle((int)bird-x 8, (int)bird-y - 8, 3); // 嘴巴 setfillcolor(RED); solidrectangle((int)bird-x 12, (int)bird-y - 4, (int)bird-x 24, (int)bird-y 4); }这里用solidcircle和solidrectangle而不是circle和rectangle因为实心版的视觉效果更饱满也免去手动填充的麻烦。另一个细节是画眼睛时用了偏移量6和-8因为小鸟朝右飞眼睛应该靠前靠上这样看起来才有动感。4.2 管道绘制与背景色管道我直接用绿色矩形上管道从窗口顶部画到upperHeight下管道从upperHeight gapHeight画到窗口底部。为了让画面更有层次感我在管道左边缘加了高亮边框右边缘加了阴影边框只需几行代码void drawOnePipe(Pipe* p) { // 上管道 setfillcolor(RGB(87, 191, 74)); solidrectangle(p-x, 0, p-x p-width, p-upperHeight); // 下管道 solidrectangle(p-x, p-upperHeight p-gapHeight, p-x p-width, WINDOW_HEIGHT); // 管道口加深色边框突出轮廓 setlinecolor(RGB(46, 122, 42)); setlinestyle(PS_SOLID, 3); rectangle(p-x, 0, p-x p-width, p-upperHeight); rectangle(p-x, p-upperHeight p-gapHeight, p-x p-width, WINDOW_HEIGHT); }背景色我用的是浅天空蓝RGB(135, 206, 235)。如果你想让画面更生动可以画几朵白云——几个大小不同的白色实心圆叠在一起就是一朵云成本极低但效果很好。4.3 在窗口上绘制中文字符easyX 默认的字体是英文字体直接输出中文会变成乱码。需要调用settextstyle设置中文字体并且设置字符集为 UTF-8 或 GBK。一个稳妥的做法是void drawScore(int score, enum GameState state) { settextstyle(30, 0, 微软雅黑); settextcolor(WHITE); TCHAR s[32]; _stprintf(s, _T(分数: %d), score); outtextxy(20, 20, s); if (state STATE_OVER) { settextstyle(50, 0, 微软雅黑); outtextxy(WINDOW_WIDTH / 2 - 120, WINDOW_HEIGHT / 2 - 30, _T(游戏结束)); } }注意outtextxy这个函数接收的是宽字符指针所以字符串需要用_T()宏包裹_stprintf也要用宽字符版本。这是 easyX 中比较容易踩的坑我在第一次开发时这里报了一堆编译错误后来才搞明白是字符集的问题。5. 运行中常见的几个坑和排查路线代码写完后最磨人的就是调 bug。我把实际运行中遇到过的坑整理出来按排查难易排序你如果遇到了可以直接对照。5.1 initgraph 初始化失败或窗口一闪而过这个现象通常是运行时弹出一个黑窗然后立即闪退。95% 的原因是initgraph失败了或者链接时 easyX 库没有正确配置。排查路线如下确认你用的开发环境是 Visual Studio2010及以上easyX 官方只支持 VS不支持 Dev-C 和 Code::Blocks。如果你用的是 Dev-C需要找第三方适配方案这里不建议折腾直接换 VS Community 版。确认项目属性里“字符集”设置的是“使用 Unicode 字符集”。项目 → 属性 → 配置属性 → 常规 → 字符集 → 使用 Unicode 字符集。关闭杀毒软件试试某些杀毒软件会拦截 easyX 的驱动级绘图操作。这个坑比较少见但真实存在。检查 initgraph 的窗口尺寸是否超出屏幕范围如果你设置的窗口宽度大于屏幕分辨率初始化会失败。我的 600x800 在普通笔记本上完全没问题如果你用的屏幕是 1366x768建议把高度改成 700。5.2 画面闪屏特别严重眼睛都要闪瞎了这是初学者最容易踩的坑原因我在前面原理部分已经详细说过没有使用双缓冲。排查方法很简单——检查你的代码里是否调用了BeginBatchDraw()并且主循环里有FlushBatchDraw()。如果你只是调用了cleardevice()和绘制函数没有批量绘制那么闪屏是必然的。另一个容易忽略的点cleardevice()是全屏清除如果绘制内容较少可以用setfillcolorfloodfill或者只清除变化的区域来降低开销。但对于 Flappy Bird 这种全屏画面全清全画反而是最简洁的方案帧率足够高时完全看不出问题。5.3 碰撞检测时灵时不灵有时候穿过管道却没死我遇到过几次这种情况一开始以为是 float 比较精度问题排查了大半天才发现是管道 x 坐标的更新时机问题。当管道以每帧3像素的速度向左移动而小鸟一帧移动了十几像素时两帧之间小鸟可能直接从管道间隙“穿过”了物理上看起来就像穿模。解决方案就是前面提到的MAX_FALL_SPEED限幅。当速度限制在 13 像素/帧而管道宽度是 70 像素时小鸟至少需要 5 帧才能完全穿过管道宽度这中间每帧都会做碰撞检测漏判概率大幅下降。如果你想让游戏难度更高、下落更快记得同时加大MAX_PIPES的检测范围或者把碰撞检测做成更精确的连续碰撞检测扫掠体但那个复杂度对新手来说得不偿失。5.4 小球被卡在管道里疯狂加分这是 scored 标志位没有正确重置导致的。当一根管道被搬回屏幕最右侧复用后如果它的scored仍然是 1那么小鸟下次飞过它时就不会加分。反过来如果初始化时忘了设置scored 0而旧值是 0 还好但如果旧值是 1小鸟飞过时反而加不了分。排查方法在initPipes函数中不仅初始化新数组还要在管道复用逻辑里务必将scored清零。我在这块踩坑后养成了一个习惯——所有结构体数组的每个字段都要显式初始化不要依赖默认值。6. 从能玩到好玩我建议你继续扩展的四个方向基础版本跑通后如果你觉得意犹未尽这里是四个可以立刻上手并且难度递增的扩展方向每个都不会超过100行代码但对能力的提升非常明显。6.1 增加开始界面和结束界面现在的版本是运行就进入游戏结束后按空格直接重开。体验上会有点突兀。你可以加一个封面显示游戏名称和“按空格开始”的提示然后在游戏结束后显示最终分数和最佳纪录。这需要用到文件读写最高分存到本地 txt 文件里每次新游戏读取、游戏结束比较并更新。一个项目同时练到图形界面和文件操作性价比很高。6.2 加入音效和背景音乐mciSendString函数可以播放 wav 或 mp3 音频。按空格时播放跳跃音效、得分时播放清脆的提示音、死亡时播放低沉的声音游戏体验会瞬间提升一个档次。音效素材可以从免费音效网站下载也可以自己用 Audacity 剪辑制作。这里注意引入mmsystem.h头文件并在项目属性里链接winmm.lib。6.3 难度递增机制原版 Flappy Bird 被诟病的一点是难度恒定玩久了没新鲜感。你可以在计分模块里做一个简单的联动每当分数超过 10 分PIPE_SPEED增大 0.3 像素/帧PIPE_GAP_HEIGHT缩小 5 像素难度平滑上升。关键是要控制好递增的步长和上限否则会出现突然从简单跳到极难的断层感。6.4 给小鸟加入动画现在的小鸟是静态的飞起来翅膀不会扇动。你可以准备两到三帧小鸟素材图或者用几何图形拼出不同翅膀位置的形态根据时间交替显示造成扑腾翅膀的视觉效果。这个扩展会让你第一次体会到“动画”的本质——连续播放静止画面简简单单几帧就能骗过眼睛。我在实际开发中发现每次扩展一个功能后重新运行总会有新的 bug 冒出来但恰恰是这些 bug让我对底层机制的理解深入了一大截。比如加分数存档时我才真正搞懂了fopen的文件模式和编码问题加音效时我才理解了 Windows 多媒体库的调用方式。做完这个项目再回头看你学过的 C 语言知识指针、结构体、数组、循环、条件判断这些抽象的东西全都变成了你手底下真实世界的规则和逻辑。这就是做小游戏对学习最大的价值——它把死知识变成活体验。如果这篇文章帮你跑通了第一个游戏去把这个项目保存好标上日期。一年后再打开看看你会清楚地看到自己的成长轨迹。本文还有配套的精品资源点击获取