news 2026/10/24 1:15:03

Win32 GDI捕鱼游戏开发:从窗口消息到贝塞尔鱼群动画

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
Win32 GDI捕鱼游戏开发:从窗口消息到贝塞尔鱼群动画

简介:本资源是一份面向计算机专业本科生的Windows平台C++课程设计实践项目——“捕鱼达人”小游戏源码包,聚焦Windows API图形编程与面向对象游戏逻辑实现,适用于《Windows程序设计》《游戏开发基础》等课程实训及C++进阶学习。压缩包共36个文件,含10个BMP图像资源(用于背景、鱼群、炮台等UI元素)、6个OGG/MP3/WAV音频文件(音效与背景音乐)、5个TXT说明文档(含游戏规则与资源说明)、4个头文件(如Bullet.h、fish.h)与1个主程序cpp文件,辅以Visual Studio工程文件(.dsw、.dsp)及链接库MSIMG32.LIB,整体7.42MB,结构完整、开箱即用。已有1307人学习下载,提供从窗口创建、GDI绘图、鼠标事件响应、碰撞检测到积分系统实现的全链路代码参考,特别适合初学者理解Windows消息循环机制与游戏主循环设计,并可基于现有框架快速扩展功能或调试优化。

1. 为什么用 Windows API 写捕鱼达人,比直接上 Unity 更快跑通核心逻辑?

这不是一个“教你怎么用引擎做游戏”的教程,而是一次回归本质的实操:用纯 Windows SDK + GDI(Graphics Device Interface)在 Win32 平台上从零搭起一个可交互、有物理反馈、能计分、带音效的捕鱼达人原型。它不追求 4K 粒子特效,但要求你真正理解窗口消息循环怎么驱动帧更新、位图资源如何双缓冲防闪烁、鼠标点击如何穿透到水下坐标系、鱼群运动如何用简化的贝塞尔插值模拟自然游弋——这些恰恰是很多 Unity 开发者在封装层之下容易忽略的底层契约。

这个方案适合三类人:刚学完 C++ 基础想落地第一个图形交互项目的学生;嵌入式或工控场景下需轻量级本地 GUI(无运行时依赖)的工程师;以及想逆向拆解商业捕鱼类游戏底层行为模式的逆向学习者。它不依赖 .NET、不调用 DirectX/OpenGL、不打包任何第三方 DLL,编译后单个 EXE 小于 350KB,Win7 SP1 及以上原生运行。我用它在某高校嵌入式课程中带学生三天内完成从建窗、绘图、响应、计分到打包的全流程,所有代码可直接粘贴进 Visual Studio 新建 Win32 项目复现。


2. 用 CreateWindowEx 和 WNDCLASSEX 注册窗口类:最小可运行框架

2.1 注册窗口类并创建主窗口:四行关键结构体初始化

// 全局变量声明(实际项目中建议封装进类) HINSTANCE g_hInst = nullptr; HWND g_hWnd = nullptr; // 窗口过程函数声明(必须为 CALLBACK,否则消息不触发) LRESULT CALLBACK WndProc(HWND, UINT, WPARAM, LPARAM); // WinMain 入口:标准 Win32 应用起点 int APIENTRY wWinMain(_In_ HINSTANCE hInstance, _In_opt_ HINSTANCE hPrevInstance, _In_ LPWSTR lpCmdLine, _In_ int nCmdShow) { g_hInst = hInstance; // 1. 填充 WNDCLASSEX 结构体:指定窗口样式、背景、光标、图标、窗口过程 WNDCLASSEX wcex = { sizeof(WNDCLASSEX) }; wcex.style = CS_HREDRAW | CS_VREDRAW; // 宽高变化时重绘 wcex.lpfnWndProc = WndProc; // 消息处理函数 wcex.cbClsExtra = 0; wcex.cbWndExtra = 0; wcex.hInstance = hInstance; wcex.hIcon = LoadIcon(hInstance, IDI_APPLICATION); wcex.hCursor = LoadCursor(nullptr, IDC_ARROW); wcex.hbrBackground = (HBRUSH)(COLOR_WINDOW+1); // 白色背景 wcex.lpszMenuName = nullptr; wcex.lpszClassName = L"FishingGameClass"; wcex.hIconSm = LoadIcon(wcex.hInstance, IDI_APPLICATION); // 2. 注册窗口类(必须在 CreateWindowEx 前调用) if (!RegisterClassEx(&wcex)) { MessageBox(nullptr, L"窗口类注册失败", L"错误", MB_ICONERROR); return -1; } // 3. 创建窗口:指定标题、大小、位置、父窗口(nullptr 表示无父窗) g_hWnd = CreateWindowEx( 0, // 扩展样式 L"FishingGameClass", // 类名(必须与 RegisterClassEx 一致) L"捕鱼达人 - Win32 原生版", // 窗口标题 WS_OVERLAPPEDWINDOW & ~WS_MAXIMIZEBOX, // 标准窗口,禁用最大化 CW_USEDEFAULT, CW_USEDEFAULT, // 初始位置(系统自动) 800, 600, // 宽高(像素) nullptr, nullptr, hInstance, nullptr); if (g_hWnd == nullptr) { MessageBox(nullptr, L"窗口创建失败", L"错误", MB_ICONERROR); return -1; } // 4. 显示并更新窗口 ShowWindow(g_hWnd, nCmdShow); UpdateWindow(g_hWnd); // 消息循环:Windows GUI 的心脏 MSG msg = {}; while (GetMessage(&msg, nullptr, 0, 0)) { TranslateMessage(&msg); DispatchMessage(&msg); } return (int) msg.wParam; }

