简介:本资源是一套基于Visual Studio开发的五子棋桌面游戏完整工程,面向C++/C#初学者与图形界面编程学习者,聚焦人机对战与人人对战双模式实现,覆盖GUI设计、事件响应、AI算法(含基础穷举与胜负判定逻辑)及音画资源集成等核心实践环节。压缩包共49个文件,约107.07MB,包含5个cpp源文件(如main.cpp、ai.cpp、game.cpp)、1个头文件renju.h、12个编译中间文件(tlog/pdb/obj等)、11张界面与棋盘图片(jpg/png)、5段音效(wav)、以及sln/vcxproj/filters等VS项目配置文件,结构完整,可直接加载编译运行。目前已有2659人学习下载,提供开箱即用的可执行逻辑、带注释的核心代码模块、多场景UI素材与音效资源,便于理解棋盘状态管理、五连珠检测算法、EasyX图形绘制集成及Windows Forms交互机制,是巩固C++/C#基础与提升游戏开发实战能力的优质参考项目。
1. 五子棋不是练手玩具,而是验证 C++ 工程能力的黑匣子:VS 环境下从 Win32 窗口创建、GDI 绘图、鼠标事件调度到 Minimax + Alpha-Beta 剪枝人机决策链路全部跑通,新手照着改坐标就能下出合法棋,老手能直接切入 AI 深度调优模块——这不是教学 Demo,是能当本地可执行程序发给朋友双击运行的完整工程。
你可能试过网上那些“50 行 Python 五子棋”,点开就报pygame not found;也可能在 VS 里新建了个空项目,卡在“怎么让窗口画出一个格子”超过两小时。这个 Visual Studio 实现的五子棋项目,用纯 Win32 API + GDI(不依赖 MFC、Qt 或任何第三方 GUI 库),所有绘图逻辑写在PaintBoard()里,所有落子响应收在WM_LBUTTONDOWN消息分支中,AI 决策封装在独立的AIPlayer.cpp文件里,支持难度分级(初级:随机落子;中级:四连检测;高级:带深度限制的 Alpha-Beta 剪枝搜索)。它不教你怎么拖控件,而是让你看清——当鼠标左键按下时,坐标如何被转换成棋盘索引;当InvalidateRect()被调用后,GDI 是怎样一帧一帧重绘整个棋面;当EvaluateBoard()返回 -9999 时,意味着当前局面已判定为玩家胜利。项目结构干净得像刚洗过的电路板:.sln+.vcxproj+ 4 个核心.cpp(main.cpp,Board.cpp,AIPlayer.cpp,GameLogic.cpp)+ 2 个头文件(Board.h,AIPlayer.h),没有隐藏的资源脚本、没有混淆的宏定义、没有需要额外安装的 SDK。如果你正卡在“VS 里怎么响应鼠标点击”或“为什么 SetPixel 总是画不出圆点”,那这份源码就是你的后悔药。
2. 工程结构与 Win32 消息循环:从空窗口到可交互棋盘的完整初始化链路
2.1 项目创建与基础配置:避开 Unicode/ANSI 编码玄学陷阱
新建项目时必须选择“Win32 项目” → “Windows 桌面应用程序” → 取消勾选“预编译头”和“安全开发生命周期 (SDL) 检查”。这是关键一步——很多初学者在CreateWindowW失败后疯狂查 MSDN,最后发现只是项目属性里字符集设成了 Unicode,而代码里混用了TEXT("Chess")和L"Chess"导致窗口类注册失败。正确做法是在项目属性 → 常规 → 字符集 → 设为“使用多字节字符集”(即 ANSI 模式),这样所有字符串字面量可直接用双引号,无需TEXT()宏包裹。
提示:若坚持用 Unicode(推荐长期项目),则所有
CreateWindow必须改为CreateWindowW,RegisterClass改为RegisterClassW,且WNDCLASSEX中lpszClassName必须是宽字符指针(如L"GoBangWnd")。本项目采用 ANSI 模式,降低入门门槛。
核心入口函数WinMain结构如下:
int APIENTRY WinMain(HINSTANCE hInstance, HINSTANCE hPrevInstance, LPSTR lpCmdLine, int nCmdShow) { // 1. 注册窗口类 WNDCLASSEX wc = { sizeof(WNDCLASSEX), CS_HREDRAW | CS_VREDRAW, WndProc, 0, 0, hInstance, NULL, NULL, (HBRUSH)(COLOR_WINDOW+1), NULL, "GoBangClass", NULL }; RegisterClassEx(&wc); // 2. 创建主窗口(800x600,居中) HWND hWnd = CreateWindow("GoBangClass", "五子棋 - VS 实现版", WS_OVERLAPPEDWINDOW ^ WS_THICKFRAME ^ WS_MAXIMIZEBOX, CW_USEDEFAULT, CW_USEDEFAULT, 800, 600, NULL, NULL, hInstance, NULL); if (!hWnd) return FALSE; ShowWindow(hWnd, nCmdShow); UpdateWindow(hWnd); // 3. 消息循环 MSG msg = {}; while (GetMessage(&msg, NULL, 0, 0)) { TranslateMessage(&msg); DispatchMessage(&msg); } return (int)msg.wParam; }这段代码里藏着三个血泪经验:
WS_OVERLAPPEDWINDOW ^ WS_THICKFRAME ^ WS_MAXIMIZEBOX是刻意去掉缩放边框和最大化按钮——五子棋界面尺寸固定(棋盘需严格 15×15 格),强行缩放会导致 GDI 绘图错位;CW_USEDEFAULT用于初始位置,但窗口大小(800×600)必须显式指定,否则GetClientRect获取的客户区尺寸不可控;GetMessage循环中不能加Sleep(1),否则鼠标响应延迟明显,尤其在快速连点时会丢事件——Win32 消息队列本身已做缓冲,加 Sleep 反而破坏实时性。
2.2 棋盘绘制:GDI 坐标系与逻辑坐标的精准对齐
棋盘绘制不在WinMain中,而在WndProc的WM_PAINT分支里完成。关键不是“怎么画线”,而是“线画在哪”。本项目采用客户区像素映射法:先用GetClientRect获取客户区大小,再将棋盘划分为 15×15 等距网格,每格宽度cellSize = rect.right / 15(高度同理),确保无论窗口是否缩放(虽然我们禁了缩放),棋盘始终铺满客户区。
case WM_PAINT: { PAINTSTRUCT ps; HDC hdc = BeginPaint(hWnd, &ps); // 获取客户区尺寸 RECT rect; GetClientRect(hWnd, &rect); int width = rect.right - rect.left; int height = rect.bottom - rect.top; const int BOARD_SIZE = 15; int cellSize = min(width, height) / BOARD_SIZE; // 取宽高较小值保证正方形格子 // 绘制背景(浅米色) HBRUSH hBrush = CreateSolidBrush(RGB(245, 245, 220)); FillRect(hdc, &rect, hBrush); DeleteObject(hBrush); // 绘制横线与竖线(黑色,1像素宽) HPEN hPen = CreatePen(PS_SOLID, 1, RGB(0, 0, 0)); SelectObject(hdc, hPen); for (int i = 0; i <= BOARD_SIZE; ++i) { // 横线:y = i * cellSize MoveToEx(hdc, 0, i * cellSize, NULL); LineTo(hdc, width, i * cellSize); // 竖线:x = i * cellSize MoveToEx(hdc, i * cellSize, 0, NULL); LineTo(hdc, i * cellSize, height); } // 绘制星位(天元与四角小星) int starRadius = cellSize / 6; POINT stars[5] = {{7,7}, {3,3}, {3,11}, {11,3}, {11,11}}; // 逻辑坐标(0~14) for (int i = 0; i < 5; ++i) { int x = stars[i].x * cellSize + cellSize/2; int y = stars[i].y * cellSize + cellSize/2; Ellipse(hdc, x - starRadius, y - starRadius, x + starRadius, y + starRadius); } DeleteObject(hPen); EndPaint(hWnd, &ps); break; }注意这里的关键参数:
cellSize = min(width, height) / BOARD_SIZE—— 避免因窗口宽高比异常导致格子被拉伸;- 星位坐标
stars[]存的是逻辑坐标(0~14),绘制前必须转为像素坐标:x = logic_x * cellSize + cellSize/2,+ cellSize/2是为了把星点画在格子中心而非左上角; Ellipse绘制实心圆需用FillRect+Ellipse组合,但本项目用Ellipse加粗笔(PS_COSMETIC)配合小半径实现视觉实心效果,更轻量。
2.3 鼠标事件绑定:从 WM_LBUTTONDOWN 到落子坐标的无损转换
落子逻辑藏在WM_LBUTTONDOWN消息处理中。难点在于:鼠标坐标是屏幕坐标,而棋盘是客户区坐标,且需映射到 15×15 的整数索引。本项目用ScreenToClient+ 整除取整实现零误差转换:
case WM_LBUTTONDOWN: { POINT pt; pt.x = LOWORD(lParam); // 鼠标x坐标(客户区) pt.y = HIWORD(lParam); // 鼠标y坐标(客户区) // 获取客户区尺寸并计算格子大小 RECT rect; GetClientRect(hWnd, &rect); int width = rect.right - rect.left; int height = rect.bottom - rect.top; const int BOARD_SIZE = 15; int cellSize = min(width, height) / BOARD_SIZE; // 将像素坐标转为逻辑坐标(0~14) int logicX = pt.x / cellSize; int logicY = pt.y / cellSize; // 边界检查:防止点击窗口边缘空白处越界 if (logicX >= 0 && logicX < BOARD_SIZE && logicY >= 0 && logicY < BOARD_SIZE) { // 调用游戏逻辑:尝试在 (logicX, logicY) 落子 if (GameLogic::MakeMove(logicX, logicY, PLAYER_BLACK)) { // 落子成功,触发重绘 InvalidateRect(hWnd, NULL, TRUE); } } break; }这里有两个易错点必须强调:
LOWORD(lParam)和HIWORD(lParam)直接获取客户区坐标,不要用GetCursorPos+ScreenToClient,后者在多显示器或 DPI 缩放下易出错;logicX = pt.x / cellSize是整数除法,天然向下取整,恰好对应左上角起始的格子编号(例如 x=0~49 映射到 logicX=0),无需floor()或round();InvalidateRect(hWnd, NULL, TRUE)的第三个参数TRUE表示擦除背景,避免重绘时残留旧棋子——这是 GDI 绘图不闪烁的关键。
3. 游戏逻辑与状态管理:Board 类设计与胜负判定的边界条件全覆盖
3.1 Board 类封装:二维数组 + 状态机驱动的棋局核心
Board.h定义了棋盘状态的核心数据结构:
// Board.h #pragma once #include <vector> enum PieceType { EMPTY = 0, PLAYER_BLACK = 1, PLAYER_WHITE = 2 }; class Board { private: std::vector<std::vector<PieceType>> board; // 15x15 二维向量 int size; // 当前棋盘大小(固定15) public: Board(int s = 15) : size(s), board(s, std::vector<PieceType>(s, EMPTY)) {} // 获取指定位置棋子类型 PieceType GetPiece(int x, int y) const { if (x < 0 || x >= size || y < 0 || y >= size) return EMPTY; return board[y][x]; // 注意:y为行,x为列,符合矩阵习惯 } // 设置棋子(仅当位置为空时) bool SetPiece(int x, int y, PieceType type) { if (x < 0 || x >= size || y < 0 || y >= size || board[y][x] != EMPTY) return false; board[y][x] = type; return true; } // 清空棋盘 void Clear() { for (int i = 0; i < size; ++i) for (int j = 0; j < size; ++j) board[i][j] = EMPTY; } // 获取棋盘大小 int GetSize() const { return size; } };关键设计点:
- 使用
std::vector<std::vector<>>而非裸指针,避免内存泄漏,且board[y][x]符合数学矩阵习惯(行优先),与 GDI 绘图的y坐标方向一致; GetPiece带越界保护,返回EMPTY,避免外部调用时手动检查边界;SetPiece返回bool,强制调用方处理“落子失败”场景(如位置已被占),这是状态机健壮性的基础。
3.2 胜负判定算法:八方向扫描与连续计数的防漏逻辑
胜负判定在GameLogic.cpp中实现,核心函数CheckWin(int x, int y, PieceType type)接收最新落子坐标与棋子类型,只扫描该点所在八个方向(横、竖、两斜)是否形成五连,而非遍历全盘——效率从 O(n²) 降到 O(1)。
// GameLogic.cpp #include "Board.h" #include <algorithm> bool GameLogic::CheckWin(const Board& board, int x, int y, PieceType type) { const int directions[8][2] = { {0,1}, {1,0}, {1,1}, {1,-1}, // 下、右、右下、右上 {0,-1},{-1,0},{-1,-1},{-1,1} // 上、左、左上、左下 }; for (int d = 0; d < 8; ++d) { int count = 1; // 当前点自身 int dx = directions[d][0], dy = directions[d][1]; // 正向延伸(同一方向连续相同棋子) for (int i = 1; i < 5; ++i) { int nx = x + i * dx, ny = y + i * dy; if (board.GetPiece(nx, ny) == type) count++; else break; } // 反向延伸(相反方向连续相同棋子) for (int i = 1; i < 5; ++i) { int nx = x - i * dx, ny = y - i * dy; if (board.GetPiece(nx, ny) == type) count++; else break; } if (count >= 5) return true; } return false; }这个算法有三个反直觉细节:
count初始化为 1(当前落子点),正反向扫描各最多累加 4 个,总和达 5 即胜,避免重复计数;for (int i = 1; i < 5; ++i)中上限是< 5而非<= 4,语义更清晰;- 方向数组
directions[8][2]用整数而非浮点,杜绝 GDI 坐标系与数学坐标系混淆——Win32 的 y 轴向下为正,与矩阵索引完全一致,无需翻转。
3.3 游戏状态机:从开局到终局的七种状态流转
GameLogic.h定义了完整状态枚举,驱动 UI 文本与按钮行为:
// GameLogic.h enum GameState { GAME_START, // 初始状态,显示“黑方先行” GAME_PLAYING, // 对局中 GAME_PLAYER_WIN, // 玩家获胜 GAME_AI_WIN, // AI获胜 GAME_DRAW, // 平局(棋盘满) GAME_UNDO, // 悔棋中(暂未实现,预留) GAME_RESTART // 重启中(暂未实现,预留) };状态切换由MakeMove函数控制:
// GameLogic.cpp GameState GameLogic::MakeMove(int x, int y, PieceType type) { static Board board; static GameState state = GAME_START; if (state == GAME_START || state == GAME_PLAYING) { if (board.SetPiece(x, y, type)) { // 检查是否获胜 if (CheckWin(board, x, y, type)) { state = (type == PLAYER_BLACK) ? GAME_PLAYER_WIN : GAME_AI_WIN; return state; } // 检查是否平局(棋盘满) bool full = true; for (int i = 0; i < board.GetSize(); ++i) { for (int j = 0; j < board.GetSize(); ++j) { if (board.GetPiece(i, j) == EMPTY) { full = false; break; } } if (!full) break; } if (full) { state = GAME_DRAW; return state; } // 切换回合 state = GAME_PLAYING; return state; } } return state; // 落子失败,状态不变 }注意:
static Board board是简化设计,实际工程中应通过单例或依赖注入传递 Board 实例,避免全局状态。此处为降低理解成本保留 static,但二次开发时建议重构。
4. 人机对战核心:Minimax + Alpha-Beta 剪枝的 C++ 实现与性能调优
4.1 AIPlayer 类架构:三层解耦设计(接口/策略/评估)
AIPlayer.h定义了清晰的抽象层:
// AIPlayer.h #pragma once #include "Board.h" class AIPlayer { public: enum Difficulty { EASY, MEDIUM, HARD }; AIPlayer(Difficulty diff = HARD) : difficulty(diff) {} // 主决策函数:返回最佳落子坐标 std::pair<int, int> GetBestMove(const Board& board, PieceType aiType); private: Difficulty difficulty; // 核心搜索函数(Minimax + Alpha-Beta) int MiniMax(const Board& board, int depth, bool isMaximizing, int alpha, int beta, PieceType aiType, PieceType playerType); // 评估函数:静态局面打分 int EvaluateBoard(const Board& board, PieceType aiType, PieceType playerType); // 难度适配:控制搜索深度 int GetSearchDepth() const; };这种设计让 AI 逻辑与游戏主循环完全解耦:GameLogic::MakeMove调用AIPlayer::GetBestMove获取坐标,再调用Board::SetPiece执行,不侵入任何 UI 或消息循环代码。新手可先注释掉 AI 模块,用rand()实现随机落子验证流程;老手可直接替换EvaluateBoard函数升级评估模型。
4.2 Minimax 搜索实现:递归深度控制与剪枝生效点验证
MiniMax函数是 AI 的心脏,本项目实现带深度限制与 Alpha-Beta 剪枝:
// AIPlayer.cpp #include <climits> #include <random> int AIPlayer::MiniMax(const Board& board, int depth, bool isMaximizing, int alpha, int beta, PieceType aiType, PieceType playerType) { // 终止条件:达到最大深度或游戏结束 if (depth == 0) { return EvaluateBoard(board, aiType, playerType); } // 检查是否已分出胜负 for (int i = 0; i < board.GetSize(); ++i) { for (int j = 0; j < board.GetSize(); ++j) { if (board.GetPiece(i, j) != EMPTY) { if (GameLogic::CheckWin(board, i, j, aiType)) { return 10000 - depth; // AI赢,越快越好 } if (GameLogic::CheckWin(board, i, j, playerType)) { return -10000 + depth; // 玩家赢,越慢越好 } } } } if (isMaximizing) { int maxEval = INT_MIN; for (int i = 0; i < board.GetSize(); ++i) { for (int j = 0; j < board.GetSize(); ++j) { if (board.GetPiece(i, j) == EMPTY) { Board newBoard = board; // 深拷贝(vector自动处理) newBoard.SetPiece(i, j, aiType); int eval = MiniMax(newBoard, depth - 1, false, alpha, beta, aiType, playerType); maxEval = std::max(maxEval, eval); alpha = std::max(alpha, eval); if (beta <= alpha) break; // Alpha-Beta 剪枝:父节点已知不会选此分支 } } if (beta <= alpha) break; // 外层剪枝优化 } return maxEval; } else { int minEval = INT_MAX; for (int i = 0; i < board.GetSize(); ++i) { for (int j = 0; j < board.GetSize(); ++j) { if (board.GetPiece(i, j) == EMPTY) { Board newBoard = board; newBoard.SetPiece(i, j, playerType); int eval = MiniMax(newBoard, depth - 1, true, alpha, beta, aiType, playerType); minEval = std::min(minEval, eval); beta = std::min(beta, eval); if (beta <= alpha) break; } } if (beta <= alpha) break; } return minEval; } }关键参数说明:
depth:搜索深度,HARD模式设为 3,MEDIUM为 2,EASY为 1(直接调用EvaluateBoard);alpha/beta:初始传入INT_MIN/INT_MAX,剪枝条件if (beta <= alpha)是标准写法,不是beta < alpha,等号成立时同样剪枝;newBoard = board触发 vector 深拷贝,安全但有性能开销——实际工程中可用std::shared_ptr<Board>优化,但本项目为清晰性保留值语义;10000 - depth的奖励值设计,让 AI 优先选择更快获胜的路径,避免“长胜”陷阱。
4.3 局面评估函数:从“有四连”到“活三威胁”的渐进式打分体系
EvaluateBoard是 AI 智能度的天花板,本项目实现三级评估:
int AIPlayer::EvaluateBoard(const Board& board, PieceType aiType, PieceType playerType) { int score = 0; const int SIZE = board.GetSize(); // 遍历所有可能的五连位置(横、竖、斜) for (int i = 0; i < SIZE; ++i) { for (int j = 0; j < SIZE; ++j) { // 检查以 (i,j) 为起点的横向五连 if (j + 4 < SIZE) { score += EvaluateFiveInRow(board, i, j, 0, 1, aiType, playerType); } // 纵向 if (i + 4 < SIZE) { score += EvaluateFiveInRow(board, i, j, 1, 0, aiType, playerType); } // 主对角线(右下) if (i + 4 < SIZE && j + 4 < SIZE) { score += EvaluateFiveInRow(board, i, j, 1, 1, aiType, playerType); } // 副对角线(右上) if (i - 4 >= 0 && j + 4 < SIZE) { score += EvaluateFiveInRow(board, i, j, -1, 1, aiType, playerType); } } } return score; } int AIPlayer::EvaluateFiveInRow(const Board& board, int startI, int startJ, int di, int dj, PieceType aiType, PieceType playerType) { int aiCount = 0, playerCount = 0, emptyCount = 0; for (int k = 0; k < 5; ++k) { int i = startI + k * di; int j = startJ + k * dj; PieceType p = board.GetPiece(i, j); if (p == aiType) aiCount++; else if (p == playerType) playerCount++; else emptyCount++; } // 评分规则(数值可调): // 5连:10000(已在MiniMax中处理,此处不重复) // 活四(两端空):1000 // 冲四(一端空):100 // 活三:100 // 冲三:10 // 活二:5 // 其他:0 if (aiCount == 4 && emptyCount == 1) { // 检查是否活四:两端都为空 int prevI = startI - di, prevJ = startJ - dj; int nextI = startI + 5*di, nextJ = startJ + 5*dj; bool isLive = (board.GetPiece(prevI, prevJ) == EMPTY && board.GetPiece(nextI, nextJ) == EMPTY); return isLive ? 1000 : 100; } if (playerCount == 4 && emptyCount == 1) { int prevI = startI - di, prevJ = startJ - dj; int nextI = startI + 5*di, nextJ = startJ + 5*dj; bool isLive = (board.GetPiece(prevI, prevJ) == EMPTY && board.GetPiece(nextI, nextJ) == EMPTY); return isLive ? -1000 : -100; // 阻断玩家活四 } if (aiCount == 3 && emptyCount == 2) { return 100; // 活三 } if (playerCount == 3 && emptyCount == 2) { return -100; // 阻断玩家活三 } return 0; }这个评估函数的威力在于:
EvaluateFiveInRow不只看“是否有四连”,而是判断是否为活四(两端空)或冲四(一端空),这是五子棋 AI 的分水岭;- 对玩家的威胁给予负分,让 AI 优先防守,避免“只顾进攻不顾死活”的低级错误;
- 所有分数设计为数量级分明(1000/100/10),确保 Minimax 能正确排序,不会因小分干扰大分决策。
5. 常见问题排查:VS 编译、GDI 绘图、AI 响应延迟的 5 条真实踩坑记录
5.1 现象:编译时报错LNK2001: unresolved external symbol _WinMain@16
原因:项目配置为“控制台应用程序”而非“Windows 应用程序”,导致链接器寻找main函数而非WinMain。
解决:右键项目 → 属性 → 链接器 → 系统 → 子系统 → 改为“Windows (/SUBSYSTEM:WINDOWS)”;同时确认入口点为WinMain(链接器 → 高级 → 入口点 → 输入WinMain)。
5.2 现象:窗口能打开,但棋盘一片空白,无任何线条
原因:WM_PAINT消息未被触发,常见于InvalidateRect调用后未UpdateWindow,或BeginPaint/EndPaint成对缺失。
解决:检查WndProc中WM_PAINT分支是否完整;在WM_LBUTTONDOWN落子后,确认调用的是InvalidateRect(hWnd, NULL, TRUE)而非RedrawWindow;用 Spy++ 工具监听窗口消息,确认WM_PAINT是否发出。
5.3 现象:鼠标点击位置与实际落子位置偏差 1~2 个格子
原因:cellSize计算使用了width / 15,但width可能不是 15 的整数倍,导致pt.x / cellSize向下取整时累积误差。
解决:改用logicX = (pt.x * 15) / width(整数乘除),确保 0~width-1 的像素均匀映射到 0~14 的逻辑坐标。例如 width=750 时,x=0~49→0,x=50~99→1…x=700~749→14,无间隙无重叠。
5.4 现象:AI 思考时间过长(>3秒),甚至卡死
原因:MiniMax未做剪枝或深度过大;Board拷贝开销大;EvaluateBoard遍历全盘 15×15×4 方向 = 900 次循环,深度为 3 时节点数达 900³ ≈ 7.29 亿。
解决:① 严格限制GetSearchDepth():HARD=2(非3),MEDIUM=1;② 在EvaluateFiveInRow前加if (emptyCount == 0) continue快速跳过满格;③ 将Board改为std::array<std::array<PieceType, 15>, 15>避免 vector 动态分配。
5.5 现象:悔棋功能(若自行添加)后,AI 下出已撤回的棋子
原因:AIPlayer内部缓存了Board状态,未与主游戏Board同步。
解决:删除AIPlayer中所有static Board或成员Board,所有GetBestMove调用均传入当前const Board&;评估函数中禁止修改board,只读取。
6. 二次开发实战:从“能下棋”到“可商用”的 3 个改造技巧与验证方法
6.1 技巧一:用 PNG 替代 GDI 绘图,支持抗锯齿棋子与透明星位
原 GDI 绘图(Ellipse)生成的棋子边缘锯齿明显,且无法实现阴影、渐变等效果。改造方案是引入 Windows GDI+(无需第三方库),用Graphics对象加载 PNG 资源:
// 在 WndProc 的 WM_PAINT 中替换棋子绘制部分 case WM_PAINT: { PAINTSTRUCT ps; HDC hdc = BeginPaint(hWnd, &ps); // 初始化 GDI+ Gdiplus::GdiplusStartupInput gdiplusStartupInput; ULONG_PTR gdiplusToken; Gdiplus::GdiplusStartup(&gdiplusToken, &gdiplusStartupInput, NULL); Gdiplus::Graphics graphics(hdc); graphics.SetSmoothingMode(Gdiplus::SmoothingModeAntiAlias); // 开启抗锯齿 // 加载黑子PNG(需提前准备 resources/black_piece.png) Gdiplus::Image blackPiece(L"resources/black_piece.png"); Gdiplus::Image whitePiece(L"resources/white_piece.png"); // 绘制棋子(假设 cellSize=40,棋子大小32x32) for (int i = 0; i < board.GetSize(); ++i) { for (int j = 0; j < board.GetSize(); ++j) { if (board.GetPiece(i, j) == PLAYER_BLACK) { graphics.DrawImage(&blackPiece, j * cellSize + 4, i * cellSize + 4, 32, 32); } else if (board.GetPiece(i, j) == PLAYER_WHITE) { graphics.DrawImage(&whitePiece, j * cellSize + 4, i * cellSize + 4, 32, 32); } } } Gdiplus::GdiplusShutdown(gdiplusToken); EndPaint(hWnd, &ps); break; }验证方法:编译后对比截图,用 Photoshop 打开并放大 400%,观察棋子边缘是否平滑;用 Resource Hacker 检查 EXE 是否嵌入了 PNG 资源(若未嵌入,需将 PNG 放入程序同目录)。
6.2 技巧二:将 AI 模块抽离为 DLL,支持热替换算法而不重编译主程序
目标是让AIPlayer.dll可被GoBang.exe动态加载,更换 DLL 即切换 AI 策略。步骤如下:
- 新建 Win32 DLL 项目,导出 C 风格函数(避免 C++ 名字改编):
// AIInterface.h extern "C" { __declspec(dllexport) void* CreateAIPlayer(int difficulty); __declspec(dllexport) void DestroyAIPlayer(void* ai); __declspec(dllexport) void GetBestMove(void* ai, const int board[15][15], int* x, int* y, int playerType); }- 主程序中用
LoadLibrary+GetProcAddress加载:
// GameLogic.cpp 中 <p> <a href="https://download.csdn.net/download/zeratools/10178730" style="color:#ec7500;font-size:14px;"> 本文还有配套的精品资源,点击获取 </a> <img alt="menu-r.4af5f7ec.gif" src="https://csdnimg.cn/release/wenkucmsfe/public/img/menu-r.4af5f7ec.gif" style="width:16px;margin-left:4px;vertical-align:text-bottom;cursor:text;"> </p>