拓冰建站拓冰建站
首页 / 资讯中心 / 正文

用Win32 API和GDI从零实现捕鱼达人:窗口、双缓冲与碰撞检测实战

简介这是一份基于Windows平台的C课程设计项目主题为捕鱼达人适合正在学习Windows程序设计或准备课程设计答辩的本科生。项目完整演示了通过Windows系统接口创建窗口、绘制图形、处理鼠标键盘事件并实现鱼类游动、子弹发射、碰撞检测和计分等核心玩法同时涉及图形设备接口绘图、多线程定时器、消息循环等知识点代码结构清晰对深入理解C与Windows程序开发具有较好的参考价值。资源包共36个文件包含C源文件与头文件、位图游戏图片素材、音频资源以及工程配置文件整体大小仅7.42兆字节既有可直接编译运行的源码也有配套的图片和音效便于学习对照。目前已有1307人学习下载适合作为大学课程设计选题的完整参考或在此基础上扩展实现更丰富的游戏功能。 如果有人问我《Windows程序设计》这门课到底应该怎么学我的回答向来很简单别光捧着书看去写一个看得见摸得着的程序。比如一个用鼠标打鱼的捕鱼达人小游戏就非常合适。窗口、消息循环、GDI绘图、定时器、碰撞检测这些让人头疼的知识点在一款小游戏里全都能串起来做完之后你会对Win32 API建立起非常直观的体感。我之前带过的学生里不少人在把这个项目跑通之后再回头看教材里的章节基本一路畅通。这篇文章我不打算讲空泛的原理就按我当时带着完整项目走下来的路线把方案选型、技术要点、实操步骤、踩坑记录全部摊开。适合正在做Windows程序设计课程设计、想练Win32 API或者准备用C/C做一个小游戏练手的同学。哪怕你现在还只会写控制台程序只要跟着节奏走也能把这个工程搭起来。1. 项目思路与方案选型1.1 为什么是 Win32 GDI而不是游戏引擎很多人一听“小游戏”第一反应是Unity、Godot甚至微信小游戏平台。但对于“Windows程序设计”这个场景我强烈建议用Win32 API GDI手动写原因有三个。第一课程考察点就是窗口、消息、GDI这些基础机制。用引擎会把底层考察点全部包住老师一眼就能看出你并没有在学API。第二这类项目规模很小用引擎属于杀鸡用牛刀反而要处理编辑器版本、安装配置、打包发布等一系列额外问题。第三也是最实在的一点Win32手动实现捕鱼达人可以让你看到每一帧画面是怎么来的每一条鱼是怎么动起来的这种掌控感是引擎给不了的。也有人问我用MFC行不行、用Qt行不行。当然可以但MFC封装太厚很多Windows消息机制细节被藏起来了Qt跨平台能力强却也弱化了Windows原生的学习路径。如果目标是认真过一遍Windows程序设计裸写Win32是风险最小、收益最高的选择。顺便说一句现在的Visual Studio建项目时记得选“Windows 桌面应用程序”或空项目后手动配置不然链接器可能在入口函数上跟你作对。1.2 捕鱼达人玩法拆解与功能清单捕鱼达人这个题材选得聪明玩法看起来花哨但拆到最小可运行版本核心逻辑其实只有三块鱼从屏幕两侧不断生成并移动鼠标控制炮台指向并点击发射渔网渔网扩散后和鱼发生碰撞并计分。只要保留这三块就已经是一个能玩、能演示、能得分的完整项目。我的做法是先列功能清单再写代码你可以直接抄这张表鱼群管理支持多种鱼、不同速度和分数游出屏幕后回收。炮台控制跟随鼠标方向旋转左键发射渔网。渔网系统发射后向外扩散并前移数秒后消失。碰撞与计分渔网命中鱼后加对应分数界面实时刷新。游戏状态时间或血量到则结束支持重新开始。绘制与动画GDI逐帧绘制双缓冲防闪烁。把这些拆清楚写代码时就不会东一榔头西一棒。我第一次做的时候就是没列清单结果写了三天功能倒是都能跑代码却乱到连自己都看不懂后来重构时才明白前期拆解的重要性。2. 核心技术点逐项拆解2.1 窗口创建与消息循环整个游戏的地基是窗口和消息循环。Win32程序的入口通常长得比较机械但每个字段都有意义int WINAPI wWinMain(HINSTANCE hInstance, HINSTANCE, PWSTR, int nCmdShow) { WNDCLASS wc {}; wc.lpfnWndProc WndProc; wc.hInstance hInstance; wc.hCursor LoadCursor(nullptr, IDC_ARROW); wc.lpszClassName LFishGame; RegisterClass(wc); HWND hwnd CreateWindowEx(0, LFishGame, L捕鱼达人, WS_OVERLAPPEDWINDOW, CW_USEDEFAULT, CW_USEDEFAULT, 1024, 720, nullptr, nullptr, hInstance, nullptr); ShowWindow(hwnd, nCmdShow); UpdateWindow(hwnd); MSG msg; while (GetMessage(msg, nullptr, 0, 0)) { TranslateMessage(msg); DispatchMessage(msg); } return 0; }注意这里用的是wWinMain而不是WinMain对应Unicode入口。WNDCLASS里的lpfnWndProc指向窗口过程所有鼠标、键盘、重绘、定时器消息最后都会进到那个回调函数里游戏逻辑的主战场就在那。初学者最容易忘记设置hCursor或窗口类名不一致导致程序一闪而过或者窗口根本创建不出来。消息循环这块新手容易玄学化。其实GetMessage在没有消息时会让线程休眠有消息就取出来DispatchMessage负责把消息派发给窗口过程。就这么简单。后面如果要高帧率循环只需把GetMessage换成PeekMessage循环就能一直跑再用GetTickCount64控制更新节奏。2.2 GDI 绘图与双缓冲窗口有了剩下就是用GDI画东西。GDI是Windows传统的绘图API虽然不如DirectX快但捕鱼达人这种2D小游戏完全够用。最核心的一个概念是画面并不会“画一笔就永远存在”窗口每次被遮挡、移动、尺寸变化都可能产生WM_PAINT所以每帧都需要重新绘制全部内容。如果不做额外处理画面一定会闪。原因在于默认的WM_ERASEBKGND会用背景色擦一遍窗口然后再绘制前景频率一高就是霓虹灯效果。解决办法是双缓冲先在内存里把一整幅图画好再一次BitBlt到屏幕上用户看到的永远是一帧完整的画面。核心代码长这样case WM_PAINT: { PAINTSTRUCT ps; HDC hdc BeginPaint(hwnd, ps); HDC memDC CreateCompatibleDC(hdc); HBITMAP hBmp CreateCompatibleBitmap(hdc, w, h); HGDIOBJ oldBmp SelectObject(memDC, hBmp); // 在这里画背景、鱼、炮台、文字 DrawGame(memDC); BitBlt(hdc, 0, 0, w, h, memDC, 0, 0, SRCCOPY); SelectObject(memDC, oldBmp); DeleteObject(hBmp); DeleteDC(memDC); EndPaint(hwnd, ps); break; }每次WM_PAINT里临时创建兼容位图写法简单但频繁分配内存DC会带来微小开销。更好的做法是在窗口创建成功后初始化一份后备缓冲在WM_SIZE里按新尺寸重建。这个优化放到项目后期再上第一版先保证逻辑正确。另一个要点是WM_ERASEBKGND里直接return TRUE告诉系统不用擦背景等于提前消灭一个闪烁源。2.3 定时器驱动的游戏逻辑游戏里的鱼不会自己动必须有东西周期性触发更新。最简单的方案是SetTimerSetTimer(hwnd, 1, 50, nullptr); // 每50ms触发一次然后在WM_TIMER里去刷新鱼的位置并调用InvalidateRect触发重绘。50ms对应差不多20FPS捕鱼这种休闲游戏足够。但有几个坑得知道。第一WM_TIMER是低优先级消息系统消息多的时候会被延后不适合把精确物理逻辑全压在它上面。第二定时器触发的最小间隔受系统时钟周期限制大约15.6ms你填1ms也不会真的1ms一次。第三窗口销毁时记得KillTimer否则窗口关闭后仍可能收到消息。如果要更稳定的游戏循环我一般用PeekMessage循环帧间隔用GetTickCount64或QueryPerformanceCounter自己算。大概结构是PeekMessage返回FALSE时调用一次UpdateAndRender两次调用之间如果没到16ms就Sleep(1)。这种写法能稳定控制帧率也不依赖WM_TIMER的派发优先级后期扩展性能也更稳。2.4 碰撞检测与随机鱼群捕鱼达人的核心玩法就是“渔网罩到鱼”所以碰撞检测是这个项目的关键点但难度其实不高。我采用的是圆形碰撞把每一条鱼简化为一个圆也就是中心点加半径每张渔网也用一个圆表示。当两圆圆心的距离小于半径之和就判定命中。距离计算用x差值平方加y差值平方再跟半径和的平方比省掉sqrt性能会好一点。鱼群的生成也值得认真设计。每次生成鱼位置在左右两条边之一方向朝对侧速度用随机数控制。为了避免所有鱼都往一个方向涌我按鱼的类型分配速度区间小鱼游得快、得分低大鱼游得慢、得分高。生成间隔用累加器控制随游戏时间逐渐缩短。还可以加一些小逻辑比如当前游戏时间越久大鱼比例越高玩起来就没那么单调。3. 实操过程从零搭一个可跑的游戏3.1 工程配置与最小窗口我习惯把工程搭建分三步走。第一步Visual Studio新建空项目在项目属性里把字符集改成“使用Unicode字符集”。只要使用wWinMain链接器入口基本能自动对上如果坚持用WinMain建议把字符集改成多字节但我不推荐因为后续处理中文文本会很麻烦。第二步新建一个main.cpp先把最小窗口跑起来确认能出现一个空白窗口再继续。不要一上来就写满几百行业务代码否则窗口都出不来都不知道是哪里错了。第三步把全局变量、结构体、函数声明拆到独立文件比如Fish.h、Game.h避免所有代码全堆在一个cpp里。Windows程序设计最容易养成的坏习惯就是把所有代码塞进一个巨大的WndProc看起来能跑改起来头疼。如果你连窗口都建不出来最常见原因无外乎三种入口函数写错、窗口类名不一致、链接器找不到WinMain。桌面应用务必要选择Windows子系统在VS里检查项目属性-链接器-系统-子系统是否为“Windows”。现在VS默认环境一般不用手动补user32、gdi32库但老VC6.0时代经常要自己加遇到链接错误先查这里。3.2 鱼群对象、移动与绘制我先把鱼设计成一个结构体而不是class因为项目足够小结构体最直接struct Fish { double x, y; // 鱼的中心坐标 double speed; // 速度 int dir; // 1 表示向右-1 表示向左 int type; // 鱼的类型 bool alive; // 是否存活 };用一个std::vector 来管理鱼群。每一帧更新时x speed * dir * deltaTime当x超出左右边界并且完全离开屏幕时把alive设为false由回收逻辑从vector里移除。共享同一个数组的好处是遍历方便碰撞检测和渲染都只需要顺着vector过一遍。绘制鱼的方式有很多种最简单的是用GDI画几何鱼用Ellipse画身体再用Polygon画三角形尾巴鱼眼睛可以补一个小实心圆。不同type的鱼用不同颜色的画刷填充视觉上就能区分。想更漂亮可以换真实图片但GDI对透明通道的支持一般新手抠图容易抠到怀疑人生。我给学生的建议是第一版全用几何图形把逻辑跑通后再考虑换成图片资源。3.3 炮台瞄准、渔网发射与捕获判定炮台的本质其实就是炮口位置与鼠标连线形成的一条射线角度。在WM_MOUSEMOVE里用GET_X_LPARAM和GET_Y_LPARAM拿到鼠标坐标在当前帧绘制时用atan2计算炮口指向鼠标的角度。绘制时把炮筒画成一条粗线再配合一个圆形底座效果就出来了double angle atan2(mouseY - gunY, mouseX - gunX);点击左键时记录当前鼠标位置作为发射方向在炮口位置生成一个Net对象。Net拥有位置、方向、扩散半径、剩余生命四个字段struct Net { double x, y; double angle; double radius; double life; // 剩余时间 };每帧更新时Net沿着角度方向前移同时半径逐渐增大life逐渐递减归零就移除。碰撞时遍历所有鱼计算鱼圆心与Net圆心的距离如果小于半径差加鱼半径就把鱼的alive置为false并把对应分数加到总分里。同时可以记录一条最近命中的提示文字绘制在界面顶部。这个机制代码量不大却是整个游戏最耐玩的部分。3.4 计分、界面文字与状态管理分数显示用TextOutW或DrawText都行。TextOutW需要指定坐标和字符串长度DrawText支持更复杂的对齐和截断更适合做UI。绘制前一定要调SetBkMode(memDC, TRANSPARENT)否则文字的白色背景会盖住海洋底色。数字转字符串建议直接用std::to_wstring再配合L得分: 拼接省去手动写itoa的麻烦。游戏状态初期就两个playing和over。我习惯用全局变量bool running和int score、int remainingTime来管理。更新和绘制函数都检查状态。结束时弹一个MessageBox询问是否再来一局比自己做结束界面省事演示效果也直观。不过MessageBox会阻塞消息循环记得在弹出前先KillTimer停止计时器否则会出现“关不掉”“背后还在动”的奇怪现象。4. 常见问题与排查技巧实录4.1 窗口闪烁和残影这是捕鱼达人项目里出现频率最高的问题。症状就是鱼在屏幕上拖着尾巴运动或者整个窗口像电灯泡一样闪。原因基本可以锁定两处一没有双缓冲二没有处理WM_ERASEBKGND。解决办法前面已经写了绘制逻辑全部在一个内存DC里完成最后BitBlt同时在WM_ERASEBKGND里直接return TRUE。如果还闪检查是不是每次WM_PAINT里都创建和释放位图导致的抖动改成缓存一份后备缓冲。还有一个残影的隐蔽来源InvalidateRect的第三个参数bErase。如果你用InvalidateRect(hwnd, NULL, TRUE)要求系统擦背景哪怕双缓冲也会先给你擦一下。我习惯统一用FALSE把所有背景绘制都交给DrawGame函数处理避免系统自动擦背景和我们的绘制逻辑打架。4.2 中文乱码与字符集Windows程序设计的经典坑窗口标题、游戏内文字显示成乱码。大部分原因是源文件编码、项目字符集、API宽窄不一致。我的做法很统一项目字符集选Unicode入口用wWinMain所有字符串用宽字符字面量比如L捕鱼达人绘制用TextOutW或DrawTextW不要混用char字符串。源文件在VS里另存为“Unicode (UTF-8 带签名)”也能避免源码本身被错误解码。如果你从旧代码复制东西很可能在调用TextOut时传了窄字符串这时把字符串前面加L并把函数改成TextOutW基本就能解决。还有一个容易踩的坑std::string转std::wstring不要手动按字节转最好用MultiByteToWideChar或者干脆全程使用std::wstring存储文本。4.3 定时器不稳定的问题用SetTimer做游戏循环时间长了会发现鱼一会儿快一会儿慢尤其系统负载上来时更明显。这不是你代码写得不对而是WM_TIMER的派发优先级低。解决办法前面提过改成PeekMessage循环配合GetTickCount64计算帧间隔。我实际测试时SetTimer在小项目里勉强能用但如果游戏里同时有很多鱼和很多渔网更新频率不稳定会让体验大打折扣所以尽早切到高精度循环更省心。另外就算用了PeekMessage循环也要注意更新逻辑里不能有耗时操作比如每帧都去创建字体、加载图片、写调试日志。捕鱼这种小项目一帧内只应该做坐标偏移、简单的碰撞判断和GDI绘制。一旦发现帧率下降先查是不是在循环里重复创建了资源。4.4 图像加载、链接与部署小坑如果后续你想把鱼从几何图形换成真实图片用LoadImage时记得指定LR_LOADFROMFILE图片路径要写对。调试时最容易出问题的是“运行目录和源文件目录不是同一个”VS默认工作目录可能是项目目录也可能是输出目录。建议用GetModuleFileName拿到exe完整路径后再拼图片相对路径或者直接把图片放到输出目录否则LoadImage会返回NULL程序却看不出明显报错。链接错误一般集中在两个点一是WinMain入口找不到二是user32/gdi32库没链接。VS环境里右键设置链接器子系统为Windows或者新建项目时直接选“Windows 桌面应用程序”模板都能绕开。另外把Release版exe拷到别的机器运行时如果用了动态版VC运行库目标机器可能提示缺少VCRUNTIME140.dll。项目属性里把运行库改成“多线程(/MT)”做静态编译或者直接让目标机器装好对应运行库都能解决。最后说一点个人体会。捕鱼达人这个项目难度不高但它把Windows程序设计的核心机制一次性练透了窗口、消息、GDI、定时器、碰撞检测每一个都是后续做更复杂Windows程序的基本功。我在带项目时总跟学生强调三件事第一命名和模块化要一开始就做好别把几十个函数塞进一个WndProc第二每完成一个功能就编译运行一次不要憋大招第三图形和特效可以最后美化先把游戏闭环跑通否则很容易陷入“一直在画图从来没玩到游戏”的尴尬。只要你把完整项目跑起来一次再看教材里的讲解会发现那些API名词突然都变得亲切了。本文还有配套的精品资源点击获取
分享:

看完干货,该让你的企业上线了

免费需求沟通 · 48 小时内出具建站方案 · 河南本地可上门