逻辑说明:这段代码构建了 Win32 应用最简骨架。WNDCLASSEX是窗口的“模板”,定义了它长什么样、怎么响应消息;CreateWindowEx是根据模板生成具体实例;GetMessage-TranslateMessage-DispatchMessage构成消息泵,把键盘、鼠标、重绘等事件源源不断地送进WndProc。注意WS_OVERLAPPEDWINDOW & ~WS_MAXIMIZEBOX是刻意禁用最大化按钮——因为捕鱼游戏需要固定分辨率适配水下坐标系,拉伸会导致碰撞检测错乱。

参数说明:

  • CS_HREDRAW | CS_VREDRAW:当窗口宽度或高度变化时强制重绘整个客户区,避免拉伸后图像撕裂;
  • CW_USEDEFAULT:让系统决定初始位置,避免硬编码坐标导致多屏适配失败;
  • 800×600是本项目的基准分辨率,后续所有鱼、炮台、子弹坐标均按此比例设计,不可随意更改;
  • ~WS_MAXIMIZEBOX使用按位取反操作符禁用最大化,比WS_CAPTION | WS_SYSMENU | WS_MINIMIZEBOX更安全,防止遗漏样式位。

2.2 实现 WndProc:拦截 WM_PAINT、WM_MOUSEMOVE、WM_LBUTTONDOWN

// 全局状态变量(实际应封装进 GameEngine 类,此处为简化演示) bool g_bIsShooting = false; POINT g_mousePos = { 0, 0 }; LRESULT CALLBACK WndProc(HWND hWnd, UINT message, WPARAM wParam, LPARAM lParam) { switch (message) { case WM_CREATE: // 窗口创建后立即初始化游戏资源(位图、字体、音频句柄) InitGameResources(hWnd); break; case WM_PAINT: { PAINTSTRUCT ps; HDC hdc = BeginPaint(hWnd, &ps); RenderGame(hdc); // 渲染主逻辑(见 3.1 节) EndPaint(hWnd, &ps); } break; case WM_MOUSEMOVE: g_mousePos.x = GET_X_LPARAM(lParam); g_mousePos.y = GET_Y_LPARAM(lParam); // 鼠标移动时实时更新炮台角度(无需重绘,由定时器统一刷新) break; case WM_LBUTTONDOWN: // 左键按下:触发射击(仅记录状态,不立即渲染,避免阻塞消息循环) g_bIsShooting = true; break; case WM_KEYDOWN: if (wParam == VK_ESCAPE) { PostQuitMessage(0); // ESC 退出 } break; case WM_DESTROY: CleanupGameResources(); // 释放位图、音频等资源 PostQuitMessage(0); break; default: return DefWindowProc(hWnd, message, wParam, lParam); } return 0; }

