ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

VC++迷宫游戏:深度优先回溯随机地图与Win32实现

VC++迷宫游戏:深度优先回溯随机地图与Win32实现 简介VC编写的迷宫小游戏源码资源面向C初学者与对Windows图形界面编程感兴趣的读者。程序支持随机生成迷宫地图运行后可通过键盘方向键控制红色方块在迷宫中移动直到顺利走出迷宫即为胜利适合用于练习MFC界面搭建、消息处理、二维数组与简单寻路逻辑。压缩包共11个文件以头文件、源文件与工程配置文件为主其中h文件分别负责迷宫生成、绘制、寻路等功能模块cpp与rc文件承载主程序入口及界面资源整体体积仅14KB结构轻量、便于快速查看与修改。当前已有487人学习下载适合希望结合小游戏实践巩固C基础、研究随机迷宫算法与键盘交互实现的学习者。1. 随机生成迷宫地图才是这个VC迷宫游戏的核心价值VC迷宫游戏最容易被低估的一环不是玩家移动也不是界面绘制而是随机生成迷宫地图。很多人照着网上源码抄完发现自己做的地图是写死的玩一次就再也没兴趣打开真正值得投入的做法是把生成器当成独立模块来写让游戏每次启动都出现一张从没走过的图。这篇内容解决三件事怎么用二维数组表示迷宫怎么用深度优先回溯“挖”出一张随机地图以及怎么把这张地图搬进VC窗口里变成可玩的游戏。适合正在做课程设计的人也适合想借着这个小项目把递归、栈、消息循环、GDI绘图串起来的VC自学者。全文不绕弯子直接给代码和参数坑单独列一章讲。2. 随机生成迷宫地图的两种主流算法为什么我首选深度优先回溯迷宫说白了是一张网格图每个格子是节点墙是边随机生成迷宫就是在一张全墙网格里随机挖出一棵生成树。树决定了任意两个路格之间有且只有一条通路。搞清楚这个前提再看任何生成算法都不会晕。2.1 深度优先回溯、随机Prim与并查集三选一怎么选常见做法有三条路深度优先回溯从起点出发随机挑一个隔两格的墙挖过去遇到死路就弹栈回头随机Prim从生长墙集合里随机挑墙打通不断扩展已连通区域并查集变体把每条墙当边做随机合并本质是Kruskal生成树的随机版。算法拓扑结构代码量迷宫风格深度优先回溯树唯一路径最小路径细长主干方向感强随机Prim树唯一路径中等分支均匀整体更开阔并查集变体多为树易扩展成带环中等接近洞穴多路径可玩对VC窗口小游戏来说地图一般只有几十乘几十格三种算法性能没有可感知的差别。我一般首选深度优先回溯理由有三个算法只需要一个栈不需要额外的优先队列或并查集容器生成的迷宫天然是完美迷宫后面做自动求解和连通性验证都简单对初学者来说“死路就回头”的直觉非常直接正好复习栈的用法。2.2 地图表示用二维数组把迷宫变成可计算的格子不管用哪种算法最终都落在一个数据结构上std::vectorstd::vectorint map; // 0表示路1表示墙map[row][col]里的row是行对应屏幕上的y方向col是列对应屏幕上的x方向。这个约定务必先定死否则后面绘制、移动、求解全部串位这是这个项目里最容易翻车的坐标问题。要让生成结果看起来自然尺寸上有硬性要求rows和cols都必须取奇数路坐标全部落在奇数行奇数列上。这样四周天然是一圈墙墙厚统一为一格玩家看到的路宽不会忽宽忽窄。起点固定放(1,1)终点放在(rows-2, cols-2)也就是地图右下角内侧一格。迷宫生成后玩家从左上出发一路摸到右下判定条件非常干净。注意行对应y、列对应x的约定必须贯穿生成、绘制、移动三处代码后文所有代码都按这个约定写。2.3 深度优先回溯生成的核心函数一次挖两格走到死路再回溯生成逻辑可以拆成四步先把整张图全部置为墙再把起点那一格挖成路然后从当前格子随机挑一个距离为2的候选格子如果它是墙就把它和当前格子中间那格墙也挖掉走到新格子继续如果当前格子周围没有可挖的墙就回退到上一个格子。下面这段是可以直接丢进控制台工程验证的完整函数// maze_gen.cpp —— DFS 回溯法随机生成迷宫核心实现 #include vector #include random #include utility // map: 0路 1墙rows/cols必须是奇数且至少为3 void generateMaze(std::vectorstd::vectorint map, int rows, int cols, unsigned int seed) { // 1) 整张地图先全部置为墙 for (int i 0; i rows; i) for (int j 0; j cols; j) map[i][j] 1; std::mt19937 rng(seed); // 2) 从(1,1)开始挖 int startR 1, startC 1; map[startR][startC] 0; // 用vector模拟栈避免深层递归导致调用栈溢出 std::vectorstd::pairint, int stack; stack.push_back({startR, startC}); // 四个方向上、下、左、右每一步跳两格 int dr[4] {-2, 2, 0, 0}; int dc[4] {0, 0, -2, 2}; while (!stack.empty()) { int r stack.back().first; int c stack.back().second; // 收集当前格子周围还没被挖通的候选方向 std::vectorint dirs; for (int k 0; k 4; k) { int nr r dr[k]; int nc c dc[k]; if (nr 0 nr rows nc 0 nc cols map[nr][nc] 1) { dirs.push_back(k); } } if (!dirs.empty()) { // 随机挑一个方向把中间那格墙也挖掉 int k dirs[rng() % dirs.size()]; int wallR r dr[k] / 2; int wallC c dc[k] / 2; map[wallR][wallC] 0; int nr r dr[k]; int nc c dc[k]; map[nr][nc] 0; stack.push_back({nr, nc}); // 踏入新格子 } else { stack.pop_back(); // 死路回溯到上一个分支点 } } }逻辑说明rng() % dirs.size() 只做一次随机映射方向的选取均匀分布不会出现固定顺序的规律图dirs为空表示当前格子四周两格范围要么越界、要么已经被挖过此时弹出栈顶程序自动回到上一个分支点。整段代码没有递归栈由vector手动维护地图做大到几百乘几百也不会爆栈。参数说明rows和cols必须是奇数否则最右侧或最下侧会残留一整条墙带玩家走到终点附近会发现路被堵死seed是随机种子同一个seed必然生成同一张图用系统高精度时间做种子就能做到每次启动都不同这个特性后文复现地图和做回归测试都要用起点不一定固定在(1,1)任意奇数坐标都行但游戏里建议固定方便玩家理解“从左上到右下”的规则dr/dc用±2而不是±1是因为一次要挖两格之间的墙跳两格是为了保证路宽恒定为一格不会挖出宽窄不一的通道。3. 把生成器接进VC窗口从控制台打印到可操作游戏控制台里能打印出字符迷宫只是第一步真正让这个项目成为“游戏”要把地图画进窗口再用键盘控制玩家走动。这一章直接给Win32落地方案按窗口骨架、双缓冲绘制、玩家移动三步走。3.1 用Win32而不是MFC一个最小窗口骨架常见做法是直接在VC里建一个Win32窗口工程而不是套MFC。这个项目的核心逻辑不依赖MFC的文档视图框架Win32的消息循环足够代码也更好抄。如果你用的是VS2017或更高版本新建项目时选“Windows桌面应用程序”把预生成代码替换成下面这段最小骨架// MazeWin.cpp —— Win32 窗口骨架 #include windows.h #include vector #include string LRESULT CALLBACK WndProc(HWND hwnd, UINT msg, WPARAM wp, LPARAM lp) { switch (msg) { case WM_DESTROY: PostQuitMessage(0); return 0; default: return DefWindowProc(hwnd, msg, wp, lp); } } int WINAPI WinMain(HINSTANCE hInst, HINSTANCE, LPSTR, int nCmdShow) { const wchar_t CLASS_NAME[] LMazeWindow; WNDCLASS wc {}; wc.lpfnWndProc WndProc; wc.hInstance hInst; wc.hCursor LoadCursor(nullptr, IDC_ARROW); wc.hbrBackground (HBRUSH)GetStockObject(WHITE_BRUSH); wc.lpszClassName CLASS_NAME; RegisterClass(wc); HWND hwnd CreateWindowEx( 0, CLASS_NAME, LVC迷宫游戏 - 随机生成地图, WS_OVERLAPPEDWINDOW, CW_USEDEFAULT, CW_USEDEFAULT, 480, 520, nullptr, nullptr, hInst, nullptr); ShowWindow(hwnd, nCmdShow); MSG msg; while (GetMessage(msg, nullptr, 0, 0)) { TranslateMessage(msg); DispatchMessage(msg); } return 0; }逻辑说明RegisterClass先注册窗口类告诉系统窗口过程函数是WndProcCreateWindowEx负责真正创建窗口标题和初始尺寸都写在这里。GetMessage循环一直取消息TranslateMessage负责把键盘按下转成字符消息DispatchMessage再把消息交给WndProc去处理。初学者最容易踩的坑是忘记RegisterClass就直接CreateWindow结果CreateWindowEx返回空句柄程序看起来启动成功但窗口不出来。参数说明窗口宽高480x520只是一个初始值实际地图的尺寸是cols乘以格子像素、rows乘以格子像素两者不一致时右侧和底部会出现多余白边解决方法是等地图生成后用SetWindowPos按计算出的客户区尺寸重新调整窗口。标题用的宽字符L写法是为了避开窄字符与Unicode字符集混用的编译问题后面避坑章节还会展开讲。3.2 双缓冲绘制迷宫不闪屏的祖传手艺直接在WM_PAINT里用FillRect一格一格画墙和路窗口刷新时会闪到怀疑人生。原因很简单每次重绘系统都要先擦掉整块背景再逐个画墙和路屏幕会先变白再变黑人眼看到的就是闪烁。标准解法是双缓冲先在内存里把整张图画完再一次BitBlt拷贝上屏。// MazeDraw.cpp —— 双缓冲绘制迷宫地图 void DrawMaze(HWND hwnd, HDC hdc, const std::vectorstd::vectorint map, int cellSize) { int rows (int)map.size(); int cols (int)map[0].size(); int width cols * cellSize; int height rows * cellSize; HDC memDC CreateCompatibleDC(hdc); HBITMAP bmp CreateCompatibleBitmap(hdc, width, height); HGDIOBJ oldBmp SelectObject(memDC, bmp); // 背景整体涂成墙的颜色 RECT bg {0, 0, width, height}; FillRect(memDC, bg, (HBRUSH)GetStockObject(BLACK_BRUSH)); // 遍历地图把路画成白色 HBRUSH roadBrush CreateSolidBrush(RGB(255, 255, 255)); for (int r 0; r rows; r) { for (int c 0; c cols; c) { if (map[r][c] 0) { RECT rc {c * cellSize, r * cellSize, (c 1) * cellSize, (r 1) * cellSize}; FillRect(memDC, rc, roadBrush); } } } DeleteObject(roadBrush); // 一次性拷上屏避免中间状态暴露 BitBlt(hdc, 0, 0, width, height, memDC, 0, 0, SRCCOPY); SelectObject(memDC, oldBmp); DeleteObject(bmp); DeleteDC(memDC); }逻辑说明CreateCompatibleDC创建一块与窗口DC兼容的内存画布CreateCompatibleBitmap分配同尺寸位图之后所有FillRect都画在这块内存上屏幕上不会出现任何中间状态。坐标换算关系是第r行第c列的路画布上从(ccellSize, rcellSize)到((c1)*cellSize, (r1)*cellSize)也就是说列决定横向x行决定纵向y。参数说明cellSize是格子像素宽度一般取10到20取10时200行乘200列的地图也不超过两千像素宽取值太小路看不清太大窗口装不下。在WM_PAINT里调用时hdc来自BeginPaint(hwnd, ps)记得用EndPaint收尾。每次绘制都创建删除内存DC不是最优解但如果只是几百格的小迷宫性能完全够用想进一步优化就把memDC和bmp提升为窗口类成员变量只在窗口尺寸变化时重建。3.3 玩家移动碰撞检测其实只有一行if地图数据和绘图函数就绪后交互只剩键盘方向键让玩家坐标移动。难点不在消息处理而在“目标格子必须是路”。墙永远不可达所以移动前只需要检查下一个坐标是否越界、map值是否为0。// MazeInput.cpp —— 方向键移动玩家位置 void MovePlayer(HWND hwnd, int playerR, int playerC, const std::vectorstd::vectorint map, int keyDir) { int rows (int)map.size(); int cols (int)map[0].size(); int nr playerR, nc playerC; if (keyDir VK_UP) nr--; else if (keyDir VK_DOWN) nr; else if (keyDir VK_LEFT) nc--; else if (keyDir VK_RIGHT) nc; // 碰撞检测越界和撞墙都算非法移动 if (nr 0 nr rows nc 0 nc cols map[nr][nc] 0) { playerR nr; playerC nc; InvalidateRect(hwnd, nullptr, TRUE); // 请求重绘不阻塞当前流程 } } void DrawPlayer(HDC hdc, int playerR, int playerC, int cellSize) { // 用红色方块表示玩家比路格小一圈更醒目 HBRUSH playerBrush CreateSolidBrush(RGB(220, 20, 20)); RECT rc {playerC * cellSize cellSize / 4, playerR * cellSize cellSize / 4, (playerC 1) * cellSize - cellSize / 4, (playerR 1) * cellSize - cellSize / 4}; FillRect(hdc, rc, playerBrush); DeleteObject(playerBrush); }逻辑说明MovePlayer先拷贝当前坐标再按方向键修改副本只有边界和路都满足条件才更新真实坐标这样就不会出现按住方向键穿过墙的翻车画面。DrawPlayer里的cellSize/4是留白边让玩家方块比路格小一圈视觉上更显眼也不会把脚下整格路盖死。参数说明InvalidateRect第三个参数TRUE表示擦除背景后触发WM_PAINT实际绘制由消息循环统一处理不在按键回调里直接画避免连续按键时界面卡顿。通关判断放在MovePlayer之后比对playerR和playerC是否等于(rows-2, cols-2)成立就弹出MessageBox提示同时用新的随机种子重新生成一张地图。这里还有一个加分细节把当前seed显示在窗口标题栏玩家迷路到想吐槽时可以拿着这个seed找你复现同一张图。4. 随机性的信任问题种子里藏着“唯一的迷宫”生成器写完最大的疑问是“它到底随不随机”。这个阶段要处理三件事换掉不可靠的rand()、用堆栈验证生成结果可通关、再引入启发式搜索做深一层分析。三者共同决定随机生成这个功能能不能被信任。4.1 从rand()的玄学到mt19937让每次启动都不一样很多人写完生成器后发现“每次跑出来的迷宫都差不多”。这不是算法写错而是随机源没选对。C标准库的rand()配srand(time(NULL))在小地图上勉强能跑但rand()的低位随机性一般而且time(NULL)精确到秒同一秒内连续启动两个进程迷宫会一模一样。这也是为什么生成器里直接用std::mt19937它的周期长、分布均匀是VC里做游戏地图生成比较稳的选择。把种子打印出来是一个值得长期保留的习惯// SeedUtil.cpp —— 生成并打印种子方便复现某张地图 #include chrono #include cstdio unsigned int GetRandomSeed() { // 用高精度时钟的计数值做种子两次启动几乎不可能相同 unsigned int seed (unsigned int)std::chrono::system_clock::now() .time_since_epoch().count(); return seed; } // WinMain 里调用 // unsigned int seed GetRandomSeed(); // std::printf([maze] seed %u\n, seed);逻辑说明time_since_epoch().count()返回纳秒级整数截断成unsigned int后当种子即使两次启动只差几微秒低32位也大概率不同。打印seed的意义在于玩家说某张图特别绕你可以用同一个seed原样复现再也不用靠肉眼在屏幕里找问题。参数说明如果想让生成器稳定可复现可以给程序加一个--seedxxxx的命令行参数没有参数时用GetRandomSeed()自动生成。这样既满足“每次启动随机”也保留“指定唯一图反复观察”的调试入口后面做批量回归测试也要靠这个设计。4.2 用DS堆栈验证生成地图的连通性每个游戏地图都要能通关DFS回溯生成本身保证所有路是连通的但如果你中途改过尺寸、跳过奇偶校验、或者额外打通过一堵墙生成结果就可能出现不可达区域。最直接的验证办法是再跑一次堆栈遍历从起点出发统计能到达多少个路格如果终点也被访问到且可达格子数大于1说明地图没有孤岛玩家理论上一定到得了终点。// MazeVerify.cpp —— 用堆栈遍历验证迷宫连通性 #include stack bool VerifyMaze(const std::vectorstd::vectorint map, int startR, int startC, int goalR, int goalC) { int rows (int)map.size(); int cols (int)map[0].size(); std::vectorstd::vectorbool visited(rows, std::vectorbool(cols, false)); std::stackstd::pairint, int st; st.push({startR, startC}); visited[startR][startC] true; int reachable 0; while (!st.empty()) { auto [r, c] st.top(); st.pop(); reachable; int dr[4] {-1, 1, 0, 0}; int dc[4] {0, 0, -1, 1}; for (int k 0; k 4; k) { int nr r dr[k], nc c dc[k]; if (nr 0 nr rows nc 0 nc cols !visited[nr][nc] map[nr][nc] 0) { visited[nr][nc] true; st.push({nr, nc}); } } } return visited[goalR][goalC] reachable 1; }逻辑说明这和生成时用到的回溯是同一套思想只是从“挖路”变成“走路”。每次弹出栈顶就计一次可达再把四个相邻路格压栈最终如果终点没有被标记为visited说明生成器一定存在bug。这个函数跑一趟只要几毫秒每次重新生成地图后调用一次能拦住绝大多数边界问题。参数说明startR/startC和goalR/goalC要和你游戏里的实际位置一致别把终点设在(rows-1, cols-1)那里是外圈墙永远不可达。我习惯把生成和验证放在同一个测试函数里验证失败就直接用断言终止避免带病地图进入游戏循环。4.3 求解器除了验证还能玩从堆栈到启发式搜索验证时用的是普通堆栈但要做最短路径分析或自动演示通关堆栈就不够用了。DFS找到的路径不保证最短启发式搜索里最常用的是A*。评估公式是f g hg是起点到当前格子的实际步数h是当前格子到终点的启发值。迷宫游戏只能上下左右走启发值用曼哈顿距离最直观abs(r-goalR) abs(c-goalC)。// MazeAStar.cpp —— 极简A*只展示启发函数核心 // 完整实现需要 openList/closeList这里省略容器细节 int Heuristic(int r, int c, int goalR, int goalC) { // 曼哈顿距离只能上下左右走时这是理论最少步数 return abs(r - goalR) abs(c - goalC); }直观理解是每次从待扩展列表里取f值最小的格子继续走小迷宫用线性查找就够了地图超过100x100再换std::priority_queue。把这个求解器接进游戏可以按F2让电脑自动演示通关或者用来评测“当前这张图平均要多少步”做课程设计答辩时有这个功能非常加分。它和4.2的验证代码用的是同一套坐标约定这也再次说明“行对应y、列对应x”的约定一旦定死整个项目都不会乱。5. 迷宫游戏避坑手册5个最容易翻车的细节这一章是血泪经验汇总每一条都按现象、原因、解决三个层次说清楚。写迷宫生成器的人十有八九会在这五个位置栽一次。5.1 迷宫一上窗口就疯狂闪屏现象玩家移动或窗口拉伸时迷宫整片闪烁严重时路径都看不清。原因WM_PAINT里直接对窗口hdc做多笔FillRect擦除和重绘的中间状态全部暴露给屏幕视觉上就是闪烁。解决改用3.2节的双缓冲先画内存DC再BitBlt上屏。如果仍然闪检查是否忘了在WM_ERASEBKGND里返回TRUE阻止系统额外擦背景。窗口尺寸不变时把内存DC和位图存成窗口类成员变量只创建一次能进一步减少闪烁。5.2 迷宫右下角总有一片空白或一堵厚墙现象玩家能到达地图大部分区域但终点右侧或下方还残留一条没挖开的路格或者一整列都是墙。原因创建map时用了偶数行或偶数列。深度优先回溯每次跳两格偶数尺寸必然导致最右侧或最下侧出现无法落脚的墙带。解决在生成入口做一次强制修正if (rows % 2 0) rows; if (cols % 2 0) cols;。同时检查起点是否落在(1,1)map[0][0]必须是墙才代表外圈完整。5.3 每次运行迷宫长得差不多换个电脑更是同一个迷宫现象用srand(time(NULL))后短时间内连续启动两张地图结构高度相似甚至完全一致。原因time(NULL)精确到秒同一秒内种子相同另外rand()低位周期短取模小数字时随机性更差。解决换用std::mt19937并传入高精度时间种子具体代码见4.1节。如果项目里其他地方仍要用rand()至少别再用rand()%4这种方式取方向改成rand() / (RAND_MAX / 4)把高位映射过去但结论是没必要和标准库较劲统一用mt19937最省心。5.4 编译报错“无法将参数从const char*转换为LPCWSTR”现象VS2017或更高版本默认Unicode字符集代码里写迷宫游戏这类窄字符串时直接编译失败或者窗口标题变成乱码。原因项目字符集默认是UNICODEWin32 API的宽字符版本接收wchar_t*普通窄字符串不能隐式转换。解决窗口类名和标题统一写LMazeWindow、LVC迷宫游戏字符串字面量全部加L前缀。也可以把项目属性里的字符集改成“使用多字节字符集”但改工程属性不如改代码直观后面接文件存档或排行榜功能时宽字符更统一不会出现半个汉字被截断的乱码问题。5.5 玩家移动方向错乱按上键人往左走还时不时穿墙现象方向键和屏幕上的移动方向对不上偶尔人直接走进墙里从画面上看就是穿模。原因地图、绘制、移动三处坐标约定不一致。map是行优先行对应y列对应x绘制RECT的left/right属于x维度。常见的错误是把map[c][r]当成坐标或者RECT写成了(rcellSize, ccellSize, rcellSizecellSize, ccellSizecellSize)正好把行和列调换。解决先做一次坐标对齐在WM_KEYDOWN里用SetWindowText把playerR和playerC实时打到窗口标题按下按键走一步比对一次能立刻看出方向映射是哪一环错了。再把绘制代码固定成“外循环行、内循环列”并且在map定义处注释一行“行y列x”防止换文件后忘掉约定。6. 让迷宫“活”起来环路参数、自动求解与生成器回归验证6.1 用一个环路参数控制难度完美迷宫太绕怎么办DFS生成的完美迷宫路径唯一地图一大就非常绕。常见做法是生成后再随机打通K堵墙让本来唯一的路径出现分叉玩家有岔路可走难度反而下降。// MazeLoops.cpp —— 随机打通墙制造环路降低难度 void AddLoops(std::vectorstd::vectorint map, int K, unsigned int seed) { std::mt19937 rng(seed); int rows (int)map.size(), cols (int)map[0].size(); int added 0; while (added K) { int r 1 (int)(rng() % (rows - 2)); int c 1 (int)(rng() % (cols - 2)); if (map[r][c] 1) { map[r][c] 0; added; } } }注意这个粗暴版本有可能打通终点外圈墙所以AddLoops之后必须再跑一次VerifyMaze失败就换个种子重来。K一般取地图格子总数的2%到5%再多就失去迷宫感变成开放场地了。6.2 把验证做成例行测试批量生成与最短路径统计我习惯把生成器和验证器放进同一个自动化测试入口连续跑上千个随机种子统计失败率和平均最短路径步数。做法是循环里依次调用generateMaze、VerifyMaze再用A*算出最短路径长度输出到控制台或CSV。调整算法参数后重跑一遍对比平均步数变化能直观知道迷宫到底变简单还是变难了。// RegressionTest.cpp —— 批量回归测试生成器 for (int i 0; i 1000; i) { unsigned int seed GetRandomSeed(); generateMaze(map, rows, cols, seed); if (!VerifyMaze(map, 1, 1, rows - 2, cols - 2)) { std::printf([FAIL] seed%u\n, seed); } }这个习惯帮我提前发现过不少只有特定种子才会触发的边界问题比用户玩到一半再反馈靠谱得多。生成器改过一次以后哪怕只是改了尺寸判断也要把整套回归跑一遍再发布。6.3 把好看的地图存下来写迷宫写得多了我自己的习惯是每次运行都把seed打印出来看到结构特别好的图就记进一个预设列表。玩家想玩“上一张图”时通过命令行参数把seed传回去生成器就能原样复现。这个设计让随机生成迷宫地图从不可控的黑匣子变成可维护的资源库比只做一次性随机耐玩得多。存档系统也可以直接拿seed当存档内容几十个字节就能描述一张完整地图不需要把几百乘几百的map序列化成文件省掉不少IO代码。如果你打算做多关卡或地图分享这种“存seed”的思路可能是整个项目里性价比最高的扩展希望帮到你。本文还有配套的精品资源点击获取
返回列表