简介:面向计算机专业学生的C++期末课程设计“飞翔的小鸟”完整项目,提供了可直接运行的源码与配套文档,适合课程作业、期末答辩或入门阶段的项目模仿与二次开发。项目基于Visual Studio工程搭建,代码已经过完整测试,作者标注答辩平均分96分,具备一定参考价值。包内共43个文件,压缩包约2.25MB,主要包含C++源文件cpp、Visual Studio工程文件sln/vcxproj、编译后的exe可执行程序、游戏使用到的bmp图片素材,以及README说明文档。文件类型涉及源码、工程配置、程序输出与资源文件,便于快速定位和复用。目前已有104人在CSDN学习或下载。资源中还保留了项目从初期“贪吃蛇”方案转向“飞翔的小鸟”的工程痕迹,可查看开发过程中的环境配置与思路变化;下载后可参考源码结构、工程配置与运行方式,也能在此基础上二次开发实现新功能,适合作为C++课程设计或期末作业的完整参考。
1. C++期末作业选飞翔的小鸟:一个能讲清楚的项目比花哨更重要
期末作业交个游戏,最怕的不是写不出代码,而是做完之后老师问一句“这个重力怎么实现的”就卡壳。飞翔的小鸟(Flappy Bird)恰好是那种“代码量不大、但每个模块都有话可讲”的经典小游戏:物理模拟、碰撞检测、随机数、状态机、计时器全都能在几百行C++里体现。用EasyX图形库在Windows上做,不需要引入外部引擎,编译环境就是Visual Studio或VS Code配好C++环境,拷到哪台机器都能跑。适合C++刚入门、想拿一份完整源代码加文档说明交作业的在校生。这篇文章我会按选型、架构、核心代码、排错、打磨的顺序,把从新建工程到写完文档说明的每一步拆开讲。
2. 先定技术路线:选EasyX图形库是目前最稳的作业方案
2.1 三种实现路线对比:控制台、Win32、EasyX
飞翔的小鸟在C++里有三条常见路线,我按期末作业的评分标准帮你分析一下。
控制台版本:用printf画字符、用kbhit检测按键。代码最短,几十行就能动起来,但画面是字符拼的,视觉上很简陋。优点是逻辑最直观,适合完全没接触过图形库的同学;缺点是答辩时很难展示,老师印象分低。
Win32 API版本:直接用CreateWindow、GetMessage、BeginPaint这些原生接口画图。能做出正经窗口程序,但代码量翻倍,大部分时间在写窗口回调、处理WM_PAINT消息,游戏逻辑反而被挤到一边。期末作业周期按周算,不推荐。
EasyX版本:这是国内高校用得最多的轻量图形库,本质是对Win32 API的封装,initgraph一行开窗口、circle画圆、getch收按键。学习成本低,代码量比Win32少一半,但跑起来是一个真正的图形窗口。网上关于EasyX的踩坑帖子也最多,比如乱码、闪屏、字体问题,这些我在后面避坑章会展开。
三个方案对比下来,EasyX是“交作业性价比”最高的选择。如果你用的是VS2022,装EasyX只需要去官网下载安装包,安装器会自动识别编译器版本,点击安装即可;如果用VS Code,需要手动配置C/C++环境并且把easyx.h的包含目录和lib文件路径填进c_cpp_properties.json和tasks.json里。我建议期末作业统一用Visual Studio,省去配置环境的时间。
2.2 一个四文件的工程结构:源代码与文档分开好交付
很多同学的期末作业是一个main.cpp塞了八百行,注释一个没写,文档说明在期末最后一天熬夜编。这不是好习惯。我一般会把工程按功能拆成四个文件,结构清晰,文档也好写:
FlappyBird/ ├── main.cpp // 程序入口,游戏循环和状态机 ├── game.h // 公共头文件,声明常量、结构体、函数 ├── game.cpp // 实现小鸟、管道、碰撞、计分逻辑 └── 期末作业文档说明.docx// game.h 核心常量定义 #ifndef GAME_H #define GAME_H #include <graphics.h> // EasyX图形库头文件 const int WIN_WIDTH = 600; // 窗口宽度,像素 const int WIN_HEIGHT = 800; // 窗口高度,像素 const int BIRD_X = 200; // 小鸟固定横坐标 const int BIRD_SIZE = 30; // 小鸟的圆形半径 const int GRAVITY = 1; // 每帧向下加速度 const int JUMP_SPEED = -12; // 跳跃瞬间的向上速度 const int PIPE_WIDTH = 70; // 管道宽度 const int PIPE_GAP = 200; // 上下管道之间的空隙 const int PIPE_SPEED = 3; // 管道左移速度 #endif这段代码看起来简单,但每个常量背后都有考试会问的考点。GRAVITY和JUMP_SPEED在期末答辩里经常被追问:为什么跳跃速度是负的?因为窗口坐标系y轴向下,向上飞是y值减小。为什么重力是1而不是9.8?因为游戏帧率约60FPS,按帧模拟而不是按秒模拟,数值需要经验调参。我一般会让GRAVITY为1、JUMP_SPEED为-12,这个组合在800x600的窗口里手感偏敏感,适合演示时连续点击鼠标。
2.3 主循环为什么是while(true)而不是for循环
游戏主循环依赖EasyX的peekmessage和while轮询。初学者最容易写错的是把游戏逻辑堆在switch里,导致按键响应不流畅。正确做法是让主循环每一帧都做四件事:处理输入、更新物理、检查碰撞、重新绘图。
// main.cpp 游戏主循环骨架 #include "game.h" int main() { initgraph(WIN_WIDTH, WIN_HEIGHT); // 创建窗口 srand((unsigned)time(NULL)); // 随机种子,管道高度要用 Bird bird = { BIRD_X, WIN_HEIGHT / 2, 0 }; // 初始位置居中 Pipe pipes[3]; // 屏幕上最多3根管道 int score = 0, frame = 0; while (true) { // 1. 处理输入:检测空格或鼠标点击 // 2. 更新小鸟速度与位置 // 3. 管道左移,生成新管道 // 4. 碰撞检测,游戏结束则跳出循环 // 5. 绘制背景、小鸟、管道、分数 Sleep(16); // 约60帧每秒 } closegraph(); // 关闭图形窗口 return 0; }逻辑说明:initgraph开一个600x800的窗口,while(true)是游戏行业通用的主循环写法,每一帧结束前Sleep(16)让CPU喘口气。如果去掉Sleep,游戏会跑满CPU并且速度不稳定,因为不同机器的主频不一样。srand((unsigned)time(NULL))是管道随机高度的前提,忘了写会导致每次运行生成的管道序列完全一样,答辩现场很尴尬。
参数说明:frame计数器用于每隔一定帧数生成新管道;pipes数组长度为3,因为窗口宽度600、管道速度3、管道间距约240,同时最多出现3根。这个数值和窗口大小是配套的,如果你把窗口改成800宽,数组长度也要相应增加。
3. 从零写核心逻辑:物理模拟、管道生成、碰撞检测三步走
3.1 小鸟的物理模型:重力、速度和帧率的关系
飞翔的小鸟在EasyX里本质是一个圆,圆心坐标(BIRD_X, bird.y)。鸟不会左右移动,只有y方向的速度在变化。每帧执行两步:
// game.cpp 更新小鸟物理 void updateBird(Bird &bird) { bird.vy += GRAVITY; // 重力加速下落 bird.y += bird.vy; // 按速度移动 if (bird.y < 0) // 撞到天花板 bird.y = 0, bird.vy = 0; }逻辑说明:bird.vy是速度向量,初始为0,每帧加一个重力加速度。如果你连续按空格,每次跳跃把vy设为JUMP_SPEED,小鸟会先向上冲再被重力拽回来,形成一条抛物线。这是整个游戏里最值得在文档说明中用一页纸讲清楚的模型——它演示了“匀速运动到变速运动”的差别。
| 参数 | 推荐值 | 调参效果 |
|---|---|---|
| GRAVITY | 1 | 越大下落越快,手感越重 |
| JUMP_SPEED | -12 | 绝对值越大,跳跃越高 |
| Sleep(16) | 16ms | 越小越快,越大越卡 |
表格说明:这三组参数要配合调整。GRAVITY从1改成2,跳跃高度会减半;JUMP_SPEED从-12改成-18,小鸟能飞到管道上沿之外,难度骤降。我调参的经验是先定帧率(Sleep值),再按“按下空格后小鸟能上升约100像素”为基准去调JUMP_SPEED。
3.2 管道生成与移动:用一个数组管住整屏幕
管道是游戏里唯一的障碍物,也是文档说明里最能体现数据结构功底的部分。每根管道由x坐标和gapY(空隙中心y坐标)决定,绘制时画一个上方矩形和下方矩形:
// game.h 管道结构体 struct Pipe { int x; // 管道右边缘的x坐标 int gapY; // 上下管道空隙中心的y坐标 bool scored; // 是否已经计过分 };// game.cpp 管道移动与生成 void updatePipes(Pipe pipes[], int &score, int &frame) { for (int i = 0; i < 3; i++) { pipes[i].x -= PIPE_SPEED; // 每帧左移 if (pipes[i].x < -PIPE_WIDTH) { // 移出屏幕左侧 int lastX = 0; for (int j = 0; j < 3; j++) // 找最右侧管道 if (pipes[j].x > lastX) lastX = pipes[j].x; pipes[i].x = lastX + 240; // 在右侧重新生成 pipes[i].gapY = 200 + rand() % 400; // 随机空隙位置 pipes[i].scored = false; } if (!pipes[i].scored && pipes[i].x < BIRD_X) { score++; // 小鸟越过管道即得分 pipes[i].scored = true; } } }逻辑说明:pipes数组是循环复用的,移出左边的管道会被重新放到最右边。gapY = 200 + rand() % 400把空隙中心限制在200到600之间,避免管道空隙偏到窗口外面。scored标志防止同一根管道反复加分——这是很多初学者会犯的bug:小鸟停在管道空隙中间,分数每秒加十几次。
参数说明:240是两根管道之间的水平间距,等于PIPE_WIDTH * 3 + 30。间距越小难度越大,最小值不要小于150,否则管道空隙在水平方向上没有足够时间让玩家反应。
3.3 碰撞检测:圆和矩形的相交判定
EasyX里没有现成的碰撞函数,期末作业的碰撞检测要自己写。圆形小鸟和矩形管道的碰撞分两种情况:直杆部分和空隙部分。
// game.cpp 碰撞检测 bool hitPipe(const Bird &bird, const Pipe &pipe) { // 如果小鸟还没有完全进入管道的x范围 int birdLeft = bird.x - BIRD_SIZE / 2; int birdRight = bird.x + BIRD_SIZE / 2; if (birdRight < pipe.x || birdLeft > pipe.x + PIPE_WIDTH) return false; // 小鸟完全在管道左边或右边 // 进入管道范围后,检查是否撞到上下矩形 bool hitTop = bird.y - BIRD_SIZE / 2 < pipe.gapY - PIPE_GAP / 2; bool hitBot = bird.y + BIRD_SIZE / 2 > pipe.gapY + PIPE_GAP / 2; return hitTop || hitBot; }逻辑说明:这个函数把碰撞拆成两步:先粗判x方向是否重叠,再细判y方向是否碰到矩形。PIPE_GAP / 2是空隙半高,bird.y是圆心y坐标,用圆心加减半径和空隙边缘比较。注意这里判断的是圆与矩形的轴对齐边界,实际圆的角落会有少量误差,但视觉上游戏节奏快,玩家感知不到,期末答辩也不需要做像素级碰撞。
3.4 状态机:游戏循环里怎么区分“菜单”“飞行”“结束”
飞翔的小鸟至少需要三个状态:开始、游戏中、游戏结束。我习惯用枚举实现:
// game.h 游戏状态 enum GameState { STATE_MENU, STATE_PLAY, STATE_OVER };主循环外维护一个GameState state = STATE_MENU;,每个状态只处理自己关心的逻辑。这样做的好处是文档说明里可以画一张状态流转图,而且是复试时老师最喜欢问的“你如何管理游戏流程”。菜单阶段显示标题和“按任意键开始”;游戏阶段执行物理和管道逻辑;结束阶段显示分数并等待“按R重新开始”。用状态机而不是一堆if标志位,后续加暂停功能、加音效开关都容易得多。
4. 避坑清单:EasyX常见报错、闪屏和期末答辩高频踩坑
4.1 现象:代码编译通过,但EasyX窗口弹出来是乱码文字
原因:控制台和EasyX图形窗口的字符集不一致。EasyX默认使用GBK编码,而VS2022的源文件可能保存成了UTF-8。这在outtextxy输出中文字符串时表现最明显。
解决:在项目配置里把字符集设置为“多字节字符集”,或者在源文件开头加#pragma execution_character_set("utf-8")。我一般推荐前者,因为VS2022新建项目默认是Unicode字符集,改一个下拉选项就行,不用动代码。如果是VS Code里配置C/C++环境,则需要在tasks.json的编译命令里加/utf-8参数。
4.2 现象:窗口不停闪烁,小鸟有拖影
原因:绘图顺序不对。初学者经常先在窗口上画一个背景色矩形覆盖全屏,再画小鸟和管道。如果忘了在每帧开头清屏,上一帧的画面残留在窗口上,就形成拖影。
解决:主循环里每帧第一件事调用cleardevice()或BeginBatchDraw()。EasyX官方推荐双缓冲:
BeginBatchDraw(); // 开启双缓冲 while (true) { cleardevice(); // 清空画布 drawGame(); // 绘制当前帧的所有元素 FlushBatchDraw(); // 一次性输出到窗口 Sleep(16); } EndBatchDraw(); // 关闭双缓冲双缓冲的原理是先在内存里画完一整帧,再一次拷贝到显存,而不是一帧画几十次导致屏幕闪烁。这个点在答辩时是加分项。
4.3 现象:明明小鸟和管道还有距离,却判定碰撞
原因:我用的是圆形半径30,但EasyX绘制时圆心在BIRD_X/2,很多同学把圆心坐标当成左上角坐标去检测,导致整个碰撞框偏移了半个小鸟宽度。还有另一个常见错误:管道矩形绘制用了fillrectangle(pipe.x, 0, pipe.x + PIPE_WIDTH, gapY - PIPE_GAP/2),但碰撞检测里却写成了pipe.x - PIPE_WIDTH。
解决:把绘制和碰撞检测统一封装成一个函数。我一般会写一个getBirdRect()返回小鸟的外接矩形,碰撞检测和绘制都从这个函数取坐标值,而不是在多处手写加减法。这样两处永远保持一致,彻底杜绝偏移。
4.4 现象:第一次玩不卡,玩十几秒后越来越卡
原因:管道数组的生成逻辑里用了lastX + 240,但忽略了已经移出窗口左侧的管道占据数组位置。如果同时有多根管道在屏幕外,重新生成时可能出现负坐标或重复生成。
解决:用循环找最靠右的管道时,跳过所有x < -PIPE_WIDTH * 2的管道。更规范的做法是我推荐使用环形队列,维护一个head指针,只处理head到head+2三根有效管道。期末作业用数组加scored标志就够,如果你的代码里出现了“管道越来越多”的现象,优先检查这里。
4.5 现象:在答辩演示机上运行,报“无法解析的外部符号_main”
原因:EasyX是Windows图形库,需要链接到Win32子系统。VS2022创建项目时如果选了“控制台应用程序”,链接器会尝试找控制台入口;而用到initgraph时需要图形入口。
解决:项目属性 -> 链接器 -> 系统 -> 子系统,改为“窗口”。这是期末作业最常见的一类环境报错,和代码逻辑无关。也可以简单粗暴地用system("pause")避免窗口秒退,但这不是根治方法。
5. 把作业做成“能讲”的:注释、文档说明和答辩演示的关键细节
5.1 注释应该写“为什么”而不是“是什么”
期末作业的文档说明和代码注释决定了老师要不要仔细看你的项目。很多同学写的注释是“// 小鸟”“// 管道”,这种注释没意义。我通常要求自己代码里每个关键函数至少有一段三行以上的注释,说明输入、输出、边界情况。
/** * 更新小鸟物理位置 * 输入:bird 当前状态(坐标和速度) * 输出:更新后的 bird 状态 * 逻辑:重力累加到速度上,速度改变位置;天花板限制 */ void updateBird(Bird &bird);文档说明里放一段这样的注释,能帮老师快速理解你的设计,也是你答辩时按图索骥的提纲。
5.2 一个自检测试清单,答辩前跑一遍
| 检查项 | 预期表现 | 通过 |
|---|---|---|
| 按空格/鼠标 | 小鸟向上跳起,随后下落 | |
| 连续点击 | 小鸟连续跳动不穿模 | |
| 小鸟撞管道 | 游戏结束,分数停在当前值 | |
| 小鸟撞天花板 | 不穿出窗口,y值不小于0 | |
| 管道移出左侧 | 右侧重新生成,空隙随机 | |
| 分数保持 | 每通过一根管道只加1分 |
这个列表直接放进文档说明里作为“测试报告”,老师一看就知道你做过验证。我用过最实在的两个技巧:一是把窗口标题改成自己的学号和项目名,SetWindowText(GetHWnd(), _T("20210001-飞翔的小鸟"));;二是游戏结束后按空格重新开始,很多人忘了实现重开功能,答辩当场只能关掉重运行。
5.3 答辩时要把“帧率”“随机性”“状态机”讲成自己的话
最常见的问题“为什么这个游戏好玩”背后的技术答案是:物理模型让运动有真实感,随机管道制造不确定性,状态机让流程可控。把这些词记熟,演示时配合代码指给老师看,比你讲十句“我做了很多努力”有用得多。我当年第一次交图形学作业时,因为没把碰撞检测的代码注释写清楚,答辩被问了一个问题就卡住了,后来养成习惯:每个.cpp文件写完先自己试讲一遍。希望帮到你。
本文还有配套的精品资源,点击获取