逻辑说明:WndProc是所有窗口事件的总入口。这里只处理最关键的四类消息:WM_CREATE(初始化)、WM_PAINT(绘制)、WM_MOUSEMOVE(跟踪鼠标)、WM_LBUTTONDOWN(射击触发)。特别注意:不要在WM_LBUTTONDOWN里执行耗时操作(如播放音效、计算弹道),否则会卡住消息循环,导致界面假死。正确做法是设标志位g_bIsShooting = true,由后续的定时器或主循环统一处理。

参数说明:

  • GET_X_LPARAM(lParam)/GET_Y_LPARAM(lParam)是 Windows SDK 提供的宏,用于从lParam中安全提取鼠标坐标(lParam低 16 位是 x,高 16 位是 y);
  • VK_ESCAPE是虚拟键码常量,对应 Esc 键,比硬写27更可读、更健壮;
  • PostQuitMessage(0)向消息队列投递WM_QUIT,使GetMessage返回 0 并退出循环,这是标准退出流程。

3. 用双缓冲 GDI 绘制:解决闪烁、实现鱼群动画与炮台旋转

3.1 双缓冲机制:用内存 DC + BitBlt 避免屏幕撕裂

// 全局缓冲区句柄(实际应作为 GameEngine 成员变量) HDC g_hMemDC = nullptr; HBITMAP g_hMemBitmap = nullptr; HDC g_hScreenDC = nullptr; // 在 WM_CREATE 中调用 void InitGameResources(HWND hWnd) { // 获取屏幕设备上下文(仅一次) g_hScreenDC = GetDC(hWnd); // 创建兼容内存 DC(与屏幕 DC 属性一致) g_hMemDC = CreateCompatibleDC(g_hScreenDC); // 创建兼容位图(800x600,32 位色深) g_hMemBitmap = CreateCompatibleBitmap(g_hScreenDC, 800, 600); if (g_hMemBitmap == nullptr) { MessageBox(hWnd, L"内存位图创建失败", L"错误", MB_ICONERROR); return; } // 将位图选入内存 DC(关键!否则绘图无效) SelectObject(g_hMemDC, g_hMemBitmap); // 加载游戏资源(位图、字体等) LoadGameBitmaps(); LoadGameFonts(); } // 在 WM_PAINT 中调用 void RenderGame(HDC hdc) { // 1. 清空内存 DC(填白色背景) RECT rc = { 0, 0, 800, 600 }; FillRect(g_hMemDC, &rc, (HBRUSH)GetStockObject(WHITE_BRUSH)); // 2. 绘制背景(海面、珊瑚、气泡等静态元素) DrawBackground(g_hMemDC); // 3. 绘制动态对象:鱼群、炮台、子弹、分数 DrawFishes(g_hMemDC); DrawCannon(g_hMemDC); DrawBullets(g_hMemDC); DrawScore(g_hMemDC); // 4. 一次性将内存 DC 内容拷贝到屏幕(消除闪烁) BitBlt(hdc, 0, 0, 800, 600, g_hMemDC, 0, 0, SRCCOPY); } // 在 WM_DESTROY 中调用 void CleanupGameResources() { if (g_hMemBitmap) DeleteObject(g_hMemBitmap); if (g_hMemDC) DeleteDC(g_hMemDC); if (g_hScreenDC) ReleaseDC(g_hWnd, g_hScreenDC); // 释放加载的位图资源 FreeGameBitmaps(); }

逻辑说明:GDI 直接在屏幕 DC 上绘图会导致严重闪烁,因为每一帧绘制都是逐行刷屏。双缓冲的核心思想是:先在内存中画好整帧,再用BitBlt一次性拷贝到屏幕。CreateCompatibleDC创建与屏幕匹配的内存设备上下文,CreateCompatibleBitmap创建同尺寸位图,SelectObject将位图“挂载”到内存 DC 上——这三步缺一不可。FillRect清屏必须在内存 DC 上执行,而非hdc,否则无效。

参数说明:

  • SRCCOPY是BitBlt的光栅操作码,表示“源像素直接覆盖目标”,是最常用且性能最好的模式;
  • GetStockObject(WHITE_BRUSH)返回系统预定义的白色画刷,比CreateSolidBrush(RGB(255,255,255))更省内存、更快;
  • 所有DrawXXX函数都以HDC为参数,确保它们都在内存 DC 上绘图,这是双缓冲生效的前提。

