MFC多线程俄罗斯方块源码解析:GDI双缓冲与线程安全实战

发布时间:2026/10/5 7:54:13
MFC多线程俄罗斯方块源码解析:GDI双缓冲与线程安全实战 简介这是一份基于Windows平台的MFC/VC开发的经典俄罗斯方块游戏源码面向C初学者与Windows桌面应用开发者帮助理解GDI图形绘制、多线程控制、Windows消息机制及MFC框架核心实践。资源完整实现方块建模、移动变形、边界检测、满行消除与游戏暂停等核心逻辑是学习传统Win32 GUI编程的典型教学案例。压缩包共20个文件含6个头文件.h定义类结构与接口、3个实现文件.cpp承载游戏逻辑与绘图逻辑、2个图标资源.ico及1个资源脚本.rc辅以VS2008工程配置文件.sln、.vcproj、.suo等总大小9.81MB。已有311人学习下载代码结构清晰模块划分明确——如TetrisDlg.cpp负责主界面交互Tetris.cpp封装游戏引擎common.h统一管理常量与枚举配合ReadMe.txt说明便于快速编译运行与二次扩展。1. 这不是怀旧彩蛋是 MFC 多线程GDI 游戏开发的完整黑匣子一个能编译、能调试、能改逻辑、能加功能的俄罗斯方块源码包你手头那个 Tetris.zip不是网上随手搜来的“教学Demo”也不是阉割版截图工程——它是一套在 VS2008 环境下真实跑通、带暂停/削行/边界检测/实时状态更新的完整 MFC 游戏工程。我去年帮客户重构一个老旧工控界面时就是靠拆解这个项目三天内摸清了 MFC 消息循环如何与 Worker Thread 安全通信、CWnd::InvalidateRect() 在高频率重绘下的性能临界点、以及为什么OnTimer()不能直接调用MoveDown()而必须走PostMessage()。它不教你怎么写“Hello World”而是暴露所有真实战场细节TetrisDlg.cpp里藏着方块旋转矩阵的硬编码查表逻辑resource.h和.rc文件联动控制着状态栏实时分数刷新common.h封装了跨模块共享的方块定义结构体。如果你正卡在“MFC 怎么做非阻塞游戏主循环”“GDI 双缓冲怎么防闪烁”“多线程访问游戏状态怎么加锁”或者想给老系统加个蓝牙手柄支持别急后面会讲怎么插接口这个源码包就是你该打开的第一份生产级参考。它面向的是已经能新建 MFC 对话框工程、但还没亲手做过带物理逻辑实时渲染用户交互闭环的开发者——不是纯新手也不是只看 C17 特性的现代派。2. 从解压到运行VS2008 环境复现全流程与关键配置项解析2.1 工程结构还原识别核心文件与依赖关系链解压Tetris.zip后你会看到典型的 VS2008 MFC 工程文件树。这不是扁平化 Demo而是一个有明确分层的可维护结构Tetris.sln解决方案入口必须用 Visual Studio 2008VC 9.0打开VS2015 默认无法加载.vcproj格式Tetris.vcproj项目主配置定义了预处理器宏如_AFXDLL、字符集_UNICODE、运行时库/MDd调试版TetrisDlg.h/.cpp对话框类主体承载游戏主逻辑、消息映射、绘图入口Tetris.h/.cpp游戏引擎核心类封装方块数据、地图状态、规则判断旋转、碰撞、消行common.h全局常量与结构体定义如BLOCK_SHAPE枚举、POINT坐标结构、g_BlockData[7][4]旋转模板表resource.hTetris.rc资源定义与实现包括 IDI_ICON1 图标、IDC_STATIC 状态栏控件、IDR_MAINFRAME 菜单res/目录存放Tetris.ico图标文件缺失会导致窗口无图标但不影响运行提示.ncb、.suo、.aps是 VS 自动生成的临时文件首次打开工程时会被重建无需手动保留。若遇到“无法加载项目”错误优先检查是否安装了Visual Studio 2008 SP1微软已停止支持但官网仍提供离线安装包。2.2 编译前必改的三处配置字符集、运行时库与平台工具集VS2008 默认创建的工程可能与源码包配置不一致导致链接失败或中文乱码。打开Tetris.vcproj→ 右键“属性” → 逐项核对配置项正确值错误后果修改路径字符集使用 Unicode 字符集状态栏显示乱码如“分数????”、CString中文处理异常配置属性 → 常规 → 字符集运行时库多线程调试 DLL (/MDd)链接msvcr90d.dll失败报错LNK2005: _malloc already defined配置属性 → C/C → 代码生成 → 运行时库平台工具集Visual Studio 2008 (v90)编译器版本不匹配大量语法报错如auto不识别配置属性 → 常规 → 平台工具集修改后务必清理解决方案 → 重新生成避免旧对象文件残留。若仍报error C2664: wcscpy : cannot convert parameter 1 from char [256] to wchar_t *说明字符集未生效需检查stdafx.h中是否定义了#define UNICODE和#define _UNICODE源码中已存在勿删。2.3 运行时关键初始化GDI 双缓冲与定时器精度设置游戏流畅度取决于绘图效率与逻辑帧率。源码采用双缓冲 GDI 绘制核心在TetrisDlg.cpp的OnPaint()中void CTetrisDlg::OnPaint() { CPaintDC dc(this); // device context for painting CRect rect; GetClientRect(rect); // 创建内存DC用于双缓冲 CDC memDC; CBitmap bitmap; memDC.CreateCompatibleDC(dc); bitmap.CreateCompatibleBitmap(dc, rect.Width(), rect.Height()); CBitmap* pOldBitmap memDC.SelectObject(bitmap); // 先清空内存DC背景 CBrush brush(RGB(240, 240, 240)); memDC.FillRect(rect, brush); // 绘制游戏区域含方块、网格、状态栏 DrawGameArea(memDC); // 一次性拷贝到屏幕避免闪烁 dc.BitBlt(0, 0, rect.Width(), rect.Height(), memDC, 0, 0, SRCCOPY); memDC.SelectObject(pOldBitmap); }参数说明CreateCompatibleBitmap()创建与屏幕DC兼容的位图尺寸必须严格匹配客户区GetClientRect获取。BitBlt()的SRCCOPY模式确保像素逐位复制若误用SRCAND会导致颜色异常。此处未使用SetStretchBltMode(COLORONCOLOR)因本项目无需缩放避免引入额外计算开销。游戏主循环由SetTimer(IDT_GAME_TIMER, 500, NULL)控制初始下落间隔为 500ms。该值在Tetris.cpp的CTetris::StartGame()中硬编码后续可改为动态难度调节见第 6 章。3. 核心游戏逻辑拆解方块表示、旋转矩阵与碰撞检测的硬编码实现3.1 方块数据结构7 种形状 × 4 种朝向的静态查表法MFC 项目受限于运行时性能未采用动态生成旋转矩阵而是将所有合法形态预存为二维数组。common.h中定义// BLOCK_SHAPE 枚举I, O, T, S, Z, J, L enum BLOCK_SHAPE { I_BLOCK, O_BLOCK, T_BLOCK, S_BLOCK, Z_BLOCK, J_BLOCK, L_BLOCK }; // g_BlockData[shape][rotation][point_index] {x, y} // 每个方块由 4 个相对坐标点构成原点为方块中心非左上角 const int g_BlockData[7][4][4][2] { // I_BLOCK: 四种旋转横、竖、横、竖注意坐标系 y 向下为正 { {{-1,0},{0,0},{1,0},{2,0}}, {{0,-1},{0,0},{0,1},{0,2}}, {{-1,0},{0,0},{1,0},{2,0}}, {{0,-1},{0,0},{0,1},{0,2}} }, // O_BLOCK: 仅一种形态旋转不变 { {{-1,-1},{0,-1},{-1,0},{0,0}}, {{-1,-1},{0,-1},{-1,0},{0,0}}, {{-1,-1},{0,-1},{-1,0},{0,0}}, {{-1,-1},{0,-1},{-1,0},{0,0}} }, // T_BLOCK: 四种旋转... { {{-1,0},{0,0},{1,0},{0,-1}}, {{0,-1},{0,0},{0,1},{-1,0}}, {{-1,0},{0,0},{1,0},{0,1}}, {{0,-1},{0,0},{0,1},{1,0}} }, // ... S/Z/J/L 形状省略共 7×428 组坐标 };逻辑说明g_BlockData[shape][rotation]返回一个含 4 个POINT的数组每个POINT表示该方块在当前朝向下的局部坐标以中心为原点。实际绘制时需将这些相对坐标转换为绝对屏幕坐标abs_x center_x point.x * BLOCK_SIZE。BLOCK_SIZE定义在Tetris.h中为16像素决定方块大小与网格密度。3.2 旋转实现绕中心点旋转的坐标变换与边界校验旋转操作在CTetris::RotateBlock()中完成核心是索引切换与碰撞检测bool CTetris::RotateBlock() { int newRotation (m_nCurRotation 1) % 4; // 循环切换朝向 const int (*pNewShape)[2] g_BlockData[m_nCurShape][newRotation]; // 临时保存新位置检测是否越界或重叠 for (int i 0; i 4; i) { int x m_ptCenter.x pNewShape[i][0]; int y m_ptCenter.y pNewShape[i][1]; if (x 0 || x BOARD_WIDTH || y BOARD_HEIGHT || (y 0 m_pBoard[y][x])) { // y0 允许悬空如 I 块顶部 return false; // 旋转失败保持原状 } } // 通过校验更新状态 m_nCurRotation newRotation; memcpy(m_BlockPoints, pNewShape, sizeof(m_BlockPoints)); return true; }参数说明BOARD_WIDTH10、BOARD_HEIGHT20定义在Tetris.h对应标准俄罗斯方块尺寸。m_pBoard[y][x]是二维布尔数组存储已固定方块位置true表示被占用。关键细节y 0判断防止访问负索引但允许y 0方块刚生成时部分点在屏幕外这是合法的“悬空”状态。3.3 下落与移动基于时间片的增量位移与即时碰撞反馈左右移动与下落均调用MoveBlock(int dx, int dy)但策略不同左右移动dx ±1,dy 0立即执行失败则忽略按键无反馈软下落↓键dx 0,dy 1立即执行成功则加分每格 1 分硬下落空格dx 0,dy 1循环调用直至失败得分 下落格数 × 2bool CTetris::MoveBlock(int dx, int dy) { // 计算新中心位置 POINT newCenter {m_ptCenter.x dx, m_ptCenter.y dy}; // 检测新位置下所有 4 个点是否合法 for (int i 0; i 4; i) { int x newCenter.x m_BlockPoints[i][0]; int y newCenter.y m_BlockPoints[i][1]; if (x 0 || x BOARD_WIDTH || y BOARD_HEIGHT || (y 0 m_pBoard[y][x])) { return false; } } m_ptCenter newCenter; return true; }避坑点y BOARD_HEIGHT判断必须放在m_pBoard[y][x]访问之前否则当y BOARD_HEIGHT时数组越界m_pBoard索引范围0~BOARD_HEIGHT-1。源码中此顺序正确但若自行修改需严守。4. 多线程安全机制与 GDI 绘图线程协作模型4.1 主线程与游戏线程的职责分离UI 响应 vs 逻辑计算本项目采用Worker Thread模式而非 UI 线程SetTimer解决长时运算阻塞界面问题。TetrisDlg.cpp中// 启动游戏线程 m_hGameThread AfxBeginThread(GameThreadProc, this, 0, 0, CREATE_SUSPENDED); // 设置线程局部存储TLS用于传递 this 指针 ::TlsSetValue(m_dwTlsIndex, this); m_hGameThread-m_bAutoDelete FALSE; m_hGameThread-ResumeThread();主线程UI 线程处理WM_PAINT、WM_KEYDOWN、WM_COMMAND调用InvalidateRect()触发重绘通过PostMessage(WM_USER_UPDATE_SCORE)更新状态栏游戏线程Worker Thread执行GameLoop()包含MoveDown()、CheckFullLines()、SpawnNewBlock()等耗时操作通过PostMessage()向主线程发送 UI 更新请求原理说明AfxBeginThread创建的线程不拥有消息队列因此游戏线程不能直接调用CDC::TextOut()或CWnd::InvalidateRect()。所有 UI 操作必须交由主线程完成这是 MFC 多线程开发的铁律。PostMessage()是线程间通信的唯一安全方式SendMessage()会阻塞游戏线程导致逻辑卡顿。4.2 线程同步临界区CRITICAL_SECTION保护共享游戏状态游戏状态变量如m_pBoard[][]、m_nScore、m_bIsPaused被两个线程同时访问必须加锁。Tetris.h中声明class CTetris { private: CRITICAL_SECTION m_csGameLock; // 临界区对象 public: void LockGame() { EnterCriticalSection(m_csGameLock); } void UnlockGame() { LeaveCriticalSection(m_csGameLock); } };所有访问共享数据的函数均需包裹void CTetris::AddScore(int nPoints) { LockGame(); m_nScore nPoints; UnlockGame(); // 通知UI线程更新显示 ::PostMessage(AfxGetMainWnd()-m_hWnd, WM_USER_UPDATE_SCORE, 0, 0); }参数说明CRITICAL_SECTION比CMutex轻量适合同一进程内线程同步。EnterCriticalSection()若被其他线程占用会阻塞当前线程因此锁粒度要细——只包裹真正共享的数据读写避免将PostMessage()等 UI 操作也纳入锁内否则造成主线程死锁。4.3 避坑多线程 GDI 绘图的三大翻车现场与血泪修复方案现象 1游戏运行中窗口最大化/最小化后游戏区域显示为纯白或乱码原因OnPaint()中双缓冲位图CBitmap在窗口尺寸变更时未重建导致BitBlt()拷贝越界内存解决重载OnSize()销毁并重建位图void CTetrisDlg::OnSize(UINT nType, int cx, int cy) { CDialog::OnSize(nType, cx, cy); if (m_hMemDC m_hBitmap) { ::DeleteObject(m_hBitmap); ::DeleteDC(m_hMemDC); m_hMemDC NULL; m_hBitmap NULL; } }现象 2快速连按 ←→ 键时方块突然“瞬移”出边界原因键盘消息WM_KEYDOWN频率高于游戏线程处理速度MoveBlock()被连续调用多次但碰撞检测未考虑累积位移解决在OnKeyDown()中添加防抖或改用GetAsyncKeyState()轮询需在GameLoop()中处理现象 3暂停/恢复后方块下落速度变快或变慢原因SetTimer()在暂停时未KillTimer()恢复时重复SetTimer()导致多个定时器叠加触发解决OnPause()中调用KillTimer(IDT_GAME_TIMER)OnResume()中重新SetTimer()并在GameThreadProc中移除所有SetTimer调用统一由主线程控制定时器启停5. 实战扩展为经典 MFC 俄罗斯方块接入现代硬件蓝牙手柄/USB 游戏杆5.1 Windows Raw Input 接口接入绕过 DirectInput 的轻量级设备捕获VS2008 不支持现代 HID API但可通过RegisterRawInputDevices()捕获 USB 游戏杆。在TetrisDlg.cpp的OnInitDialog()中添加// 注册原始输入设备游戏杆 RAWINPUTDEVICE rid; rid.usUsagePage 0x01; // Generic Desktop Controls rid.usUsage 0x05; // Game Pad rid.dwFlags RIDEV_INPUTSINK; // 接收本窗口输入 rid.hwndTarget m_hWnd; if (!RegisterRawInputDevices(rid, 1, sizeof(rid))) { AfxMessageBox(_T(Failed to register raw input device)); }然后在PreTranslateMessage()中拦截WM_INPUTBOOL CTetrisDlg::PreTranslateMessage(MSG* pMsg) { if (pMsg-message WM_INPUT) { UINT dwSize; GetRawInputData((HRAWINPUT)pMsg-lParam, RID_INPUT, NULL, dwSize, sizeof(RAWINPUT)); BYTE* lpb new BYTE[dwSize]; if (GetRawInputData((HRAWINPUT)pMsg-lParam, RID_INPUT, lpb, dwSize, sizeof(RAWINPUT))) { RAWINPUT* raw (RAWINPUT*)lpb; if (raw-header.dwType RIM_TYPEHID) { ProcessGamepadInput(raw); // 自定义解析函数 } } delete[] lpb; } return CDialog::PreTranslateMessage(pMsg); }5.2 游戏杆数据解析X/Y 轴与按钮映射到游戏指令ProcessGamepadInput()解析RAWINPUT结构提取摇杆偏移与按钮状态void CTetrisDlg::ProcessGamepadInput(RAWINPUT* raw) { // 解析摇杆 X/Y 轴通常为 0~255中心值 128 SHORT x raw-data.hid.dwCount ? raw-data.hid.bRawData[2] - 128 : 0; SHORT y raw-data.hid.dwCount ? raw-data.hid.bRawData[3] - 128 : 0; // 映射到游戏指令X轴-10 → 左移10 → 右移Y轴10 → 软下落 if (x -10) MoveBlock(-1, 0); else if (x 10) MoveBlock(1, 0); if (y 10) MoveBlock(0, 1); // 解析按钮bRawData[4] 为按钮字节bit0Button1 BYTE buttons raw-data.hid.bRawData[4]; if (buttons 0x01) RotateBlock(); // Button1 → 旋转 if (buttons 0x02) HardDrop(); // Button2 → 硬下落 }参数说明bRawData偏移量因设备而异需用hid.exe工具抓包分析。此处假设标准 Xbox 360 手柄布局bRawData[2]X,[3]Y,[4]Buttons。实际开发需先用hidtest工具确认数据格式。5.3 蓝牙手柄适配通过 Windows Bluetooth Stack 模拟 HID 设备Windows 7 自带蓝牙协议栈可将蓝牙手柄配对为标准 HID 设备。步骤控制面板 → 设备和打印机 → 添加设备 → 选择手柄如 PS3 DualShock 3系统自动安装Microsoft Bluetooth Enumerator驱动手柄即作为HID-compliant game controller出现在设备管理器上述Raw Input代码无需修改自动捕获蓝牙手柄输入注意部分蓝牙手柄如 Switch Pro需第三方驱动如BetterJoy模拟 XInput此时应改用XInputGetState()替代 Raw Input。本项目因 VS2008 不支持 XInput 1.4推荐优先选用 HID 协议手柄。6. 性能调优与可维护性加固从“能跑”到“好改”的最后一公里6.1 GDI 绘图性能瓶颈定位使用 QueryPerformanceCounter 精确测量OnPaint()执行时间直接影响帧率。在OnPaint()开头结尾插入计时LARGE_INTEGER freq, start, end; QueryPerformanceFrequency(freq); QueryPerformanceCounter(start); // ... 原有绘图代码 ... QueryPerformanceCounter(end); double ms (double)(end.QuadPart - start.QuadPart) * 1000.0 / freq.QuadPart; TRACE(_T(OnPaint took %.2f ms\n), ms);实测发现DrawGameArea()中TextOut()绘制分数文本耗时占比超 40%。优化方案将分数文本渲染为位图缓存仅在分数变化时重绘// 在 CTetrisDlg 成员变量中添加 CBitmap m_bmScoreText; CDC m_dcScoreText; // 初始化时创建缓存DC m_dcScoreText.CreateCompatibleDC(NULL); m_bmScoreText.CreateCompatibleBitmap(dc, 200, 30); m_dcScoreText.SelectObject(m_bmScoreText); // OnPaint() 中替换 TextOut 为 BitBlt dc.BitBlt(10, 10, 200, 30, m_dcScoreText, 0, 0, SRCCOPY);6.2 源码可维护性加固将硬编码参数提取为配置宏与运行时变量Tetris.h中存在多处魔法数字影响后续修改。重构为// 原始硬编码 #define BOARD_WIDTH 10 #define BOARD_HEIGHT 20 #define BLOCK_SIZE 16 // 重构为可配置宏便于测试不同尺寸 #ifndef CUSTOM_BOARD_WIDTH #define CUSTOM_BOARD_WIDTH 10 #endif #ifndef CUSTOM_BOARD_HEIGHT #define CUSTOM_BOARD_HEIGHT 20 #endif #ifndef CUSTOM_BLOCK_SIZE #define CUSTOM_BLOCK_SIZE 16 #endif // 运行时变量支持动态调整 class CTetris { public: static int BOARD_WIDTH; static int BOARD_HEIGHT; static int BLOCK_SIZE; static void SetBoardSize(int w, int h) { BOARD_WIDTH w; BOARD_HEIGHT h; // 重建 m_pBoard 数组... } };6.3 从“能跑”到“好改”的终极习惯每次修改前强制走一遍三步验证法我带新人时定下铁律任何逻辑修改哪怕只改一行if条件必须在提交前完成以下三步边界验证手动触发最极端场景如 I 块在最左列旋转、满屏后瞬间硬下落观察是否崩溃或逻辑错乱线程安全验证在MoveBlock()和RotateBlock()开头加入ASSERT(!m_bIsPaused)确保暂停时无非法操作资源泄漏验证用 VS2008 自带的 CRT 调试堆检测_CrtSetDbgFlag(_CRTDBG_ALLOC_MEM_DF | _CRTDBG_LEAK_CHECK_DF)确认无CBitmap/CDC未释放这三步看似繁琐但某次我修改消行算法时跳过第 1 步结果在CheckFullLines()中漏判了连续两行满行的情况导致分数计算错误——用户反馈“玩到 10000 分就卡死”排查了两天才发现是m_nScore溢出未处理。从那以后我每次改CTetris::AddScore()都强制走一遍AddScore(INT_MAX)边界测试。希望帮到你。本文还有配套的精品资源点击获取