简介: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::vector<std::vector<int>> 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::vector<std::vector<int>>& 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::vector<std::pair<int, 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::vector<int> 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[] = L"MazeWindow"; 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, L"VC++迷宫游戏 - 随机生成地图", 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先注册窗口类,告诉系统窗口过程函数是WndProc;CreateWindowEx负责真正创建窗口,标题和初始尺寸都写在这里。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::vector<std::vector<int>>& 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)到((c+1)*cellSize, (r+1)*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::vector<std::vector<int>>& 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原样复现,再也不用靠肉眼在屏幕里找问题。
参数说明:如果想让生成器稳定可复现,可以给程序加一个--seed=xxxx的命令行参数;没有参数时用GetRandomSeed()自动生成。这样既满足“每次启动随机”,也保留“指定唯一图反复观察”的调试入口,后面做批量回归测试也要靠这个设计。
4.2 用DS堆栈验证生成地图的连通性:每个游戏地图都要能通关
DFS回溯生成本身保证所有路是连通的,但如果你中途改过尺寸、跳过奇偶校验、或者额外打通过一堵墙,生成结果就可能出现不可达区域。最直接的验证办法是再跑一次堆栈遍历,从起点出发统计能到达多少个路格,如果终点也被访问到且可达格子数大于1,说明地图没有孤岛,玩家理论上一定到得了终点。
// MazeVerify.cpp —— 用堆栈遍历验证迷宫连通性 #include <stack> bool VerifyMaze(const std::vector<std::vector<int>>& map, int startR, int startC, int goalR, int goalC) { int rows = (int)map.size(); int cols = (int)map[0].size(); std::vector<std::vector<bool>> visited(rows, std::vector<bool>(cols, false)); std::stack<std::pair<int, 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 + h,g是起点到当前格子的实际步数,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字符集,代码里写"迷宫游戏"这类窄字符串时直接编译失败,或者窗口标题变成乱码。
原因:项目字符集默认是UNICODE,Win32 API的宽字符版本接收wchar_t*,普通窄字符串不能隐式转换。
解决:窗口类名和标题统一写L"MazeWindow"、L"VC++迷宫游戏",字符串字面量全部加L前缀。也可以把项目属性里的字符集改成“使用多字节字符集”,但改工程属性不如改代码直观,后面接文件存档或排行榜功能时,宽字符更统一,不会出现半个汉字被截断的乱码问题。
5.5 玩家移动方向错乱:按上键人往左走,还时不时穿墙
现象:方向键和屏幕上的移动方向对不上,偶尔人直接走进墙里,从画面上看就是穿模。
原因:地图、绘制、移动三处坐标约定不一致。map是行优先,行对应y,列对应x;绘制RECT的left/right属于x维度。常见的错误是把map[c][r]当成坐标,或者RECT写成了(rcellSize, ccellSize, rcellSize+cellSize, ccellSize+cellSize),正好把行和列调换。
解决:先做一次坐标对齐:在WM_KEYDOWN里用SetWindowText把playerR和playerC实时打到窗口标题,按下按键走一步比对一次,能立刻看出方向映射是哪一环错了。再把绘制代码固定成“外循环行、内循环列”,并且在map定义处注释一行“行=y,列=x”,防止换文件后忘掉约定。
6. 让迷宫“活”起来:环路参数、自动求解与生成器回归验证
6.1 用一个环路参数控制难度:完美迷宫太绕怎么办
DFS生成的完美迷宫路径唯一,地图一大就非常绕。常见做法是生成后再随机打通K堵墙,让本来唯一的路径出现分叉,玩家有岔路可走,难度反而下降。
// MazeLoops.cpp —— 随机打通墙,制造环路降低难度 void AddLoops(std::vector<std::vector<int>>& 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”的思路可能是整个项目里性价比最高的扩展,希望帮到你。
本文还有配套的精品资源,点击获取