3.2 炮台旋转:用 SetWorldTransform 实现平滑角度变换

// 全局变量:炮台中心点(固定在窗口底部中央) const POINT CANNON_CENTER = { 400, 550 }; // x=400, y=550 // 在 DrawCannon 中调用 void DrawCannon(HDC hdc) { // 1. 保存当前变换矩阵(重要!否则影响后续绘图) SetGraphicsMode(hdc, GM_ADVANCED); XFORM oldXform; GetWorldTransform(hdc, &oldXform); // 2. 计算炮台旋转角度(基于鼠标位置) double angleRad = CalculateCannonAngle(); // 3. 构造旋转矩阵:先平移到中心,旋转,再平移回原位 XFORM xform = { 1, 0, 0, 1, 0, 0 }; xform.eM11 = cos(angleRad); // cosθ xform.eM12 = sin(angleRad); // sinθ xform.eM21 = -sin(angleRad); // -sinθ xform.eM22 = cos(angleRad); // cosθ xform.eDx = CANNON_CENTER.x * (1 - cos(angleRad)) + CANNON_CENTER.y * sin(angleRad); xform.eDy = CANNON_CENTER.y * (1 - cos(angleRad)) - CANNON_CENTER.x * sin(angleRad); // 4. 应用变换并绘制炮管位图 SetWorldTransform(hdc, &xform); DrawBitmap(hdc, IDB_CANNON_TUBE, CANNON_CENTER.x, CANNON_CENTER.y, 64, 64); // 5. 恢复原始变换(关键!否则文字、鱼群也会被旋转) SetWorldTransform(hdc, &oldXform); } double CalculateCannonAngle() { // 计算鼠标相对于炮台中心的角度(弧度制) int dx = g_mousePos.x - CANNON_CENTER.x; int dy = CANNON_CENTER.y - g_mousePos.y; // Y 轴翻转(屏幕坐标系 vs 数学坐标系) return atan2(dy, dx); // 返回 [-π, π] 区间 }

逻辑说明:GDI 默认不支持旋转绘图,必须启用高级图形模式GM_ADVANCED并手动设置世界变换矩阵。XFORM结构体定义了仿射变换(缩放、旋转、平移),其中eDx/eDy是平移分量,需结合旋转中心计算。每次应用变换前必须GetWorldTransform保存旧矩阵,绘制后SetWorldTransform恢复,否则后续所有绘图(包括分数文字)都会被扭曲——这是新手最常踩的坑。

参数说明:

  • atan2(dy, dx)比atan(dy/dx)更鲁棒,能正确处理 dx=0 的情况,并返回完整象限角;
  • CANNON_CENTER.y - g_mousePos.y是 Y 轴翻转,因为屏幕坐标系 Y 向下增长,而数学极坐标 Y 向上增长;
  • DrawBitmap是自定义函数(见 3.3 节),负责将资源 ID 对应的位图绘制到指定位置。

3.3 绘制位图资源:用 LoadImage 加载 PNG 并透明抠图

// 全局位图句柄数组 HBITMAP g_hBmpFish[5] = { nullptr }; // 存储 5 种鱼的位图 HBITMAP g_hBmpCannon = nullptr; HBITMAP g_hBmpBullet = nullptr; // 在 LoadGameBitmaps 中调用 void LoadGameBitmaps() { // 加载 PNG 格式位图(支持 Alpha 通道) g_hBmpCannon = (HBITMAP)LoadImage( g_hInst, MAKEINTRESOURCE(IDB_CANNON_TUBE), IMAGE_BITMAP, 0, 0, LR_LOADTRANSPARENT | LR_LOADMAP3DCOLORS | LR_CREATEDIBSECTION); // 加载鱼群位图(假设资源 ID 从 IDB_FISH_SMALL 到 IDB_FISH_BOSS) for (int i = 0; i < 5; ++i) { UINT resID = IDB_FISH_SMALL + i; g_hBmpFish[i] = (HBITMAP)LoadImage( g_hInst, MAKEINTRESOURCE(resID), IMAGE_BITMAP, 0, 0, LR_LOADTRANSPARENT | LR_LOADMAP3DCOLORS | LR_CREATEDIBSECTION); } } // 绘制带透明通道的位图(关键:使用 TransparentBlt) void DrawBitmap(HDC hdc, HBITMAP hBmp, int x, int y, int width, int height) { if (hBmp == nullptr) return; HDC hMemDC = CreateCompatibleDC(hdc); HBITMAP hOldBmp = (HBITMAP)SelectObject(hMemDC, hBmp); // TransparentBlt 支持 Alpha 通道混合(比 BitBlt 强) TransparentBlt( hdc, // 目标 DC x, y, // 目标左上角 width, height, // 目标宽高 hMemDC, // 源 DC 0, 0, // 源左上角 width, height, // 源宽高 RGB(255, 0, 255) // 透明色键(粉红色,PNG 中通常用此色标透明) ); SelectObject(hMemDC, hOldBmp); DeleteDC(hMemDC); }

