资讯中心

用纯C和Win32 API从零构建Windows原生跳棋游戏

📅 2026/8/2 19:16:01
用纯C和Win32 API从零构建Windows原生跳棋游戏
1. 项目概述从零构建一个Windows原生跳棋游戏最近在整理自己的项目仓库翻到了一个几年前用纯C语言和Windows API写的跳棋游戏。这个项目虽然界面朴素但五脏俱全从窗口创建、消息循环、图形绘制到游戏逻辑判断全部手搓完成。它不像现在用Unity或Qt那样快速但能让你透彻理解一个桌面应用程序特别是游戏在Windows系统下究竟是如何“跑”起来的。如果你对C/C底层开发感兴趣或者厌倦了各种框架的封装想亲手摸一摸Windows GUI编程的“筋骨”那么这个实战项目会是一个绝佳的练手材料。它适合有一定C语言基础对Win32 API好奇并且想通过一个完整项目来巩固知识的开发者。通过这个项目你将不仅仅是在写一个游戏更是在学习如何与操作系统直接对话构建一个真正的原生Windows窗口程序。2. 核心架构与设计思路拆解2.1 为什么选择纯C与Windows API在游戏引擎和跨平台框架大行其道的今天选择纯C和原生Windows API似乎有些“复古”。但这恰恰是本项目的核心价值所在。首先它极度轻量最终生成的可执行文件可能只有几十KB不依赖任何运行时库除了系统自带的user32.dll, gdi32.dll等启动速度极快。其次它提供了无与伦比的掌控感。每一个像素的绘制、每一次鼠标点击的消息处理、每一帧的刷新都由你直接控制没有中间层带来的性能损耗和认知黑盒。这对于理解计算机图形学基础、事件驱动编程模型至关重要。最后这是对Windows编程基础的夯实。Win32 API是Windows GUI的基石MFC、WPF乃至一些游戏框架底层都与之交互。掌握它就如同掌握了内功心法再看其他上层框架会豁然开朗。本项目的设计目标很明确实现一个双人本地对战的跳棋游戏。核心模块可以划分为四块窗口管理模块负责创建主窗口、处理系统消息、图形渲染模块负责绘制棋盘、棋子和动画、游戏逻辑模块负责棋子移动规则、胜负判定以及用户交互模块负责处理鼠标事件。所有模块都通过一个核心的消息循环Message Loop串联起来。2.2 核心数据结构设计游戏的核心是数据表示。我们如何用代码来描述棋盘和棋子一个直观的想法是用一个二维数组比如int board[8][8]。但国际跳棋Checkers的棋盘是8x8且只有一半的深色格子是有效棋位。我们可以用值0表示空位1表示红方棋子2表示黑方棋子3和4表示升变后的“王棋”。但这样数组中有近一半的空间是浪费的无效的浅色格。更高效的方法是使用一个一维数组只存储有效的32个棋位。我们可以定义一个POINT结构数组来存储每个棋位在屏幕上的像素坐标再用一个int pieces[32]来对应每个位置上的棋子状态。typedef struct { int x; // 屏幕横坐标 int y; // 屏幕纵坐标 int index; // 在逻辑数组中的索引 } BoardCell; typedef struct { BoardCell cells[32]; // 32个有效棋位 int piece[32]; // 棋子状态0空1红兵2黑兵3红王4黑王 int currentPlayer; // 当前行棋方1红方2黑方 int selectedIndex; // 当前选中的棋子索引-1表示未选中 POINT possibleMoves[16]; // 当前选中棋子可走的位置 int moveCount; // 可走位置的数量 } GameState;GameState结构体就是整个游戏的“大脑”它封装了某一时刻游戏的全部状态。所有绘制和逻辑判断都围绕这个状态进行。注意在Windows API编程中大量使用POINT,RECT,SIZE等预定义结构体。与它们打交道是家常便饭例如在计算绘制区域和碰撞检测时。3. 开发环境搭建与Windows编程基础3.1 配置极简C开发环境你不需要庞大的Visual Studio IDE。一个文本编辑器如VSCode加上MinGW-w64编译器就足够了。这也是当前很多开发者偏好的轻量级组合。安装MinGW-w64去 MinGW-w64官网 下载安装程序选择x86_64架构和win32线程模型。安装后将bin目录例如C:\mingw64\bin添加到系统的PATH环境变量中。配置VSCode安装C/C扩展。然后创建一个项目文件夹在里面新建.vscode文件夹并创建三个配置文件c_cpp_properties.json配置编译器路径和包含目录。{ configurations: [ { name: Win32, includePath: [ ${workspaceFolder}/**, C:/mingw64/x86_64-w64-mingw32/include // 你的MinGW包含路径 ], defines: [_DEBUG, UNICODE, _UNICODE], windowsSdkVersion: 10.0.20348.0, // 可选使用系统SDK compilerPath: C:/mingw64/bin/gcc.exe, cStandard: c17, cppStandard: c17, intelliSenseMode: windows-gcc-x64 } ], version: 4 }tasks.json配置编译构建任务。{ version: 2.0.0, tasks: [ { label: build checkers, type: shell, command: gcc, args: [ -g, ${workspaceFolder}/*.c, -o, ${workspaceFolder}/checkers.exe, -mwindows, // 关键指定生成GUI程序不显示控制台 -lgdi32 ], group: { kind: build, isDefault: true }, problemMatcher: [$gcc] } ] }launch.json配置调试任务。{ version: 0.2.0, configurations: [ { name: (gdb) Launch, type: cppdbg, request: launch, program: ${workspaceFolder}/checkers.exe, args: [], stopAtEntry: false, cwd: ${workspaceFolder}, environment: [], externalConsole: false, MIMode: gdb, miDebuggerPath: C:/mingw64/bin/gdb.exe, setupCommands: [ { description: Enable pretty-printing for gdb, text: -enable-pretty-printing, ignoreFailures: true } ] } ] }关键编译参数-mwindows告诉GCC链接Windows GUI子系统库而不是控制台子系统。这样程序启动时就不会弹出黑乎乎的控制台窗口了。-lgdi32则是显式链接GDI32库用于图形绘制。3.2 Windows程序骨架WinMain与窗口过程每一个Windows GUI程序都始于WinMain函数它是程序的入口点相当于控制台程序的main。#include windows.h // 声明窗口过程函数 LRESULT CALLBACK WindowProc(HWND hwnd, UINT uMsg, WPARAM wParam, LPARAM lParam); int WINAPI WinMain(HINSTANCE hInstance, HINSTANCE hPrevInstance, LPSTR lpCmdLine, int nCmdShow) { // 1. 设计并注册窗口类 const char CLASS_NAME[] CheckerWindowClass; WNDCLASS wc {0}; wc.lpfnWndProc WindowProc; // 指定处理消息的函数 wc.hInstance hInstance; wc.lpszClassName CLASS_NAME; wc.hCursor LoadCursor(NULL, IDC_ARROW); // 加载箭头光标 wc.hbrBackground (HBRUSH)(COLOR_WINDOW1); // 窗口背景色 RegisterClass(wc); // 2. 创建窗口 HWND hwnd CreateWindowEx( 0, // 扩展样式 CLASS_NAME, // 窗口类名 经典跳棋, // 窗口标题 WS_OVERLAPPEDWINDOW ~WS_THICKFRAME ~WS_MAXIMIZEBOX, // 样式普通窗口不可调整大小和最大化 CW_USEDEFAULT, CW_USEDEFAULT, 800, 600, // 位置和大小 NULL, // 父窗口 NULL, // 菜单 hInstance, // 实例句柄 NULL // 附加数据 ); if (hwnd NULL) { return 0; } // 3. 显示窗口 ShowWindow(hwnd, nCmdShow); UpdateWindow(hwnd); // 立即发送WM_PAINT消息 // 4. 消息循环程序的心脏 MSG msg {0}; while (GetMessage(msg, NULL, 0, 0) 0) { TranslateMessage(msg); // 转换键盘消息 DispatchMessage(msg); // 将消息分发给窗口过程 } return 0; }WindowProc函数是窗口的“事件处理器”。操作系统会将所有发生在这个窗口上的事件鼠标移动、点击、键盘按下、窗口需要重绘等包装成消息Message发送给它。LRESULT CALLBACK WindowProc(HWND hwnd, UINT uMsg, WPARAM wParam, LPARAM lParam) { switch (uMsg) { case WM_DESTROY: PostQuitMessage(0); // 发送退出消息结束消息循环 return 0; case WM_PAINT: { // 处理窗口绘制 PAINTSTRUCT ps; HDC hdc BeginPaint(hwnd, ps); // 在这里调用你的绘图函数 DrawGame(hdc); EndPaint(hwnd, ps); return 0; } case WM_LBUTTONDOWN: { // 处理鼠标左键按下 int x GET_X_LPARAM(lParam); int y GET_Y_LPARAM(lParam); HandleMouseClick(x, y); InvalidateRect(hwnd, NULL, TRUE); // 标记窗口区域无效触发重绘 return 0; } // ... 处理其他消息 } // 默认处理其他消息 return DefWindowProc(hwnd, uMsg, wParam, lParam); }理解WinMain和WindowProc的协作是Windows GUI编程的第一课。消息循环GetMessage-DispatchMessage是程序保持响应的关键。4. 图形绘制模块用GDI绘制棋盘与棋子Windows的图形设备接口GDI是一组用于绘制图形、文本的API。虽然性能上不如DirectX或OpenGL但对于棋盘游戏这种2D、非实时动画的场景完全够用且简单易学。4.1 初始化棋盘坐标在窗口创建后例如响应WM_CREATE消息我们需要计算32个棋位在客户区Client Area中的坐标。为了适配不同窗口大小坐标应基于客户区尺寸动态计算。void InitializeBoard(HWND hwnd, GameState *state) { RECT clientRect; GetClientRect(hwnd, clientRect); // 获取窗口客户区大小 int width clientRect.right - clientRect.left; int height clientRect.bottom - clientRect.top; int boardSize min(width, height) * 0.8; // 棋盘占据80%的短边 int cellSize boardSize / 8; // 每个格子的大小 int boardLeft (width - boardSize) / 2; int boardTop (height - boardSize) / 2; int index 0; for (int row 0; row 8; row) { for (int col 0; col 8; col) { // 跳棋棋盘只有 (rowcol) 为奇数的深色格是有效棋位 if ((row col) % 2 1) { state-cells[index].x boardLeft col * cellSize cellSize / 2; state-cells[index].y boardTop row * cellSize cellSize / 2; state-cells[index].index index; // 初始化棋子前三行黑棋后三行红棋 if (row 3) { state-piece[index] 2; // 黑兵 } else if (row 4) { state-piece[index] 1; // 红兵 } else { state-piece[index] 0; // 空位 } index; } } } state-currentPlayer 1; // 红方先行 state-selectedIndex -1; }4.2 绘制函数实现绘制在WM_PAINT消息处理中触发。我们需要绘制棋盘网格、深色格、棋子以及高亮显示可选移动。void DrawGame(HDC hdc) { // 1. 绘制棋盘背景和网格 HBRUSH hBrushLight CreateSolidBrush(RGB(240, 217, 181)); // 浅色格 HBRUSH hBrushDark CreateSolidBrush(RGB(181, 136, 99)); // 深色格 HPEN hGridPen CreatePen(PS_SOLID, 2, RGB(0, 0, 0)); // 网格线 SelectObject(hdc, hGridPen); for (int row 0; row 8; row) { for (int col 0; col 8; col) { RECT cellRect { boardLeft col * cellSize, boardTop row * cellSize, boardLeft (col 1) * cellSize, boardTop (row 1) * cellSize }; // 交替填充颜色 if ((row col) % 2 0) { FillRect(hdc, cellRect, hBrushLight); } else { FillRect(hdc, cellRect, hBrushDark); } // 绘制格子边框 Rectangle(hdc, cellRect.left, cellRect.top, cellRect.right, cellRect.bottom); } } // 2. 绘制棋子 for (int i 0; i 32; i) { if (state.piece[i] 0) continue; // 空位跳过 int x state.cells[i].x; int y state.cells[i].y; int radius cellSize / 2 - 5; // 棋子半径略小于格子 // 根据棋子状态选择颜色和画刷 HBRUSH hPieceBrush; if (state.piece[i] 1 || state.piece[i] 3) { hPieceBrush CreateSolidBrush(RGB(220, 60, 60)); // 红色 } else { hPieceBrush CreateSolidBrush(RGB(40, 40, 40)); // 黑色 } SelectObject(hdc, hPieceBrush); // 画圆形棋子 Ellipse(hdc, x - radius, y - radius, x radius, y radius); // 如果是“王棋”在中间画一个金色小圆或“K”字标识 if (state.piece[i] 3 || state.piece[i] 4) { HBRUSH hKingBrush CreateSolidBrush(RGB(255, 215, 0)); SelectObject(hdc, hKingBrush); int kingRadius radius / 2; Ellipse(hdc, x - kingRadius, y - kingRadius, x kingRadius, y kingRadius); DeleteObject(hKingBrush); } DeleteObject(hPieceBrush); } // 3. 绘制选中状态和高亮可移动位置 if (state.selectedIndex ! -1) { // 高亮选中的棋子画一个稍大的同心圆边框 HPEN hSelectPen CreatePen(PS_SOLID, 3, RGB(0, 120, 215)); // 蓝色边框 SelectObject(hdc, hSelectPen); HBRUSH hNullBrush (HBRUSH)GetStockObject(NULL_BRUSH); // 空心画刷 SelectObject(hdc, hNullBrush); int selectRadius cellSize / 2 - 2; int x state.cells[state.selectedIndex].x; int y state.cells[state.selectedIndex].y; Ellipse(hdc, x - selectRadius, y - selectRadius, x selectRadius, y selectRadius); // 高亮可移动的位置画一个绿色小圆点或方框 HBRUSH hMoveBrush CreateSolidBrush(RGB(50, 205, 50)); for (int i 0; i state.moveCount; i) { int moveX state.possibleMoves[i].x; int moveY state.possibleMoves[i].y; int spotSize 6; Ellipse(hdc, moveX - spotSize, moveY - spotSize, moveX spotSize, moveY spotSize); } DeleteObject(hMoveBrush); DeleteObject(hSelectPen); } // 4. 清理GDI对象非常重要防止资源泄漏 DeleteObject(hBrushLight); DeleteObject(hBrushDark); DeleteObject(hGridPen); }重要心得GDI编程必须牢记“创建什么就要删除什么”。CreateSolidBrush,CreatePen等函数创建的对象都是系统资源使用完后必须用DeleteObject释放。否则会造成GDI资源泄漏长时间运行可能导致程序或系统图形界面不稳定。这是一个非常经典的坑。5. 游戏逻辑模块规则与状态管理跳棋的规则是项目的核心算法部分。主要包括棋子移动合法性判断、吃子逻辑、连续吃子、升变为王棋以及胜负判定。5.1 移动与吃子规则实现我们需要为当前行棋方找出所有可能的走法。这分为两个步骤首先检查是否有强制吃子如果有则只能走吃子的棋如果没有才可以走普通移动。// 查找一个棋子的所有合法走法包括普通移动和吃子 void FindValidMoves(GameState *state, int pieceIndex) { ClearMoves(state); // 清空之前的可走位置 int player state-currentPlayer; int pieceType state-piece[pieceIndex]; if (pieceType 0 || (pieceType % 2) ! (player % 2)) { return; // 空位或不是当前玩家的棋子 } int row pieceIndex / 4; // 逻辑行0-7 int col (pieceIndex % 4) * 2 (row % 2); // 逻辑列0-7 // 首先检查是否存在任何强制吃子机会全局检查 bool mustJump CheckForcedJump(state, player); // 根据棋子类型和方向生成可能的移动目标 int directions[4][2]; // 存储方向向量 int dirCount 0; // 红兵1只能向上移动黑兵2只能向下移动王棋3,4可上下移动 if (pieceType 1 || pieceType 3) { // 红方或红王 directions[dirCount][0] -1; directions[dirCount][1] -1; // 左上 directions[dirCount][0] -1; directions[dirCount][1] 1; // 右上 } if (pieceType 2 || pieceType 3) { // 黑方或黑王 directions[dirCount][0] 1; directions[dirCount][1] -1; // 左下 directions[dirCount][0] 1; directions[dirCount][1] 1; // 右下 } for (int d 0; d dirCount; d) { int newRow row directions[d][0]; int newCol col directions[d][1]; int targetIndex GetBoardIndex(newRow, newCol); if (targetIndex ! -1 state-piece[targetIndex] 0) { // 普通移动目标为空位 if (!mustJump) { AddMove(state, targetIndex, -1); // -1表示没有吃子 } } else if (targetIndex ! -1 IsOpponent(state-piece[targetIndex], player)) { // 尝试吃子前方是对手棋子检查再前方是否为空 int jumpRow newRow directions[d][0]; int jumpCol newCol directions[d][1]; int jumpIndex GetBoardIndex(jumpRow, jumpCol); if (jumpIndex ! -1 state-piece[jumpIndex] 0) { // 可以吃子 AddMove(state, jumpIndex, targetIndex); // 记录目标位置和被吃的棋子索引 } } } } // 根据逻辑行列获取棋盘数组索引 int GetBoardIndex(int row, int col) { if (row 0 || row 8 || col 0 || col 8) return -1; if ((row col) % 2 0) return -1; // 不在深色格上 return row * 4 col / 2; // 巧妙的一维映射 }CheckForcedJump函数需要遍历当前玩家的所有棋子检查是否存在至少一个吃子走法。如果存在则mustJump为真在FindValidMoves中就只能添加吃子走法。5.2 执行移动与状态更新当玩家点击一个高亮的可移动位置时需要执行移动操作。void MakeMove(GameState *state, int fromIndex, int toIndex, int capturedIndex) { // 1. 移动棋子 state-piece[toIndex] state-piece[fromIndex]; state-piece[fromIndex] 0; // 2. 如果发生吃子移除被吃的棋子 if (capturedIndex ! -1) { state-piece[capturedIndex] 0; } // 3. 检查升变棋子到达对方底线则升变为“王” int row toIndex / 4; if ((state-piece[toIndex] 1 row 0) || // 红兵到达顶行0行 (state-piece[toIndex] 2 row 7)) { // 黑兵到达底行7行 state-piece[toIndex] 2; // 1-3 (红王), 2-4 (黑王) } // 4. 处理连续吃子 bool canJumpAgain false; if (capturedIndex ! -1) { // 检查从新的位置toIndex是否还能继续吃子 ClearMoves(state); FindValidMoves(state, toIndex); // 只查找吃子走法 for (int i 0; i state-moveCount; i) { if (IsJumpMove(state, fromIndex, state-possibleMoves[i])) { // 判断是否为吃子移动 canJumpAgain true; break; } } } // 5. 更新游戏状态 if (!canJumpAgain) { // 不能连续吃子切换玩家 state-currentPlayer (state-currentPlayer 1) ? 2 : 1; state-selectedIndex -1; ClearMoves(state); // 切换玩家后需要为新玩家计算所有合法走法判断是否无棋可走判负 if (!HasAnyValidMove(state)) { // 游戏结束当前玩家无棋可走对方获胜 GameOver(state-currentPlayer 1 ? 2 : 1); } } else { // 可以连续吃子保持当前玩家并选中刚刚移动的棋子 state-selectedIndex toIndex; // 注意此时FindValidMoves已经在上面的步骤中调用过possibleMoves里已经是新的吃子走法 } }连续吃子的逻辑是跳棋的难点。实现的关键在于在一次吃子移动后不立即切换玩家而是检查被移动的棋子从新位置出发是否还有吃子机会。如果有则保持当前玩家和该棋子的选中状态等待下一次点击只能点击吃子位置。6. 用户交互与消息处理游戏的所有交互都通过WindowProc函数中的鼠标消息来处理。6.1 鼠标点击处理流程void HandleMouseClick(int x, int y) { // 1. 将屏幕坐标转换为棋盘索引 int clickedIndex GetCellIndexAtPoint(x, y); if (clickedIndex -1) return; // 点击在棋盘外 // 2. 情况A当前没有选中任何棋子 if (state.selectedIndex -1) { // 点击的是否是当前玩家的棋子 if (state.piece[clickedIndex] ! 0 (state.piece[clickedIndex] % 2) (state.currentPlayer % 2)) { state.selectedIndex clickedIndex; FindValidMoves(state, clickedIndex); } } // 3. 情况B当前已有选中棋子 else { // 检查点击位置是否在可移动列表中 for (int i 0; i state.moveCount; i) { int moveIndex GetCellIndexAtPoint(state.possibleMoves[i].x, state.possibleMoves[i].y); if (moveIndex clickedIndex) { // 执行移动需要知道这个移动是否是吃子以及吃了哪个棋子 int capturedIndex -1; // 我们需要在FindValidMoves时记录吃子信息这里简化处理通过计算判断 if (abs(clickedIndex - state.selectedIndex) 4) { // 简单判断移动距离大于1格即为吃子 capturedIndex (state.selectedIndex clickedIndex) / 2; // 被吃棋子的索引近似 } MakeMove(state, state.selectedIndex, clickedIndex, capturedIndex); break; } } // 如果点击的不是可移动位置则视为重新选择或取消选择 // 如果点击的是另一个己方棋子则切换选中 if (state.piece[clickedIndex] ! 0 (state.piece[clickedIndex] % 2) (state.currentPlayer % 2)) { state.selectedIndex clickedIndex; FindValidMoves(state, clickedIndex); } else { // 点击空白处或其他不清除选中保持连续吃子状态 // 只有在不能连续吃子时MakeMove内部会清除选中状态 } } }GetCellIndexAtPoint函数通过计算鼠标点击坐标与每个棋位中心点的距离找到最近的棋位索引。6.2 实现窗口重绘与简单动画每次游戏状态改变如移动棋子、选中棋子后都需要刷新窗口以显示最新状态。我们通过InvalidateRect函数通知系统窗口需要重绘。// 在HandleMouseClick或MakeMove等改变状态的函数末尾 InvalidateRect(hwnd, NULL, TRUE); // TRUE表示擦除背景TRUE参数会先发送WM_ERASEBKGND消息擦除背景再发送WM_PAINT。对于简单的棋盘游戏这足够了。如果你想实现更平滑的动画如棋子移动过程则需要更复杂的方法比如使用定时器WM_TIMER在多个时间片内逐步改变棋子的绘制坐标并在每个时间片触发重绘。这涉及到双缓冲绘图技术来防止闪烁是进阶的内容。7. 项目优化、调试与扩展思考7.1 常见问题与调试技巧程序编译通过但运行瞬间闪退检查点最常见的原因是WinMain或WindowProc函数签名错误。确保WinMain是int WINAPI WinMain(...)WindowProc是LRESULT CALLBACK WindowProc(...)。调试方法在WinMain开头用MessageBox弹个窗看是否执行。在WindowProc的开头加一个MessageBox或OutputDebugString看消息是否进来。VSCode配合GDB可以设置断点进行单步调试。窗口不显示或显示异常检查点CreateWindowEx的参数是否正确特别是窗口类名是否与RegisterClass注册的一致。ShowWindow的参数nCmdShow是否正确传递。检查点消息循环GetMessage是否正常启动。如果GetMessage获取到WM_QUIT消息GetMessage返回0循环会结束。点击无反应或逻辑错乱检查点鼠标坐标转换是否正确。lParam的低16位是x坐标高16位是y坐标使用GET_X_LPARAM和GET_Y_LPARAM宏安全提取。检查点GameState全局变量或静态变量是否正确定义和初始化。确保在WM_LBUTTONDOWN中访问的是有效的状态。终极调试法在HandleMouseClick函数里把点击的坐标、转换后的索引、当前游戏状态用sprintf格式化到一个字符串然后用MessageBox或OutputDebugString输出一目了然。内存/GDI资源泄漏检查点所有通过CreatePen,CreateSolidBrush,CreateFont等创建的GDI对象是否在WM_PAINT消息处理结束后都用DeleteObject删除了。检查点通过GetDC获取的设备上下文DC是否用ReleaseDC释放。在WM_PAINT中应使用BeginPaint和EndPaint配对它们会自动管理。7.2 项目优化与扩展方向这个基础版本完成后你可以从多个方向进行深化AI对手实现一个简单的电脑AI。可以从规则型AI开始比如评估所有走法选择吃子数最多、或最靠近对方底线的走法。进阶可以尝试实现 Minimax 算法配合 Alpha-Beta 剪枝甚至机器学习。网络对战使用Windows SocketWinsockAPI实现双人网络对战。这涉及到网络编程、协议设计如序列化游戏状态、以及处理网络延迟。图形与音效升级双缓冲绘图在内存中创建一个兼容DCCreateCompatibleDC和一个位图CreateCompatibleBitmap将所有绘制操作先在内存位图上完成最后一次性BitBlt到屏幕DC。这能彻底解决画面闪烁问题。加载图片资源使用LoadImage加载BMP等格式的图片作为棋盘和棋子纹理代替纯色绘制。播放音效使用PlaySoundAPI播放WAV格式的移动、吃子、胜利音效。游戏功能完善悔棋功能维护一个游戏状态栈GameState数组每次移动前将当前状态压栈悔棋时弹出。游戏存档/读档将GameState结构体序列化到文件。更精美的UI自定义绘制标题栏、按钮实现开始菜单、胜负提示框等。7.3 从Win32到现代C的思考这个项目纯粹用C和Win32 API完成它像一把解剖刀让你看清了Windows GUI应用的肌肉和骨骼。当你掌握了这些再去学习像Qt、wxWidgets这样的C GUI框架或者.NET WinForms/WPF你会理解它们底层在帮你做什么封装了什么遇到问题时也更容易定位。例如Qt的信号槽机制其底层思想与Windows消息循环是相通的只不过更加类型安全和易用。对于想深入C游戏开发的读者这个项目也是一个很好的垫脚石。理解了消息驱动和GDI绘图后你可以更平滑地过渡到DirectX或OpenGL的学习那时你关注的重点将从“如何创建一个窗口并响应事件”转移到“如何高效地渲染3D图形”上。这个跳棋项目代码量不大但涉及了Windows桌面程序开发的完整链条。它没有炫酷的界面但每一个像素、每一次交互都完全在你的掌控之下。这种从底层构建的成就感是使用高级框架无法比拟的。希望这个详细的拆解能为你打开一扇窗让你看到Windows编程最原本的样子。