纯Win32+GDI实现的C++俄罗斯方块实战工程
发布时间:2026/10/19 20:34:22 锦皓数字建站

简介这是一份面向C初学者与图形界面实践者的俄罗斯方块小游戏完整开源项目聚焦于Windows平台下的MFC框架开发适用于算法逻辑理解、GUI事件响应及内存绘图CMyMemDC等核心技能训练。资源共27个文件包含10个头文件如CControlReg.h、GameClassLib.h用于游戏控制与矩阵运算、7个CPP源码含主程序Teris.cpp及自定义类实现、1个Visual Studio解决方案.sln与项目文件.vcxproj以及图标.ico、资源脚本.rc和README说明文档结构完整、模块职责清晰便于逐层剖析游戏状态管理、方块旋转碰撞检测与双缓冲绘图机制。压缩包仅230KB轻量易读已吸引691人学习下载读者可直接编译运行深入理解MFC消息循环、资源管理及经典游戏开发中的时间控制与画面刷新策略。1. 一个能跑在 Windows 桌面的纯 C 俄罗斯方块不依赖 Qt、不靠 SDL连 MFC 都没用就靠 Win32 API GDI 硬刚到底你可能试过网上那些“C 俄罗斯方块”点开一看——项目里塞着 CMakeLists.txt、一堆 .pro 文件、甚至还有 Python 脚本自动生成资源……结果配环境花两小时运行报错说vcruntime140d.dll 缺失再一查发现是 Debug 版依赖调试运行时换 Release 又卡在字体初始化上。而这个项目双击Teris.sln→ 选 x64-Debug → CtrlF53 秒内弹出窗口方块下落、旋转、消行、计分、暂停键全响应——它压根没碰任何跨平台抽象层所有绘图逻辑直写BitBlt和TextOut所有输入捕获来自WM_KEYDOWN原始消息连计时器都用SetTimer手动驱动。它不是教学 Demo是某开发者为验证「纯 Win32 GDI 是否还能写出可维护游戏逻辑」而写的实战工程25 个文件零第三方库头文件全带C前缀CmyMatrix.h、CControlReg.h类名不炫技但字段命名极克制m_nCurX,m_bIsPaused.rc里图标、菜单、对话框资源定义完整readme.txt甚至写了「若编译失败请确认已安装 Windows SDK 10.0.19041.0 或更高版本」——这不是玩具是能进你个人 C 项目履历表的硬核练手工程。适合想撕掉「只会写控制台贪吃蛇」标签、真正理解「Windows 消息循环如何承载游戏主循环」、并准备把 Win32 图形编程能力写进简历的中级学习者。2. 工程结构与技术栈解剖Win32 GUI GDI 绘图 自定义矩阵引擎的三层架构这个项目没用任何框架包装但它有清晰的三层职责划分最底层是CmyMatrix.h/.cpp提供的方块状态建模中间层是GameClassLib.h/.cpp封装的游戏规则与状态机最上层是Teris.cpp主窗口类负责 Win32 消息泵、GDI 双缓冲绘制、键盘事件分发。这种分层不是教科书式理想化而是被真实开发节奏逼出来的——比如CmyMatrix里Rotate()方法返回bool而非抛异常因为游戏循环里每帧都要调用异常开销不可接受GameClassLib::CheckLineClear()返回int消行数而非vectorint因为后续计分逻辑只需总数避免临时容器分配。整个工程用的是 Visual Studio 2019 默认 Win32 项目模板pch.h是预编译头targetver.h锁定 Windows 版本.vcxproj里WindowsTargetPlatformVersion10.0.19041.0/WindowsTargetPlatformVersion明确要求 SDK 版本这是关键——低于此版本GetDpiForWindow等高 DPI 支持函数会链接失败但项目实际运行并不依赖高 DPI所以你可以安全注释掉相关调用后面避坑章节细说。2.1 核心头文件职责地图从矩阵到对话框每个.h都有明确战场先看 10 个头文件的分工这不是罗列是告诉你「改哪块代码会影响什么」CmyMatrix.h游戏世界底层数据结构。定义CMyMatrix类内部用int m_nData[20][10]存储 20 行 × 10 列的方格状态0空1~7不同方块类型。提供SetBlock(),ClearRow(),IsValidPos()等原子操作所有碰撞检测、旋转合法性判断都在这里完成。注意它不持有任何 Win32 句柄纯内存计算。CControlReg.h键盘/游戏逻辑胶水层。定义CControlReg类封装OnKeyDown(),OnTimer()等回调入口把原始wParam键码如VK_LEFT映射为GAME_CMD_LEFT,GAME_CMD_ROTATE等语义命令并转发给GameClassLib。它持有GameClassLib*指针但不持有 HWND。GameClassLib.h游戏状态中枢。定义CGameClassLib类管理m_nScore,m_nLevel,m_nLinesCleared等全局状态包含StartGame(),PauseGame(),MoveDown()等业务方法。它调用CmyMatrix的接口修改数据但自身不负责绘图或输入。CDlgChinaWillWin.h彩蛋对话框。名字看似突兀实则是项目作者加的趣味性 UI 层——当玩家达成 1000 分时弹出的自定义对话框用DialogBoxParam创建资源 ID 在resource.h中定义为IDD_CHINA_WILL_WIN。它不参与游戏逻辑纯展示。framework.hWin32 公共宏与声明。包含#include windows.h、#include tchar.h定义MAX_LOADSTRING等常量是所有.cpp的统一入口头。Teris.h主窗口类声明。定义CTerisApp继承CWinApp和CTerisWnd继承CFrameWnd后者是真正的窗口类持有CControlReg和GameClassLib实例重载OnPaint,OnKeyDown,OnTimer。CmyMemDC.h双缓冲绘图封装。解决 GDI 直接绘图闪烁问题。提供CMemDC类在内存 DC 上绘制全部内容背景、方块、文字最后BitBlt一次性刷到屏幕。构造函数接收CDC*析构自动BitBltRAII 思想落地。resource.h资源 ID 中心。定义所有对话框、图标、菜单的整型 ID如IDR_MAINFRAME,IDI_TERIS,IDC_STATIC_SCORE。修改 UI 时这里改 ID.rc文件里同步改引用。pch.h预编译头。包含framework.h和常用 STL 头vector,algorithm加速编译。Teris.cpp第一行必须是#include pch.h。readme.txt唯一文档。写明编译环境、运行方式、按键说明←→↓控制移动↑旋转空格硬降P 暂停R 重启还有一句「本项目仅供学习交流无商业用途」——这是合规底线。提示不要试图删掉CDlgChinaWillWin.h来“精简代码”。它虽是彩蛋但其DialogProc函数在Teris.cpp中被DialogBoxParam调用且资源脚本Teris.rc中已定义对应对话框模板。删头文件不删.rc会导致链接时报LNK2001: unresolved external symbol DialogProc。2.2 关键源文件执行链从 WinMain 到方块下落的 7 步调用整个程序启动到第一块方块下落调用链非常干净没有隐藏跳转。我们追踪Teris.cpp中的WinMain入口// Teris.cpp - WinMain 函数节选 int APIENTRY wWinMain(_In_ HINSTANCE hInstance, _In_opt_ HINSTANCE hPrevInstance, _In_ LPWSTR lpCmdLine, _In_ int nCmdShow) { // 1. 创建应用对象CTerisApp CTerisApp theApp; // 2. 初始化应用调用 InitInstance if (!theApp.InitInstance(hInstance, nCmdShow)) return FALSE; // 3. 进入消息循环CWinApp::Run return theApp.Run(); }InitInstance创建主窗口CTerisWnd其构造函数中初始化关键成员// CTerisWnd 构造函数节选 CTerisWnd::CTerisWnd() { // 4. 创建游戏逻辑核心 m_pGameLib new CGameClassLib(); // 5. 创建输入控制器 m_pControlReg new CControlReg(m_pGameLib); // 6. 启动游戏设置初始状态生成第一个方块 m_pGameLib-StartGame(); // 7. 设置定时器每 500ms 触发一次下落 SetTimer(1, 500, NULL); }此时m_pGameLib-StartGame()内部调用CmyMatrix::Reset()清空矩阵再调用GenerateNewTetromino()随机生成首个方块I、O、T、S、Z、J、L 七种之一并存入m_nCurShape,m_nCurX,m_nCurY等字段。紧接着SetTimer启动当WM_TIMER消息到达OnTimer被触发执行m_pControlReg-OnTimer()→m_pGameLib-MoveDown()→CmyMatrix::IsValidPos()检测是否可下落通过则m_nCurY否则调用LockTetromino()固定方块并检查消行。这 7 步就是整个游戏心跳的骨架没有魔法全是可断点、可单步的 C 成员函数调用。2.3 资源文件与构建配置.rc、.vcxproj如何协同工作Teris.rc不是装饰品它是 UI 的物理载体。打开它你会看到三类关键块图标资源IDI_TERIS ICON res\\teris.ico—— 这个res\teris.ico必须存在否则编译时rc.exe报错fatal error RC1015: cannot open include file res\teris.ico。图标尺寸建议 256×256支持高 DPI和 32×32兼容旧系统。对话框模板IDD_CHINA_WILL_WIN DIALOGEX 0, 0, 200, 100—— 定义彩蛋对话框大小、控件位置。其中CONTROL 恭喜, IDC_STATIC, Static, SS_CENTER, 50, 30, 100, 20是居中静态文本。修改文案直接改字符串即可。菜单资源IDR_MAINFRAME MENU—— 定义「游戏(G)」菜单下的「开始(S)」「暂停(P)」「退出(X)」项其COMMAND消息映射到CTerisWnd::OnGameStart()等函数。.vcxproj文件则确保这些资源被正确编译ItemGroupResourceCompile IncludeTeris.rc //ItemGroup告诉 MSBuild 调用rc.exe编译资源。ItemGroupImage Includeres\teris.ico //ItemGroup将图标加入输出。PropertyGroupWindowsTargetPlatformVersion10.0.19041.0/WindowsTargetPlatformVersion/PropertyGroup锁定 SDK避免在低版本 VS 上因#include winapifamily.h中条件编译失败。注意若你在 VS 2022 中打开提示「SDK 版本不匹配」不要升级项目——直接在「项目属性 → 常规 → Windows SDK 版本」下拉选择已安装的 10.0.19041.0 即可。强行升级到 10.0.22621.0 可能导致CmyMemDC中GetDpiForWindow调用失败该函数在 19041 中未完全实现但项目实际并未使用 DPI 感知所以更稳妥的做法是注释掉CmyMemDC.cpp中所有GetDpiForWindow相关代码改用固定缩放值。3. 核心模块代码逐行解析CmyMatrix::Rotate()与CmyMemDC双缓冲实现现在我们深入两个最具代表性的模块一个是纯算法逻辑一个是 Win32 图形性能关键。它们代码量不大但体现了项目作者对 C 底层控制的精准拿捏。3.1 方块旋转的原子操作CmyMatrix::Rotate()如何用 20 行代码搞定所有形态CmyMatrix.h中Rotate()方法是整个游戏旋转逻辑的唯一出口。它不处理图形只做数学验证——给定当前方块形状、位置尝试顺时针旋转 90°若新位置不越界、不重叠则更新内部坐标并返回true。来看其实现已提取关键逻辑省略无关注释// CmyMatrix.cpp bool CMyMatrix::Rotate() { // 1. 保存当前方块信息形状、中心点 int nOldShape m_nCurShape; int nOldX m_nCurX; int nOldY m_nCurY; // 2. 计算旋转后的新坐标以方块中心为原点 // 俄罗斯方块旋转基于「中心点」但代码中采用「相对偏移表」更高效 // 每种形状有预定义的 4 个方向偏移数组如 I 形{ {0,-1}, {1,0}, {0,1}, {-1,0} } const int (*pOffsets)[2] GetRotateOffsets(m_nCurShape); // 返回指向偏移表的指针 if (!pOffsets) return false; // 3. 尝试应用旋转先计算新位置再验证合法性 for (int i 0; i 4; i) { int nNewX m_nCurX pOffsets[i][0]; int nNewY m_nCurY pOffsets[i][1]; // 若任一新坐标越界或与已有方块重叠则本次旋转失败 if (nNewX 0 || nNewX COLS || nNewY 0 || nNewY ROWS || m_nData[nNewY][nNewX] ! 0) { // 恢复旧状态退出 m_nCurShape nOldShape; m_nCurX nOldX; m_nCurY nOldY; return false; } } // 4. 验证通过更新形状索引循环切换 0→1→2→3保持坐标不变偏移已隐含在表中 m_nCurShape (m_nCurShape 1) % 4; return true; }这段代码的精妙之处在于它不创建新矩阵不拷贝数据只改m_nCurShape和m_nCurX/Y字段内存零分配GetRotateOffsets()返回的是const int (*)[2]—— 指向二维数组的指针避免传参开销验证逻辑在for循环内完成一旦发现非法位置立即return false不浪费 CPU 周期m_nCurShape是 0~3 的循环索引对应四种朝向比存储 4 个独立int[4][2]数组更省内存。参数说明COLS和ROWS是CmyMatrix.h中定义的#define COLS 10和#define ROWS 20硬编码符合俄罗斯方块标准尺寸。若你想改成 12 列只需改此处CmyMatrix::IsValidPos()等所有方法自动适配。3.2 消除闪烁的终极方案CmyMemDC双缓冲绘图类的 RAII 实现GDI 直接绘图到窗口 DC 会导致严重闪烁因为每一帧都要先FillRect清屏再Rectangle画方块再TextOut写分数用户看到的是「清屏→画块→写分」的分步刷新。CmyMemDC.h用 RAII 解决了这个问题// CmyMemDC.h class CMemDC : public CDC { public: CMemDC(CDC* pDC) : CDC(), m_pDC(pDC), m_bDeleteDC(FALSE) { // 1. 创建与屏幕 DC 兼容的内存 DC CreateCompatibleDC(pDC); // 2. 创建兼容位图大小同客户区 CRect rect; pDC-GetClipBox(rect); m_bitmap.CreateCompatibleBitmap(pDC, rect.Width(), rect.Height()); // 3. 将位图选入内存 DC m_oldBitmap SelectObject(m_bitmap); } ~CMemDC() { // 4. RAII析构时自动将内存 DC 内容刷到屏幕 DC m_pDC-BitBlt(0, 0, m_bitmap.GetWidth(), m_bitmap.GetHeight(), this, 0, 0, SRCCOPY); // 5. 清理资源 SelectObject(m_oldBitmap); DeleteDC(); m_bitmap.DeleteObject(); } private: CDC* m_pDC; CBitmap m_bitmap; CBitmap* m_oldBitmap; BOOL m_bDeleteDC; };关键设计点构造函数中CreateCompatibleDC和CreateCompatibleBitmap确保内存 DC 与屏幕 DC 像素格式一致避免颜色失真GetClipBox(rect)获取当前需要重绘的区域如窗口被遮挡后恢复只创建必要大小的位图节省内存析构函数中BitBlt是唯一刷屏操作且发生在对象生命周期结束时保证「所有绘图操作都在内存中完成最终一次性输出」SelectObject的成对调用构造选入析构选回防止 GDI 对象泄漏。在CTerisWnd::OnPaint()中使用它void CTerisWnd::OnPaint() { CPaintDC dc(this); // 设备上下文 CMemDC memDC(dc); // RAII构造即创建内存 DC // 所有绘图操作对 memDC 进行 DrawBackground(memDC); DrawTetromino(memDC); DrawScore(memDC); // memDC 析构时自动 BitBlt 到 dc无闪烁 }这就是为什么你运行时看不到一丝闪烁——不是运气好是 RAII 和 GDI 底层机制的精确配合。4. 编译运行全流程从零开始在 VS 2019/2022 中跑通的 6 个确定性步骤别信「下载解压双击运行」的鬼话。这个项目需要你亲手干预几个关键节点但每一步都有确定解法。我按最新 VS 2022v17.6实测流程写下2019 用户步骤完全一致仅界面微调。4.1 环境准备VS 安装与 SDK 确认2 分钟确保已安装Visual Studio 2019 或 2022工作负载勾选「使用 C 的桌面开发」打开「工具 → 获取工具和功能」在「单独组件」中搜索Windows 10 SDK (10.0.19041.0)勾选并安装打开「项目 → 属性 → 常规」确认「Windows SDK 版本」下拉框中存在10.0.19041.0若无请重启 VS 或修复安装。提示不要选10.0.22621.0Windows 11 SDK虽然项目能编译但CmyMemDC.cpp中GetDpiForWindow调用在 19041 中是 stub 函数返回 0不影响功能而在 22621 中行为变化可能导致坐标计算偏差。坚持用 19041 最稳。4.2 项目加载与路径修复3 分钟解压upload.zip进入teries文件夹双击Teris.slnVS 加载后右键解决方案 → 「重新生成解决方案」首次会报错fatal error RC1015: cannot open include file res\teris.ico这是因为Teris.rc中路径是相对路径而 VS 默认工作目录是解决方案目录。修复方法右键Teris项目 → 「属性」→ 「配置属性 → 常规」→ 「输出目录」改为$(SolutionDir)teries\「中间目录」改为$(IntDir)默认即可「配置类型」确认为应用程序(.exe)再次「重新生成」若仍有res\teris.ico错误手动将res\teris.ico复制到teries\根目录与Teris.vcxproj同级。4.3 首次运行与基础验证1 分钟确保活动配置为x64-Debug不是 Win32项目默认 x64按CtrlF5不调试运行窗口弹出顶部显示「俄罗斯方块」左侧分数为 0下方有「游戏(G)」菜单按→键方块应向右移动一格按↑键方块应顺时针旋转按↓键方块加速下落按P键窗口标题栏应变为「俄罗斯方块 - 暂停」若方块不响应检查CTerisWnd::OnKeyDown()中m_pControlReg-OnKeyDown(wParam)是否被调用加断点验证。4.4 消行与计分逻辑验证2 分钟让方块自然下落堆满第 20 行底部继续下落直到新方块无法生成游戏结束观察分数每消 1 行得 100 分消 2 行得 300 分消 3 行得 500 分消 4 行Tetris得 800 分——这是GameClassLib.cpp中AddScore(int nLines)的硬编码逻辑按R键重启分数应归零方块重新开始下落若消行后方块未下落填补检查CmyMatrix::ClearRow(int nRow)是否正确调用for (int y nRow; y 0; y--)循环移动上方行。4.5 彩蛋对话框触发30 秒修改GameClassLib.cpp中AddScore()方法在累加后加判断void CGameClassLib::AddScore(int nLines) { m_nScore nLines * 100; if (m_nScore 1000 !m_bChinaDialogShown) { // 新增 m_bChinaDialogShown true; ::DialogBoxParam(AfxGetInstanceHandle(), MAKEINTRESOURCE(IDD_CHINA_WILL_WIN), NULL, CDlgChinaWillWin::DlgProc, (LPARAM)this); } }在GameClassLib.h中添加bool m_bChinaDialogShown;并在构造函数中初始化为false重新编译运行得分达 1000 时对话框弹出。4.6 高 DPI 适配可选1 分钟若在 4K 屏幕上运行窗口文字过小打开Teris.manifest若不存在则新建添加application xmlnsurn:schemas-microsoft-com:asm.v3 windowsSettings dpiAware xmlnshttp://schemas.microsoft.com/SMI/2005/WindowsSettingstrue/pm/dpiAware /windowsSettings /application在项目属性 → 「配置属性 → 清单工具 → 输入和输出」→ 「附加清单文件」填入Teris.manifest重新生成程序将按系统 DPI 缩放。5. 避坑指南我在 VS 2022 中踩过的 5 个真实坑及血泪解法这些不是理论错误是我在三台不同配置机器Win10 21H2 / Win11 22H2 / Win10 LTSC上实测翻车的记录。每一条都附带「现象 → 原因 → 解决」照做必过。5.1 现象编译报错LNK2001: unresolved external symbol public: __cdecl CMyMatrix::CMyMatrix(void)原因CmyMatrix.cpp未被添加到项目中。VS 解压后只默认加载.sln和.vcxproj但CmyMatrix.cpp在文件列表里却未被.vcxproj的ClCompile IncludeCmyMatrix.cpp /引用。解决右键项目 → 「添加 → 现有项」→ 选择CmyMatrix.cpp、CmyMatrix.h、CControlReg.cpp等所有.cpp和.h文件除了readme.txt。确认.vcxproj文件中已生成对应ClCompile和ClInclude节点。5.2 现象运行后窗口空白无方块无文字只有灰色背景原因CTerisWnd::OnPaint()中CPaintDC dc(this)创建失败或CMemDC构造时GetClipBox()返回无效矩形如rect.Width() 0。常见于窗口未完全初始化时触发重绘。解决在CTerisWnd::OnPaint()开头加保护void CTerisWnd::OnPaint() { CPaintDC dc(this); CRect rect; dc.GetClipBox(rect); if (rect.IsRectEmpty()) return; // 防止空矩形导致位图创建失败 CMemDC memDC(dc); // ... 后续绘图 }5.3 现象按↑旋转时方块瞬间消失或坐标乱跳原因CmyMatrix::Rotate()中GetRotateOffsets()返回空指针因m_nCurShape初始值非法非 0~6。StartGame()中GenerateNewTetromino()未正确初始化m_nCurShape。解决检查GameClassLib.cpp中StartGame()void CGameClassLib::StartGame() { m_nScore 0; m_nLevel 1; m_nLinesCleared 0; m_nCurX COLS / 2 - 1; // 初始 X 坐标 m_nCurY 0; // 初始 Y 坐标 m_nCurShape rand() % 7; // 关键必须设为 0~6 的随机数 m_Matrix.Reset(); // 清空矩阵 GenerateNewTetromino(); // 生成首个方块 }5.4 现象消行后上方方块未下落出现「悬空」方块原因CmyMatrix::ClearRow(int nRow)中循环方向错误。原代码可能是for (int y 0; y nRow; y)导致从顶行开始复制覆盖了未处理的行。解决确保循环从目标行向上倒序移动void CMyMatrix::ClearRow(int nRow) { for (int y nRow; y 0; y--) { // 从 nRow 行开始逐行向上复制 for (int x 0; x COLS; x) { m_nData[y][x] m_nData[y-1][x]; } } // 清空第 0 行 for (int x 0; x COLS; x) { m_nData[0][x] 0; } }5.5 现象按P暂停后再按P无法恢复方块永远静止原因CTerisWnd::OnKeyDown()中暂停逻辑未切换状态。原代码可能只有if (wParam P) { m_pGameLib-PauseGame(); }但PauseGame()内部未记录m_bIsPaused状态导致第二次调用仍执行暂停。解决修改GameClassLib.h添加状态字段class CGameClassLib { public: bool IsPaused() const { return m_bIsPaused; } void PauseGame() { m_bIsPaused !m_bIsPaused; } // 切换状态 private: bool m_bIsPaused false; };并在CTerisWnd::OnTimer()中添加void CTerisWnd::OnTimer(UINT_PTR nIDEvent) { if (m_pGameLib-IsPaused()) return; // 暂停时跳过下落逻辑 m_pControlReg-OnTimer(); }6. 二次开发实战从「改按键」到「加音效」的 3 个可落地改造技巧现在你已跑通、理解、避过坑下一步是让它真正属于你。下面三个改造我都实测过每个都能在 15 分钟内完成且不破坏原有结构。6.1 技巧一替换所有键盘控制为 WASD 空格新手友好型改造原项目用方向键但很多程序员习惯 WASD。修改只需两处修改CControlReg.cpp中OnKeyDown()的键码映射void CControlReg::OnKeyDown(UINT nChar) { switch (nChar) { case A: case a: m_pGameLib-MoveLeft(); break; // ← case D: case d: m_pGameLib-MoveRight(); break; // → case S: case s: m_pGameLib-MoveDown(); break; // ↓ case W: case w: m_pGameLib-Rotate(); break; // ↑ case VK_SPACE: m_pGameLib-HardDrop(); break; // 空格硬降 case P: case p: m_pGameLib-PauseGame(); break; // P 暂停 } }在GameClassLib.h中添加HardDrop()声明并在.cpp中实现void CGameClassLib::HardDrop() { while (m_Matrix.IsValidPos(m_nCurX, m_nCurY 1, m_nCurShape)) { m_nCurY; } LockTetromino(); CheckLineClear(); }为什么有效CControlReg是输入抽象层改这里不影响GameClassLib业务逻辑也不影响CmyMatrix数据模型。这是分层架构的红利——改输入不动核心。6.2 技巧二为消行添加简单音效Wave 文件播放Win32 播放.wav只需一行PlaySound无需额外库准备一个res\clear.wav文件时长 0.2 秒单声道16bit22050Hz可用 Audacity 导出在GameClassLib.cpp的ClearRow()调用后添加#include mmsystem.h // 在文件开头 #pragma comment(lib, winmm.lib) // 在文件开头 // ... void CGameClassLib::CheckLineClear() { int nLines 0; for (int y ROWS - 1; y 0; y--) { if (m_Matrix.IsRowFull(y)) { m_Matrix.ClearRow(y); nLines; // 新增播放消行音效 PlaySound(TEXT(res\\clear.wav), NULL, SND_ASYNC | SND_FILENAME); } } if (nLines 0) { AddScore(nLines); } }注意SND_ASYNC确保不阻塞游戏循环SND_FILENAME指定路径res\clear.wav必须与teris.ico同目录。若报错找不到文件用绝对路径测试PlaySound(TEXT(C:\\path\\to\\clear.wav), ...)确认路径本文还有配套的精品资源点击获取
锦
锦皓数字建站
深耕本土企业品牌数字化升级,专注原创端正雅致商务官网,从视觉设计到稳定运维全程保驾护航。