简介:这是一份基于 Windows 平台、使用 C++ 完成的“捕鱼达人”小游戏课程设计资源,面向正在学习 Windows 程序设计与面向对象开发的在校学生。项目以捕鱼游戏为场景,完整覆盖窗口创建、GDI 图形绘制、鼠标键盘事件响应、多线程与定时器、碰撞检测、得分计算等关键环节;代码按模块划分,灵活运用继承、多态与封装,便于逐段阅读和二次修改。压缩包共 36 个文件,大小约 7.42MB,内部包含 bmp 图像素材、ogg/mp3/wav 音效、cpp/h 源码、txt 说明文档以及 dsp/dsw 工程文件,素材、代码与配置文件齐全,可直接查看项目结构和运行逻辑。已有 1307 人学习下载。通过阅读源码,可掌握 Windows API 消息循环、图像资源加载与释放、防止内存泄漏、游戏循环与碰撞算法等实用技术,也能借鉴其完整工程组织方式,为同类课程设计提供可落地的参考方案。
1. 捕鱼达人课程设计:一份 C++ Win32 工程能带给你什么
如果你正在找 Windows 程序设计这门课的课程设计源码,捕鱼达人小游戏是出现频率最高的选题之一,也是能把 Win32 窗口、GDI 绘图、消息循环、碰撞检测全部串起来的最好载体。这份资源不是那种网上随手复制、十几行代码拼出来的 demo,而是一个结构完整、能直接编译运行的 C++ 工程:从窗口创建到鱼群生成,从开炮到计分结算,逻辑全部在 Win32 API 层面手写完成,没有用到任何游戏引擎。适合两类人,一是课程设计选题定了但不知道从哪下手的同学,二是已经写过简单窗口程序、想看看一个完整小游戏怎么组织代码的初学者。照着它跑通一遍,比看十遍教材上零散的 API 讲解都管用。
2. 先拆双循环骨架:消息循环与游戏循环的分工
2.1 为什么这个项目需要“两套循环”同时转
Windows 程序天生是事件驱动:鼠标点击、键盘按下、窗口重绘,系统都会往消息队列里塞一条消息,程序通过消息循环取出来再分发到窗口过程去处理。但游戏是连续的:鱼每帧都要移动、炮弹每帧都要飞,不能等用户动了才更新画面。所以捕鱼达人这样的工程必须让两套循环并行工作——一套循环负责“等系统消息”,另一套负责“按固定节奏推进游戏逻辑”。
对应到代码里,最常见的做法是用SetTimer在窗口上挂一个定时器,让系统每隔一段固定时间向窗口过程发送一条WM_TIMER消息。这样消息循环不用改,游戏逻辑全部塞进WM_TIMER分支里:移动鱼、移动炮弹、检测碰撞、刷新画面。也就是把游戏循环“伪装”成消息循环的一部分。这种做法的好处是结构简单,消息和逻辑在同一个线程里,不需要考虑同步问题,课程设计这个规模的项目完全够用。
2.2 从 WinMain 到窗口创建的关键调用链
整个工程的入口是WinMain,这也是 Win32 程序区别于控制台程序的第一道门槛。源码里通常分四步走:注册窗口类、创建窗口、进入消息循环、处理窗口过程。核心代码大致是这样:
int WINAPI WinMain(HINSTANCE hInstance, HINSTANCE hPrevInstance, LPSTR lpCmdLine, int nCmdShow) { WNDCLASS wc = { 0 }; wc.hInstance = hInstance; wc.lpfnWndProc = WndProc; // 绑定窗口过程 wc.lpszClassName = TEXT("FishHunterWindow"); wc.hbrBackground = (HBRUSH)GetStockObject(BLACK_BRUSH); wc.hCursor = LoadCursor(NULL, IDC_HAND); RegisterClass(&wc); // 第一步:注册窗口类 HWND hwnd = CreateWindow( TEXT("FishHunterWindow"), // 窗口类名,必须与上面一致 TEXT("捕鱼达人 - Windows程序设计课程设计"), WS_OVERLAPPEDWINDOW, // 窗口样式,带标题栏和边框 CW_USEDEFAULT, CW_USEDEFAULT, 900, 640,// 窗口大小与位置 NULL, NULL, hInstance, NULL); // 父窗口、菜单、实例句柄均无或默认 ShowWindow(hwnd, nCmdShow); UpdateWindow(hwnd); // 触发第一次 WM_PAINT MSG msg; while (GetMessage(&msg, NULL, 0, 0)) // 第二步:进入消息循环 { TranslateMessage(&msg); // 键盘消息转译 DispatchMessage(&msg); // 分发到 WndProc } return 0; }这里要先解释清楚RegisterClass和CreateWindow的关系:RegisterClass只是把窗口类的“模板”登记进系统,真正把窗口创建出来、拿到窗口句柄HWND的是CreateWindow。写错lpszClassName或者忘记lpfnWndProc赋值是新手最容易出问题的地方,前者导致 CreateWindow 失败返回 NULL,后者导致窗口消息无人处理、一启动就退出。GetMessage在收到WM_QUIT时返回 0,这是窗口通过PostQuitMessage发出的退出信号,消息循环就在这里断开,整个进程结束。
2.3 消息处理的分流:每个分支只干一件事
窗口过程是静态函数,用switch (message)把不同类型消息分到不同分支。捕鱼达人这里的关键分支有五个:WM_CREATE里做资源初始化,WM_TIMER里推进游戏逻辑,WM_PAINT里重绘画面,WM_LBUTTONDOWN里响应鼠标开炮,WM_DESTROY里清理资源并退出。每个分支只做自己该做的事,画面绘制只放在WM_PAINT分支,逻辑推进只放在WM_TIMER分支,互不越界,模块化就清晰了。
我一般会在WM_CREATE里启动定时器,把帧率控制在每秒 30 帧左右:
case WM_CREATE: // 30帧/秒,即每帧间隔33ms SetTimer(hwnd, TIMER_GAME, 33, NULL); InitGame(); // 初始化鱼群、炮弹、分数等全局数据 break; case WM_TIMER: UpdateGame(hwnd); // 推进一帧逻辑:移动鱼、移动炮弹、检测碰撞 InvalidateRect(hwnd, NULL, TRUE); // 请求重绘 break; case WM_LBUTTONDOWN: // lParam 低16位是x坐标,高16位是y坐标 FireBubble(GET_X_LPARAM(lParam), GET_Y_LPARAM(lParam)); break;WM_TIMER里那一句InvalidateRect是关键:它不直接画图,而是向系统声明“这个客户区需要重绘”,系统随后合成一条WM_PAINT消息。为什么要拐这个弯,因为BeginPaint/EndPaint必须在WM_PAINT里配对使用,强制把绘制放在WM_PAINT分支,能让 GDI 的资源分配与释放始终成对出现。直接拿GetDC画也没问题,但课程设计里如果到处乱画、不用BeginPaint,画面容易出现残留,答辩时很减分。
需要注意GET_X_LPARAM需要额外的头文件支持,老代码里更常见的是LOWORD(lParam)和HIWORD(lParam)分开取坐标,效果一样。窗口坐标默认是相对于客户区左上角的坐标系统,开炮的瞄准点计算直接用它就行,不需要二次换算。定时器 ID 用TIMER_GAME这种宏定义而不是裸数字,如果后面要加第二个定时器,逻辑上不会混淆。
3. 让鱼动起来:GDI 双缓冲、贴图动画与鱼群数据结构
3.1 闪屏的根源与双缓冲的解法
先做一个小实验你就懂闪屏从哪来了:在WM_PAINT里先擦背景、再逐条画鱼,画面会快速经历“全黑 → 有几条鱼 → 全黑 → 有几条鱼”的交替。因为每一次擦除重画,都直接作用在屏幕可见的区域上,人眼感知到的就是高频闪烁。闪屏的根源不是画得多慢,而是“擦”和“画”之间隔了可见的时间窗口。
双缓冲的原理是把画布从屏幕换到内存:先在内存里创建一块与窗口客户区大小一致的位图,所有的擦除、贴图、绘制都在这个内存 DC 上完成,全部画完以后再用一条BitBlt把整个内存位图一次性拷到屏幕上。屏幕端看到的永远是一幅完整的画面,闪烁自然消失。捕鱼达人这个工程里,鱼的数量多、贴图切换频繁,双缓冲是必须的,不是可选项。
实现上就三步,初始化时创建内存 DC,每帧绘制到内存 DC,最后整体拷贝:
// 初始化:在 WM_CREATE 或 InitGame 中调用 void InitBackBuffer(HWND hwnd) { HDC hdc = GetDC(hwnd); g_memDC = CreateCompatibleDC(hdc); // 内存DC g_memBmp = CreateCompatibleBitmap(hdc, WINDOW_W, WINDOW_H); g_oldBmp = (HBITMAP)SelectObject(g_memDC, g_memBmp); // 把位图选入内存DC ReleaseDC(hwnd, hdc); } // 绘制:WM_PAINT 分支里 case WM_PAINT: { PAINTSTRUCT ps; HDC hdc = BeginPaint(hwnd, &ps); // 第一步:在内存DC上擦背景 RECT rc = { 0, 0, WINDOW_W, WINDOW_H }; FillRect(g_memDC, &rc, (HBRUSH)GetStockObject(BLACK_BRUSH)); // 第二步:在内存DC上画所有鱼、炮弹、分数 DrawFishes(g_memDC); DrawBubbles(g_memDC); // 第三步:一次性拷到屏幕 BitBlt(hdc, 0, 0, WINDOW_W, WINDOW_H, g_memDC, 0, 0, SRCCOPY); EndPaint(hwnd, &ps); break; }CreateCompatibleDC创建的 DC 初始状态只有一个 1x1 像素的黑色位图,所以必须马上用CreateCompatibleBitmap建一块真正大小的位图并把旧位图用SelectObject换掉。选入的同时会返回旧位图句柄,要把它存起来,程序退出时再SelectObject换回去、删除内存 DC 和位图,否则 GDI 对象会泄漏。课程设计跑一晚上不关程序,任务管理器里 GDI 对象数持续上涨,基本都是这个原因。
3.2 鱼群数据结构与运动参数
鱼的属性不外乎坐标、方向、速度、贴图类型、存活状态这几样。捕鱼达人工程里用一个结构体数组管理鱼群,数组长度固定,鱼死亡后只改标志位而不重新分配内存,避免频繁 new / delete 造成内存碎片。结构体大致长这样:
#define MAX_FISH 40 // 同屏最多40条鱼 #define FISH_TYPE_COUNT 4 // 4种鱼,分值不同 typedef struct _Fish { int x, y; // 当前坐标 int speed; // 速度,单位:像素/帧 int dir; // 方向角度,0~359 int type; // 鱼的类型,决定贴图与分值 int frame; // 当前动画帧序号 bool alive; // 是否存活 bool captured; // 是否已被网住 } Fish; Fish g_fishes[MAX_FISH];运动更新就是一个极简的线性位移:x += speed * cos(dir),y += speed * sin(dir)。这里方向用角度还是弧度要先统一,C 语言的sin/cos接收的是弧度,如果初始化时把dir按角度存,每次更新前要先做dir * PI / 180换算,忘了换算的话鱼的轨迹会完全乱掉,朝左上走的鱼突然朝右下飞。这类问题在答辩调试时很折磨人,建议结构体里面直接存弧度,展示时再转角度给人看。
边界处理是鱼群逻辑里最容易出细节 bug 的地方。我惯用的规则是:鱼碰到左右边界就翻转方向(dir = 180 - dir),碰到上下边界就反向(dir = -dir),并让x/y强制回到边界内,防止鱼卡在边界外一直来回穿越但画面看不到。速度参数按鱼的类型错开取值,小鱼快、大鱼慢,同屏速度差让画面有层次感,也让玩家能优先打慢速的大鱼。
3.3 动画帧切换与贴图选择
鱼的贴图是一组序列帧,本质上是几张连续动作的位图按顺序循环播放。资源里贴图命名常见的是fish1_0.bmp、fish1_1.bmp、fish1_2.bmp,同一种鱼的几帧动作循环播放,看起来鱼就在摆尾游动。帧切换逻辑就一行状态判断:
void UpdateFishFrame(Fish* f) { // 每4帧切换一次动画帧,避免切换过快 static int frameCounter = 0; if (++frameCounter % 4 == 0) { f->frame = (f->frame + 1) % 3; // 3帧循环 } }这里有个容易被忽略的联动:鱼朝左游和朝右游时贴图不能是同一张,否则鱼是倒着游的。工程里常见的处理是准备两张方向相反的位图,或者在贴图时用StretchBlt配合镜像变换。Win32 GDI 没有直接的镜像绘制函数,只能在绘制时用SetWorldTransform做坐标变换,或者干脆加载两张位图、按方向选择。课程设计按后一种方案做就行,简单直接,效果也不差。
绘制函数每帧要对每条鱼调用一次贴图加载。这里有一个性能大坑:LoadBitmap/LoadImage每条鱼每帧都调用一次,程序不卡才怪。这些 API 是把位图从磁盘读进来再解码,属于重型操作。正确的做法是在初始化时把所有贴图加载好,存进一个HBITMAP数组,游戏循环里只做SelectObject切换。SelectObject本身也有开销,一条鱼一张位图反复选入也费时,进阶做法是把同一种鱼的全部帧合并到一张大位图里,绘制时用BitBlt的srX/srY参数取其中一帧。资源里如果已经把帧合成好了,直接抄这个思路即可。
绘制的深度排序也值得一提:同屏鱼会互相遮挡,DrawFishes里如果按数组下标顺序画,后画的永远盖住先画的,视觉上大鱼总是压在前面。简单处理是按鱼的 y 坐标从小到大排个序再画,y 值小(屏幕上方)的鱼先画,y 值大(屏幕下方)的后画,就可以做出粗略的距离感。这个排序每帧做一次,40 条鱼用冒泡都无所谓,但能让画面观感提升一个档次。
4. 从开炮到结算:碰撞检测、计分与关卡数值
4.1 碰撞检测:为什么选圆形判定
捕鱼达人的碰撞判定发生在网与鱼之间。用矩形判定最简单,但网和鱼都是带弧度的贴图,矩形判定在视觉误差上很明显:鱼头和尾巴不在矩形内却被判定命中,玩家朝鱼头打却因为尾巴蹭到矩形边缘判定打空,观感和手感都不对。圆形判定是精确度和复杂度之间的最好平衡点,成本低、误差小、代码直观。
标准写法是判断两个圆心距离是否小于两圆半径之和,为了省掉开根号的耗时,通常比较距离平方:
bool CheckHit(int fishX, int fishY, int fishR, int netX, int netY, int netR) { int dx = fishX - netX; int dy = fishY - netY; int distSq = dx * dx + dy * dy; // 距离平方,避免sqrt int hitSq = (fishR + netR) * (fishR + netR); return distSq <= hitSq; }不要小看这个“不调用sqrt”的细节。每秒 30 帧,同屏最多 40 条鱼,最多同时存在几个网,每帧做几百次距离判定。每一次 sqrt 都是依次浮点运算,乘加运算和整数比较的耗时能差一个数量级。真实场景里你可能感觉不到差别,但写成距离平方是比较好的工程习惯,也方便面试或者答辩时讲性能优化思路。fishR和netR是预设在初始化阶段的数据,不同鱼有不同的半径,根据贴图尺寸估算即可,原则是宁可略小于贴图可见轮廓,也不要大出一圈导致命中反馈太“假”。
4.2 炮弹飞行与网展开的坐标计算
鼠标点击后,炮弹从炮台位置出发,朝鼠标位置直线飞行。炮台坐标固定在窗口底部中间,鼠标点击位置存成目标点。炮弹每一帧沿着两点连线方向移动固定步长,直到到达目标点,然后原地展开成一张网。网的命中范围随时间逐渐扩大再收缩消失,模拟撒网捕鱼的效果。
炮弹与网的状态合在一个结构体里管理,简化成:
typedef struct _Bubble { int x, y; // 当前位置 int targetX, targetY; // 鼠标点击的目标 int state; // 0=飞行中 1=网展开 2=结束 int lifeFrame; // 剩余帧数,到0则回收 int radius; // 网的当前半径 } Bubble;飞行状态就一行步进逻辑,每帧按速度沿方向向量移动:
void UpdateBubble(Bubble* b) { if (b->state == 0) { double dx = b->targetX - b->x; double dy = b->targetY - b->y; double len = sqrt(dx * dx + dy * dy); double step = BUBBLE_SPEED; // 单位:像素/帧 // 防止最后一帧越过目标点 if (len < step) { b->x = b->targetX; b->y = b->targetY; b->state = 1; // 到达,开始撒网 b->lifeFrame = NET_LIFE; // 网存在20帧 b->radius = 10; } else { b->x += (int)(dx / len * step); b->y += (int)(dy / len * step); } } else if (b->state == 1) { // 网的半径先扩大再收缩,最后一定帧数后消失 b->radius += 2; if (--b->lifeFrame <= 0) { b->state = 2; // 回收该网 } } }BUBBLE_SPEED和NET_LIFE这类参数在设计阶段不可能一次定准,需要边跑边调。炮弹飞太快玩家来不及瞄准,飞太慢则打断操作节奏;网的存在帧数太短则大鱼还没碰到就消失了,太长则玩家可以预判提前撒网。合理的起始值是炮弹速度 18 像素/帧、网存在 20 帧左右,再根据实际屏幕分辨率等比缩放。要注意不同分辨率的适配:900x640 窗口下 18 像素/帧合适,切到全屏或者较大窗口时同一速度会显得很慢,可以考虑按窗口宽度做一次比例换算。
4.3 计分规则与关卡数值怎么调
捕鱼达人的计分核心是不同鱼有不同分值,工程里用一个数组按类型索引查表,是比写一堆if-else更整洁的做法:
// 4种鱼的分值,按鱼类型索引 static const int FISH_SCORE[FISH_TYPE_COUNT] = { 10, 20, 50, 100 }; // 同屏最大鱼数、鱼生成间隔(帧)、大鱼概率 static const int FISH_SPEED[FISH_TYPE_COUNT] = { 6, 4, 3, 2 };鱼类的产生由生成策略控制。典型做法是每若干帧尝试生成一条鱼,生成位置固定在屏幕左侧或右侧之外,进入屏幕后再参与更新,这样观众能看到鱼“游进来”的过程,而不是凭空刷在画面里。生成哪个类型用加权随机控制:小鱼 60%、中鱼 25%、大鱼 12%、极品鱼 3%,这个比例决定游戏的经济节奏——如果大鱼概率太高,分数膨胀,玩家十几分钟就通关;太低则打在鱼群上全是低分,挫败感明显。
炮弹的消耗与鱼的分值需要做平衡。常见做法是炮弹每次发射消耗 1 金币(或者叫子弹数),命中后返还鱼的分值,净收益 = 命中分值 - 消耗。FISH_SCORE的最低档是 10,消耗设为 1,意味着玩家即使只打中最小鱼也有 9 点净收益,游戏正向反馈建立起来了。这个平衡参数是课程设计答辩时老师最爱问的点,能讲清“为什么消耗设 1 而不是设 5”背后的逻辑,会比单纯演示游戏加分不少。关卡进阶可以按时间或者累计分数来调整鱼速和概率,比如每 30 秒把大鱼概率上浮 2 个百分点、鱼速整体上调 1,让后期刺激程度递增。
5. 课程设计最常翻车的五个坑:从闪退到闪屏的排查实录
5.1 现象:窗口一闪就退出,根本没有游戏画面
原因:消息循环没有正确的退出条件,或者窗口过程里WM_DESTROY分支缺失,PostQuitMessage(0)没有被调用,程序在收到窗口关闭消息后直接走默认处理退出了。还有一个高频原因是CreateWindow返回了 NULL,窗口压根没创建成功,ShowWindow之后就崩了。
解决:先在WinMain里判断CreateWindow的返回值,NULL 就直接MessageBox报错退出。确认窗口过程里case WM_DESTROY: PostQuitMessage(0); return 0;分支存在。字符集导致的崩溃或者lpszClassName不一致也会造成CreateWindow失败,见下面第 5.4 条。
5.2 现象:鱼动起来画面狂闪,像老式日光灯一样
原因:没有双缓冲,每帧直接对窗口 DC 擦背景再画鱼,擦除与绘制之间的空白窗口被人眼捕捉到了。这是单缓冲绘制的必然结果,不是驱动或系统问题。
解决:按第 3.1 节的方案做内存 DC +BitBlt。注意WM_PAINT里画到g_memDC时,所有绘制函数传的 DC 必须是g_memDC而不是hdc,经常有人初始化时正确、绘制时漏传参数导致白忙一场。创建内存 DC 放在WM_CREATE或者首次WM_PAINT时只做一次,千万不要放进每帧绘制代码里重复创建。
5.3 现象:鱼一多就卡成幻灯片,帧率肉眼可见地下降
原因:有 90% 的可能是每帧绘制时反复调用LoadBitmap/LoadImage从磁盘加载贴图。这个操作从磁盘读数据、解析 BMP 文件头、分配 GDI 对象,每一条鱼每帧都做一遍,性能和内存都扛不住。另一个常见原因是贴图位图过大(比如一张 1000x1000 的 BMP 直接用于缩放绘制),StretchBlt的缩放开销远超BitBlt。
解决:初始化阶段把全部贴图加载完毕存入HBITMAP数组,绘制只做SelectObject和BitBlt。若资源里有大图需要缩放显示,预先用StretchBlt处理好生成一张目标尺寸的小图存内存,避免每帧实时缩放。同屏鱼数设置为 40 以下,超过这个数值视觉上也不会更丰满,只会增加无意义开销。
5.4 现象:编译报错,提示“无法从 ‘const char *’ 转换为 ‘LPCWSTR’”
原因:工程默认启用了 Unicode 字符集,TCHAR被展开成wchar_t宽字符,字符串字面量"FishHunterWindow"却还是窄字符。把窄字符串窄传给宽字符接口,编译器直接报错。这是 Win32 程序新手最常见的第一道坎,跟游戏业务逻辑完全无关。
解决:字面量字符串统一用TEXT()宏包裹,例如TEXT("FishHunterWindow")。这样在 Unicode 编译选项下自动转成宽字符,在多字节字符集下保持窄字符。另一种做法是改工程字符集为“使用多字节字符集”,但这只是逃避问题,换一台默认 Unicode 的机器又重新报错。用TEXT()宏最彻底,运行库函数也尽量用_t前缀等。注意LPSTR与LPCWSTR是两种不同的指针类型,绝不可以在它们之间直接强制转换。
5.5 现象:Debug 版本跑得好好的,切到 Release 版本就崩
原因:90% 是数组越界或读未初始化的内存。Debug 模式下编译器会在数组边界插入安全检查,堆内存会自动清零,越界写不一定会立刻爆炸;Release 模式下所有保护都拿掉,栈里的垃圾值被当成数组下标或函数指针,崩溃方式千奇百怪。
解决:排查鱼群数组和动画帧数组的下标。常见写法f->frame = (f->frame + 1) % 3看起来安全,但如果f本身是野指针,整个运算行为就不可预期。先在UpdateGame开头对每条鱼做alive检查再更新,排除“已死亡的鱼还在运动”的路径。同时把结构体内所有字段显式初始化,尤其是bool类型,不初始化就读取会造成随机结果。Release 崩溃时用_CrtSetBreakAlloc在 Debug 下还原同样路径不可行时,就在关键数组操作前加_ASSERTE断言,直接暴露出问题行。
6. 交付前的最后一课:答辩演示、参数微调与机油打包
演示效果好不好,有时候不取决于代码写得多完善,而是几个参数值合不合适。提交课程设计之前,我会把FISH_SPEED整体调慢 1~2 像素/帧,把大鱼概率拉高 5 个百分点。理由是答辩现场的机器性能未知,鱼速过快加上机器卡顿,容易出现一种“鱼在瞬移”的廉价感;鱼稍慢一点,动画帧切换的细节看得清楚,鱼的贴图也更耐看。炮弹速度BUBBLE_SPEED保持稍快,宁可打到目标点边上落空,也不要让全场等待一颗炮弹慢慢飞——答辩时你要讲解逻辑,注意力不在操作上,慢炮弹会让你频繁打空,场面很难看。
打包方面,Windows 程序设计课程设计最常见的交付形态是直接把 Visual Studio 工程拷给老师,但更好的做法是生成 Release 版本,把 exe 和资源文件放在同一个文件夹里,整个文件夹打包提交。要确认 exe 里没有硬编码绝对路径,比如C:\\Users\\xxx\\Desktop\\fish.bmp这种写死在LoadBitmap里的路径,换一台机器就加载不出贴图。正规做法是把资源路径改成相对路径,或把位图加到工程资源里用MAKEINTRESOURCE加载。基于动设备跑不起来,通常就是这个问题。
答辩演示的法则是“先跑一轮完整流程,再做代码讲解”。先当着老师的面完整跑一局:开炮、命中大鱼、分数跳动、网消失回收、新鱼生成,让老师对这个工程的完整性建立印象,然后才进入代码讲解。讲解时优先讲双缓冲、碰撞检测、状态机这几块,因为这三点是你和普通抄工程同学拉开差距的地方。我被问得最多的问题就是“网和鱼的碰撞怎么判断的”,只要你能当场把那个距离平方判断的代码指出来并画个示意,基本就能过。
资源打包时把全套 Win32 源码、贴图资源、可直接运行的 Release 工程放在一起,方便你对照阅读和修改参数。建议先把程序完整跑通一遍,再逐行改着玩:把鱼的生成间隔改成 5 帧,看看屏幕瞬间爆满是什么感受;把双缓冲去掉,体会一次闪屏再改回来。亲手把每个坑踩一遍,答辩时老师问什么都答得上来。希望这份工程能帮你把课程设计稳稳落地,也希望你改完参数做出属于自己的那版捕鱼达人。
本文还有配套的精品资源,点击获取