逻辑说明:LoadImage比LoadBitmap更强大,支持 PNG、JPEG 等格式,并通过LR_LOADTRANSPARENT自动识别位图中的透明色(默认粉红RGB(255,0,255))。TransparentBlt是 GDI 中唯一能处理 Alpha 通道的位图拷贝函数,它会将源位图中非透明色部分混合到目标 DC 上,实现平滑边缘。注意:SelectObject必须配对使用,DeleteDC不可遗漏,否则内存泄漏。

参数说明:

  • LR_CREATEDIBSECTION强制创建 DIB 位图(Device Independent Bitmap),确保跨 DPI 兼容性;
  • LR_LOADMAP3DCOLORS将 3D 颜色映射(如按钮阴影色)转换为系统颜色,提升视觉一致性;
  • RGB(255,0,255)是经典 Magenta 透明键,Photoshop 导出 PNG 时勾选“透明度”即可自动生成此色。

4. 鱼群行为模拟:用贝塞尔曲线 + 状态机实现自然游动与受击反馈

4.1 鱼类数据结构:封装位置、速度、生命值与状态

// 鱼类状态枚举 enum class FishState { IDLE, // 闲逛 FLEEING, // 受击后逃跑 SINKING, // 死亡下沉 GONE // 已移除 }; // 单条鱼的数据结构 struct Fish { int id; // 唯一 ID(用于调试) FishState state = FishState::IDLE; POINT pos = { 0, 0 }; // 当前屏幕坐标 POINT target = { 0, 0 }; // 目标点(用于贝塞尔路径终点) POINT control1 = { 0, 0 }; // 贝塞尔控制点 1 POINT control2 = { 0, 0 }; // 贝塞尔控制点 2 double speed = 0.0; // 当前游速(像素/帧) double maxSpeed = 0.0; // 最大游速 int value = 0; // 分数价值 int hp = 1; // 生命值(1 表示普通鱼,2+ 表示 BOSS) HBITMAP hBmp = nullptr; // 对应位图 bool isFlipped = false; // 是否水平翻转(决定游动方向) DWORD lastHitTime = 0; // 上次受击时间(毫秒) }; // 全局鱼群容器(实际应为 std::vector<Fish>,此处用数组简化) Fish g_fishes[32]; // 最多 32 条鱼同时存在 int g_fishCount = 0;

逻辑说明:每条鱼是一个独立实体,包含位置、目标、控制点、状态等字段。FishState状态机驱动行为切换:IDLE时沿贝塞尔路径游动;FLEEING时加速远离炮台;SINKING时垂直下落并缩小;GONE表示已回收。isFlipped控制鱼朝向,避免所有鱼同向游动的机械感。lastHitTime用于实现“受击无敌时间”,防止连续命中。

参数说明:

  • POINT是 Windows SDK 内置结构体,x/y成员为LONG类型,足够表示 800×600 坐标;
  • DWORD是 32 位无符号整数,GetTickCount()返回毫秒数,精度满足游戏需求;
  • hBmp指向g_hBmpFish[i]数组中的某个位图,实现资源复用。

4.2 贝塞尔游动算法:用三次贝塞尔曲线生成自然路径

