
简介这份压缩包是一套基于微软基础类库MFC编写的数据结构课程设计——连连看游戏项目面向正在学习数据结构、面向对象程序设计或Windows桌面开发的学生也适合需要参考完整实例来巩固理论知识的开发者。包内共含532个文件主体包括13个头文件与9个C源文件以及Visual Studio工程配置文件界面资源方面有69个bmp背景图、309个png图片素材和18个wav音效另附可执行程序、PDF说明、Word文档等压缩包整体约287MB结构较完整便于直接打开工程查看代码与资源。目前已有477人浏览学习参考价值得到一定验证。项目利用二维数组表示棋盘状态使用队列进行广度优先搜索求解可消除图案对并借助MFC的CDC完成图形绘制与消息映射处理交互除游戏主逻辑外还包含开始界面、背景切换、消除动画等素材包内各部分分工清晰适合结合数据结构知识点逐模块分析也可在此基础上扩展功能或二次开发。1. 拆开 LLK-MFC-master一个用二维数组撑起来的连连看很多人第一次看到 LLK-MFC-master 这样的工程注意力都会被那堆 bmp 资源吸引——prtScr1.bmp、gamebkg (1).bmp 到 (5).bmp、llk_start_background0.bmp 和 1.bmp还有那个 Lianliankan.aps。但真正决定这个项目能不能跑、跑起来卡不卡、消除判定准不准的是棋盘背后的数据结构选型。连连看看起来是个休闲小游戏实际上是一道很典型的数据结构综合题棋盘怎么存、连通路径怎么找、消除后空缺怎么处理三件事分别对应二维数组、广度优先搜索和状态维护。这个工程适合两类人一类是正在做数据结构课程设计、想找一个能把数组、队列、BFS 串起来的完整案例另一类是想碰 MFC 但一直停留在对话框 Demo 阶段想看看 CDC 绘图、消息映射、资源加载在真实项目里怎么配合。下面按数据层、渲染层、算法层、工程层拆开讲所有步骤都以 Visual Studio 打开 vcxproj 后能直接操作为准。2. 棋盘数据结构选型与 MFC 工程落地2.1 为什么最终还是要回到二维数组摘要里列了二维数组、链表、队列、堆四种候选。真动手写就会发现链表在连连看里是个陷阱。棋盘是固定 8×12 或 10×14 的矩形行列位置天然连续用链表存反而要为每次「取第 i 行第 j 列」付出 O(n) 的遍历代价而连通判定里这个操作会被调用上万次。常见做法是用一个带边界填充的二维数组// 棋盘四周留一圈空白消除判定时不必反复做边界检查 const int ROWS 12; // 含上下各一圈空白 const int COLS 14; // 含左右各一圈空白 const int EMPTY 0; // 0 表示该格已空 int m_board[ROWS][COLS]; // 初始化只给内部区域填图案编号外圈保持 EMPTY void InitBoard() { for (int r 0; r ROWS; r) for (int c 0; c COLS; c) m_board[r][c] EMPTY; // 内部区域 1..ROWS-2, 1..COLS-2 成对填充图案 for (int r 1; r ROWS - 1; r) for (int c 1; c COLS - 1; c) m_board[r][c] RandomType(); // 返回 1..N 的图案编号 }参数说明ROWS 和 COLS 比实际棋盘大 2是为了让「绕棋盘外侧走」这条合法路径天然落在数组范围内。图案编号从 1 开始0 专门留给空位这样判断空格只需一次比较不用额外的布尔数组。RandomType 返回的图案必须成对出现否则会出现无解残局常见做法是先按对数生成再整体洗牌。2.2 在 VS 里把 MFC 工程跑起来vcxproj 打开后第一件事不是编译是确认 MFC 库挂上了。项目属性里检查三处配置项路径应设为使用 MFC配置属性 → 高级 → 使用 MFC在共享 DLL 中使用 MFC字符集配置属性 → 高级 → 字符集使用 Unicode 字符集子系统链接器 → 系统 → 子系统Windows (/SUBSYSTEM:WINDOWS)如果报「无法打开 afxwin.h」或一堆 unresolved external基本都是「使用 MFC」被设成了「不使用 MFC」。另外字符集如果不统一CDC::DrawText 和 CString 的传参会报类型不匹配这是新手在这个工程上最常卡住的地方。资源视图里能看到 rc 文件定义的对话框和图标。aps 文件是 VS 自动生成的资源符号缓存删掉后重新打开工程会重建不用手动维护。2.3 用 CDC 把二维数组画到客户区数据层就绪后渲染是直接的映射数组下标乘单元格尺寸就是像素坐标。void CLLKView::OnDraw(CDC* pDC) { const int CELL 48; // 每格 48 像素与 bmp 图案尺寸一致 for (int r 1; r ROWS - 1; r) { for (int c 1; c COLS - 1; c) { int type m_board[r][c]; if (type EMPTY) continue; // 空格不绘制 int x (c - 1) * CELL; int y (r - 1) * CELL; // 选中格加高亮边框提升点击反馈 if (r m_selR c m_selC) pDC-Rectangle(x - 2, y - 2, x CELL 2, y CELL 2); m_imgList.Draw(pDC, type - 1, CPoint(x, y), ILD_NORMAL); } } }这里用 CImageList 管理图案比每次 LoadBitmap 高效得多图案在初始化时一次性载入。注意 (c-1) 和 (r-1) 的偏移因为数组外圈是空白绘制时要扣掉否则整个棋盘会右下偏移一格。视觉资源里那几张 gamebkg 就是在这里当背景贴图用的先 Draw 背景再叠图案即可。3. 连通判定BFS 找路径与转弯数控制3.1 连连看判定规则的形式化规则说起来简单两个同图案格子如果之间存在一条不经过其他图案、拐弯不超过两次的路径就能消除。关键在「拐弯不超过两次」这个约束它把问题变成带状态的最短路搜索。判定的核心洞察是任意一条合法路径最多三段直线拐点为零到两个。所以不必真的搜最短路径只要枚举所有可能的拐点组合对每段做直线可达性检查即可。常见实现是枚举两个拐点的行列O(N²) 量级在 12×14 的棋盘上完全够用用 BFS 按转弯数做分层搜索也可以逻辑更统一。3.2 直线可达性与转弯点枚举先写最基础的原子操作两点在同一行或同一列且中间全是空的。// 判断同行或同列两点之间是否畅通不含端点 bool CLLKView::IsLineClear(int r1, int c1, int r2, int c2) { if (r1 r2) // 同一行横向扫描 { int lo min(c1, c2) 1, hi max(c1, c2); for (int c lo; c hi; c) if (m_board[r1][c] ! EMPTY) return false; return true; } if (c1 c2) // 同一列纵向扫描 { int lo min(r1, r2) 1, hi max(r1, r2); for (int r lo; r hi; r) if (m_board[r][c1] ! EMPTY) return false; return true; } return false; // 既不同行也不同列不存在直线 }注意端点本身不参与检查因为端点格子是有图案的检查它必然返回 false。这个细节写错会导致所有相邻同类图案都判不出来。一转弯判定就是找一个既是 A 的可达点、又是 B 的可达点的拐点bool CLLKView::CanLinkOneTurn(int r1, int c1, int r2, int c2) { // 拐点 (r1,c2)A 横向到它再纵向到 B if (m_board[r1][c2] EMPTY IsLineClear(r1, c1, r1, c2) IsLineClear(r1, c2, r2, c2)) return true; // 拐点 (r2,c1)A 纵向到它再横向到 B if (m_board[r2][c1] EMPTY IsLineClear(r1, c1, r2, c1) IsLineClear(r2, c1, r2, c2)) return true; return false; }拐点必须是空格这一点常被忽略。如果拐点上有图案路径实际上被挡住了判定会误判为可消除。两转弯则在两点的行列延伸线上枚举拐点对逐段验证。有了外圈那层空白绕棋盘外侧走的情况自动被覆盖不需要单独写分支。3.3 消除后的状态维护与死局检测消除成功后要立刻清空数组并重绘void CLLKView::Eliminate(int r1, int c1, int r2, int c2) { m_board[r1][c1] EMPTY; m_board[r2][c2] EMPTY; m_remain - 2; Invalidate(); // 触发 OnDraw 重绘 if (!HasAnyMatch()) // 无解处理 { if (!ShuffleBoard()) // 洗牌也救不回来 AfxMessageBox(_T(无解游戏结束)); } }HasAnyMatch 的常见做法是遍历所有同类型的格子对调用一次判定函数类型分组可以先按图案编号建索引避免全量两两比较。ShuffleBoard 把剩余图案重新随机分配到现有空格上重新分配后要再检测一次防止洗出死局。这套「消除—检测—洗牌」的循环是保证游戏可玩性的关键缺了死局检测玩家会遇到点遍全场都消不掉的局面。4. 消息映射、资源加载与工程结构排错4.1 消息映射把点击事件接到判定逻辑MFC 不像 Qt 那样用信号槽双击类视图里的对话框资源或者手动在消息映射表里加条目// LLKView.h afx_msg void OnLButtonDown(UINT nFlags, CPoint point); // LLKView.cpp BEGIN_MESSAGE_MAP(CLLKView, CView) ON_WM_LBUTTONDOWN() ON_WM_PAINT() ON_WM_ERASEBKGND() END_MESSAGE_MAP() void CLLKView::OnLButtonDown(UINT nFlags, CPoint point) { int c point.x / CELL 1; // 屏幕坐标反算回数组下标 int r point.y / CELL 1; if (r 1 || r ROWS - 1 || c 1 || c COLS - 1) return; // 点在棋盘外忽略 if (m_board[r][c] EMPTY) return; if (m_selR 0) // 首次选中 { m_selR r; m_selC c; } else if (m_selR r m_selC c) // 点同一格取消选中 { m_selR m_selC -1; } else if (m_board[r][c] m_board[m_selR][m_selC] CanLink(m_selR, m_selC, r, c)) { Eliminate(m_selR, m_selC, r, c); m_selR m_selC -1; } else // 换选 { m_selR r; m_selC c; } Invalidate(); }这里写成point.x / CELL 1而不是减是因为数组带外圈屏幕坐标 (0,0) 对应数组的 (1,1)。坐标换算的偏移方向搞反是整个工程里最容易出现、最难肉眼发现的一类 bug表现是点哪里都消不掉或者消错格子。4.2 资源与 bmp 加载的常见坑工程里那批 bmp 要挂到资源里才能被 LoadBitmap 读到。资源视图 → 右键「添加资源」→ Bitmap → 导入然后给每个 ID 起名。CImageList 建议在窗口初始化时创建BOOL CLLKView::PreCreateWindow(CREATESTRUCT cs) { m_imgList.Create(48, 48, ILC_COLOR24 | ILC_MASK, 0, 8); // 按图案编号顺序加载索引与 m_board 里的编号减一对应 for (UINT id IDB_TILE_BEGIN; id IDB_TILE_END; id) m_imgList.Add(CBitmap(), (COLORREF)0); return CView::PreCreateWindow(cs); }ILC_MASK 配合透明色可以去掉图案背景不然所有格子都会是方块。如果 bmp 尺寸和 CELL 不一致图案会被拉伸或裁切建议先在资源编辑器里确认每张图都是 48×48。现象大概率原因处理编译报 afxwin.h 找不到未启用 MFC属性里改「使用 MFC」图案显示为方块未设透明色ILC_MASK 指定掩码色点击位置偏移坐标换算漏加外圈偏移检查 /CELL1消除后界面不刷新未调用 Invalidate状态变更后立即重绘相邻同类消不掉直线检查把端点算进去IsLineClear 排除端点4.3 拆分源码文件保持工程可维护一个把逻辑、绘制、消息全塞进 View 类的工程写到后面会很难改。常见做法是按职责拆成三个单元GameBoard 负责 m_board、InitBoard、HasAnyMatch、ShuffleBoardLinkChecker 负责 IsLineClear、CanLink 这类纯算法View 只保留绘图和消息响应。这样算法部分可以单独写控制台测试用例不用起整个 MFC 窗口就能验证判定逻辑调试效率完全不同。5. 用控制台测试桩验证连通判定MFC 工程的调试痛点是每次验证算法都要走 GUI慢且难定位。一个实用技巧是把棋盘和判定逻辑抽到一个不依赖 MFC 的独立类里用控制台程序跑自动化用例// test_link.cpp —— 不引用任何 MFC 头文件 int main() { GameBoard board; board.LoadFromString( 0000000 0110000 0010000 0000000); // 1 表示同种图案 LinkChecker checker(board); // 两个 1 可以经外侧绕行应返回 true assert(checker.CanLink(1, 1, 2, 2) true); board.Set(1, 2, 1); // 挡住直连路径 assert(checker.CanLink(1, 1, 2, 2) true); // 仍有绕行路径 board.Set(1, 3, 1); board.Set(2, 1, 1); // 三面围死 assert(checker.CanLink(1, 1, 2, 2) false); return 0; }用字符串写棋盘的好处是测试用例一眼能看懂改一个字符就能构造新场景。把 IsLineClear、CanLink 从 MFC 的 CView 里剥出来是这个工程最值得做的重构它让数据结构部分的正确性可以被独立验证而不是靠肉眼盯着屏幕猜。判定正确之后再看性能。12×14 棋盘上每次点击的判定量在毫秒级一般不需要优化但如果把棋盘做到 20×20 以上两转弯枚举的 O(N⁴) 会开始有感。这时可以按转弯数做分层 BFS每一层只扩展当前转弯数可达的格子把复杂度压下来。常见的分层做法是起点先向四个方向做直线延伸遇到的空格入队并标记为「1 转弯可达」再从这些格子继续向垂直方向延伸超过两次转弯就停止。用已访问标记避免重复入队是这类搜索的标准写法。路径本身也可以存下来用于绘制消除时的那道连线动画把转弯点坐标传给 CDC 画折线即可这是让游戏手感明显提升的一步。本文还有配套的精品资源点击获取