探索迷宫生成的艺术与科学

在为Wall-E项目创建迷宫地图时,原本以为会找到一个快速简单的方法来完成这项任务。然而,很快就发现自己沉浸在迷宫和迷宫的广阔而迷人的世界中。之前并不知道这个主题的广度和深度。发现迷宫可以以七种不同的方式进行分类,每种方式都有无数的变化和无数的生成算法。

令人惊讶的是,没有找到任何全面涵盖这一主题的算法书籍,甚至Wikipedia页面也没有提供系统的概述。幸运的是,偶然发现了一个涵盖各种迷宫类型和算法的绝佳资源,强烈推荐探索。

开始了一段旅程,学习不同类型的迷宫,包括维度和超维度变化,完美迷宫与单路径迷宫,平面和稀疏迷宫等。

如何创建迷宫

主要目标是生成一个代表迷宫的2D地图。虽然实现各种迷宫生成算法进行比较会很诱人,但也想要一种更有效的方法。发现的最快解决方案涉及随机选择连接的单元格。这正是使用mazerandom所做的。这个单文件应用程序创建了一个20x20单元格的网格表,然后使用深度优先搜索(DFS)遍历随机连接它们。换句话说,只是在网格中雕刻通道。

如果手动在纸上这样做,它看起来会像这样:

// 假设有一个20x20的网格 vector> maze_cells; // 使用栈进行DFS遍历 stack my_stack; my_stack.push(Coord(0, 0)); // 从第一个单元格开始 // 遍历网格中的每个单元格,并将它的邻居推到栈上进行深度遍历 while (visitedCells < HORIZONTAL_CELLS * VERTICAL_CELLS) { vector neighbours; // 第一步:创建一个未访问邻居单元格的数组 // (来自北,东,南和西)。 // 北方尚未访问? if ((maze_cells[offset_x(0)][offset_y(-1)] & CELL_VISITED) == 0) { neighbours.push_back(0); } // 东方尚未访问? if ((maze_cells[offset_x(1)][offset_y(0)] & CELL_VISITED) == 0) { neighbours.push_back(1); } // 对西和南做同样的操作... // 如果至少有一个未访问的邻居 if (!neighbours.empty()) { int next_cell_dir = neighbours[rand() % neighbours.size()]; // 在邻居和当前单元格之间创建一条路径 switch (next_cell_dir) { case 0: // 北 // 标记为已访问。在南北两个方向上标记连接。 maze_cells[offset_x(0)][offset_y(-1)] |= CELL_VISITED | CELL_PATH_S; maze_cells[offset_x(0)][offset_y(0)] |= CELL_PATH_N; my_stack.push(Coord(offset_x(0), offset_y(-1))); break; case 1: // 东 // 标记为已访问。在东西两个方向上标记连接。 maze_cells[offset_x(1)][offset_y(0)] |= CELL_VISITED | CELL_PATH_W; maze_cells[offset_x(0)][offset_y(0)] |= CELL_PATH_E; my_stack.push(Coord(offset_x(1), offset_y(0))); break; // 对西和南做同样的操作... } visitedCells++; } else { my_stack.pop(); } } // 最后,调用drawMaze()方法使用SFML库在屏幕上绘制迷宫。 // 如果当前单元格没有标记为CELL_PATH_S、CELL_PATH_N、CELL_PATH_W或CELL_PATH_E, // 则在两个单元格之间绘制一堵墙。

然而,这个迷宫并不能保证有解决方案。在许多情况下,它会生成一个没有清晰路径的地图。虽然这种随机性可能很有趣,但想要更结构化的东西。

确保迷宫有解决方案的唯一方法是使用一种预定的结构,以某种方式连接迷宫的每个部分。

使用图论创建迷宫

众所周知的迷宫生成算法依赖于图。每个单元格是图中的一个节点,每个节点至少必须与其他节点有一个连接。

如前所述,迷宫有多种形式。有些被称为“单路径”迷宫,它们作为只有一个入口的迷宫,该入口也作为出口。其他的可能有多个解决方案。然而,生成过程通常从创建一个“完美”迷宫开始。

“完美”迷宫,也称为简单连通迷宫,没有环路、闭合电路和无法到达的区域。从它的任何一点,都有一条精确的路径到达任何其他点。迷宫有一个单一的、可解的解决方案。

如果使用图作为迷宫的内部表示,构建一个生成树确保从起点到终点有一条路径。

在计算机科学术语中,这样的迷宫可以被描述为一组单元格或顶点的生成树。

可能存在多个生成树,但目标是确保至少有一个从起点到终点的解决方案,如下图所示:

上图只显示了一个解决方案,但实际上有多个路径。没有单元格是孤立的,无法到达。那么如何实现这一点呢?

发现了一个设计精良的mazegenerator代码库,由@razimantv创建,它实现了这一点,生成SVG文件格式的迷宫。

因此,分叉了仓库,并基于它构建了解决方案。感谢@razimantv的优雅OOP设计,它允许自定义结果,使用SFML库创建视觉上吸引人的图像,或者生成包含必要地图描述的文本文件,用于Wall-E项目。

重构了代码,去除了不必要的组件,专注于矩形迷宫。

然而,保留了支持各种算法构建生成树的支持。

还在整个代码库中添加了注释,以便更容易理解,所以不需要在这里详细解释。主要的管道可以在\mazegenerator\maze\mazebaze.cpp中找到:

void MazeBase::GenerateMaze(SpanningtreeAlgorithmBase* algorithm) { // Generates entire maze spanning tree auto spanningTreeEdges = algorithm->SpanningTree(_verticesNumber, _edgesList); // Find a solution of a maze based on Graph DFS. _Solve(spanningTreeEdges); // Build a maze by removing unnecessary edges. _RemoveBorders(spanningTreeEdges); }

引入了使用SFML图形库的可视化,这要归功于一个简单的_Draw_函数。

虽然DFS是创建生成树的默认算法,但有多种算法可供选择。

结果是一个方便的实用程序,可以生成矩形“完美”迷宫并在屏幕上显示它们:

如所见,它恰好包含一个输入和一个输出,在左上角和右下角。代码仍然生成SVG文件,这是原始代码库的核心功能(尽管它是核心功能)。

沪ICP备2024098111号-1
上海秋旦网络科技中心:上海市奉贤区金大公路8218号1幢 联系电话:17898875485