// 计算三次贝塞尔曲线上 t 处的点(t ∈ [0,1]) POINT BezierPoint(const POINT& p0, const POINT& p1, const POINT& p2, const POINT& p3, double t) { double u = 1.0 - t; double tt = t * t; double uu = u * u; double uuu = uu * u; double ttt = tt * t; POINT pt; pt.x = (LONG)(uuu * p0.x + 3 * uu * t * p1.x + 3 * u * tt * p2.x + ttt * p3.x); pt.y = (LONG)(uuu * p0.y + 3 * uu * t * p1.y + 3 * u * tt * p2.y + ttt * p3.y); return pt; } // 更新单条鱼的位置(在游戏主循环中调用) void UpdateFish(Fish& fish, DWORD deltaTimeMs) { if (fish.state == FishState::GONE) return; // 1. 根据状态选择更新策略 switch (fish.state) { case FishState::IDLE: UpdateIdleFish(fish, deltaTimeMs); break; case FishState::FLEEING: UpdateFleeingFish(fish, deltaTimeMs); break; case FishState::SINKING: UpdateSinkingFish(fish, deltaTimeMs); break; } // 2. 边界检测:超出窗口则重置(从左侧或右侧重新入场) if (fish.pos.x < -100 || fish.pos.x > 900 || fish.pos.y < -100 || fish.pos.y > 700) { ResetFish(fish); } } // IDLE 状态:沿贝塞尔路径匀速移动 void UpdateIdleFish(Fish& fish, DWORD deltaTimeMs) { // 时间步进:t 从 0 增加到 1,完成整条路径 fish.t += fish.speed * deltaTimeMs * 0.001; // 转换为秒 if (fish.t >= 1.0) { fish.t = 0.0; // 生成新路径:随机目标点 + 两个控制点 GenerateNewBezierPath(fish); } // 计算当前 t 对应的位置 fish.pos = BezierPoint(fish.pos, fish.control1, fish.control2, fish.target, fish.t); } // 生成新贝塞尔路径(入口点随机,出口点随机,控制点偏移制造弯曲) void GenerateNewBezierPath(Fish& fish) { // 入口点:从窗口左侧或右侧进入 if (rand() % 2 == 0) { fish.pos.x = -50; fish.pos.y = rand() % 300 + 100; // y ∈ [100,400] fish.isFlipped = false; } else { fish.pos.x = 850; fish.pos.y = rand() % 300 + 100; fish.isFlipped = true; } // 出口点:对侧离开 fish.target.x = fish.isFlipped ? -50 : 850; fish.target.y = rand() % 200 + 200; // y ∈ [200,400] // 控制点:随机偏移,制造自然弯曲 fish.control1.x = fish.pos.x + rand() % 200 - 100; fish.control1.y = fish.pos.y + rand() % 100 - 50; fish.control2.x = fish.target.x + rand() % 200 - 100; fish.control2.y = fish.target.y + rand() % 100 - 50; fish.t = 0.0; fish.speed = 0.002 + (rand() % 10) * 0.0002; // 0.002 ~ 0.004 }

逻辑说明:三次贝塞尔曲线由起点p0、两个控制点p1/p2、终点p3定义,公式B(t) = (1-t)^3*p0 + 3*(1-t)^2*t*p1 + 3*(1-t)*t^2*p2 + t^3*p3能生成平滑、可控的曲线路径。UpdateIdleFish中fish.t是归一化参数(0→1),每帧按速度累加,到达 1 后重置并生成新路径。GenerateNewBezierPath保证鱼从左右边界随机入场,控制点随机偏移避免直线感,isFlipped控制朝向。

参数说明:

  • deltaTimeMs是上一帧到当前帧的毫秒差,来自GetTickCount()差值,实现帧率无关运动;
  • rand() % 200 - 100生成 [-100,100] 随机偏移,控制弯曲程度;
  • fish.speed设为0.002 ~ 0.004,意味着走完整条路径需 250~500 帧(约 4~8 秒),符合鱼类游动节奏。

4.3 受击与死亡反馈:状态切换 + 缩放动画 + 分数叠加

// 射击检测:判断子弹是否击中鱼(简化为矩形碰撞) bool IsBulletHitFish(const POINT& bulletPos, const Fish& fish) { // 鱼的包围盒(宽 64px,高 64px,中心在 pos) RECT fishRect = { fish.pos.x - 32, fish.pos.y - 32, fish.pos.x + 32, fish.pos.y + 32 }; return PtInRect(&fishRect, bulletPos); } // 处理鱼受击(在主循环中检测到命中时调用) void OnFishHit(Fish& fish) { if (fish.state == FishState::SINKING || fish.state == FishState::GONE) return; // 1. 减生命值 fish.hp--; // 2. 若未死亡,进入逃跑状态 if (fish.hp > 0) { fish.state = FishState::FLEEING; fish.lastHitTime = GetTickCount(); // 逃跑方向:远离炮台中心 int dx = fish.pos.x - CANNON_CENTER.x; int dy = fish.pos.y - CANNON_CENTER.y; double dist = sqrt(dx*dx + dy*dy); if (dist > 0) { fish.speed = 0.01; // 加速逃跑 fish.control1.x = fish.pos.x + dx * 2; fish.control1.y = fish.pos.y + dy * 2; fish.target.x = fish.pos.x + dx * 5; fish.target.y = fish.pos.y + dy * 5; } return; } // 3. 死亡:进入下沉状态 fish.state = FishState::SINKING; fish.sinkSpeed = 2.0; fish.scale = 1.0; fish.valueAccum = fish.value; // 累计分数(BOSS 可能分多次给) } // SINKING 状态更新:垂直下落 + 缩放 + 透明度渐变 void UpdateSinkingFish(Fish& fish, DWORD deltaTimeMs) { fish.pos.y += (LONG)(fish.sinkSpeed * deltaTimeMs * 0.001); fish.scale *= 0.995; // 每帧缩小 0.5% fish.alpha = (BYTE)(255 * fish.scale); // 透明度随缩放降低 // 下沉到底部后标记为 GONE if (fish.pos.y > 700) { fish.state = FishState::GONE; g_score += fish.valueAccum; fish.valueAccum = 0; } } // 绘制鱼(支持翻转、缩放、透明度) void DrawFish(HDC hdc, const Fish& fish) { if (fish.state == FishState::GONE || fish.hBmp == nullptr) return; HDC hMemDC = CreateCompatibleDC(hdc); HBITMAP hOldBmp = (HBITMAP)SelectObject(hMemDC, fish.hBmp); // 计算绘制区域(考虑缩放) int drawWidth = (int)(64 * fish.scale); int drawHeight = (int)(64 * fish.scale); int x = fish.pos.x - drawWidth / 2; int y = fish.pos.y - drawHeight / 2; // 设置透明度(需启用 Alpha 混合) BLENDFUNCTION blend = { AC_SRC_OVER, 0, fish.alpha, AC_SRC_ALPHA }; AlphaBlend( hdc, x, y, drawWidth, drawHeight, hMemDC, 0, 0, 64, 64, blend ); SelectObject(hMemDC, hOldBmp); DeleteDC(hMemDC); }

逻辑说明:受击检测采用轻量级矩形碰撞(PtInRect),避免复杂几何计算。OnFishHit根据剩余 HP 切换状态:未死则FLEEING(加速+远离炮台),死亡则SINKING(垂直下落+缩放+淡出)。UpdateSinkingFish中fish.scale每帧衰减,驱动尺寸和透明度同步变化,AlphaBlend实现硬件加速的 Alpha 混合。DrawFish中x/y基于中心点计算,确保缩放后仍以鱼为中心。

参数说明:

  • AC_SRC_OVER是 Alpha 混合模式,表示“源像素覆盖目标”;
  • AC_SRC_ALPHA表示使用源位图的 Alpha 通道(若位图无 Alpha,则用blend.SourceConstantAlpha);
  • fish.alpha = (BYTE)(255 * fish.scale)将缩放比例线性映射到 0~255 透明度,实现自然淡出。

5. 避坑指南:Win32 游戏开发中 5 个血泪经验总结

5.1 现象:窗口最大化后鱼群消失或坐标错乱

原因:硬编码了 800×600 坐标,但WM_SIZE消息未处理,GetClientRect返回的实际客户区尺寸变化,导致DrawFish中的x/y计算越界。
解决:在WndProc中添加WM_SIZE处理,重置g_windowWidth/g_windowHeight全局变量,并在所有坐标计算中使用该变量而非固定值;或直接禁用最大化(如 2.1 节所示)。

5.2 现象:鼠标点击无反应,WM_LBUTTONDOWN消息不触发

原因:窗口类注册时wcex.hbrBackground设置为NULL或COLOR_WINDOW(非COLOR_WINDOW+1),导致窗口无背景,Windows 认为“不可点击”。
解决:严格使用(HBRUSH)(COLOR_WINDOW+1),这是 Win32 文档明确要求的背景刷类型;或在WM_ERASEBKGND中手动返回TRUE。

5.3 现象:音效播放卡顿,PlaySound调用后界面冻结

原因:PlaySound默认同步播放(SND_SYNC),阻塞主线程,而游戏逻辑(如UpdateFish)在消息循环中执行,导致帧率骤降。
解决:改用PlaySound(szSound, hInst, SND_RESOURCE | SND_ASYNC | SND_NODEFAULT),SND_ASYNC异步播放,SND_RESOURCE从资源加载,避免文件 I/O。

5.4 现象:双缓冲后文字模糊、锯齿严重

原因:GDI 默认使用TEXT模式绘图,未启用 ClearType 或抗锯齿。
解决:在DrawScore等文字绘制前,调用SetTextRenderingHint(hdc, TextRenderingHintClearTypeGridFit)(需

本文还有配套的精品资源,点击获取

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/10/22 20:19:02

电子劳动合同避坑指南:从平台选择到归档留存,照着做不出错

10 月 9 日&#xff0c;甘肃省人社厅发布了一份电子劳动合同的科普说明&#xff0c;澎湃等平台做了转载。内容不算新&#xff0c;但讲得很系统——把订立电子劳动合同要走的环节、要满足的条件&#xff0c;按顺序梳理了一遍。这类官方说明值得看的原因在于&#xff0c;它把平时…

作者头像 李华
网站建设 2026/10/24 1:14:41

Android应用内悬浮窗实现详解:WindowManager与生命周期管理

先把结论放前面&#xff1a;微信语音通话那种悬浮窗&#xff0c;技术上并不复杂&#xff0c;核心就三件事——WindowManager、WindowManager.LayoutParams、还有一套能陪你折腾半天的生命周期管理。很多人一听到“悬浮窗”就以为是系统级悬浮窗&#xff0c;要申请“悬浮窗权限”…

作者头像 李华
网站建设 2026/10/23 12:10:22

淘宝用户行为分析:Python数据处理实战与避坑指南

1. 拿到一份用户行为日志&#xff0c;先别急着画图很多人做用户行为分析&#xff0c;第一步就是打开 Jupyter Notebook&#xff0c;pd.read_csv一读&#xff0c;然后直接df.groupby(behavior_type).count().plot()出个柱状图&#xff0c;觉得分析就算做完了。我见过太多这样的项…

作者头像 李华
网站建设 2026/10/23 10:11:53

t3code全栈开发实战:TypeScript类型闭环与tRPC最佳实践

1. 从“t3code”这个名字说起&#xff1a;它到底是什么第一次看到“t3code”这个词&#xff0c;很多人会以为是某个新出的编程语言&#xff0c;或者某个开源框架的缩写。实际上&#xff0c;在开发者圈子里&#xff0c;t3code 通常指向的是一套围绕 TypeScript 全栈开发的技术组…

作者头像 李华
网站建设 2026/10/22 8:30:35

机器人退役全流程指南:数据清除、权限回收与硬件处置实操

1. 机器人退役到底在做什么第一次听到“Robot Decommission”这个词&#xff0c;很多人脑子里浮现的画面可能是把机器人关机、拔电、推到仓库角落吃灰。但真正做过产线设备退役的人都知道&#xff0c;这件事远比“关机走人”复杂得多。它本质上是一次有计划的、可追溯的、带风险…

作者头像 李华
网站建设 2026/10/20 23:33:17

Django电商平台开发:蛋糕商城毕业设计实战

1. 项目概述&#xff1a;蛋糕电商平台的Django实现方案这个基于Django的蛋糕购物商城项目&#xff0c;是我在指导毕业设计过程中反复验证过的成熟方案。不同于简单的商品展示网站&#xff0c;它完整实现了电商核心功能链&#xff1a;从用户注册登录、商品展示、购物车管理到订单…

作者头像 李华