【UE4 C++】迷宫生成——DFS、Prim、Kruskal算法实现
- 主要参考: 三套简单的迷宫地图生成方案(兔四),按照自己的理解实现
- 实现版本: 4.26.2
- 本文原创地址
DFS 算法
-
主要步骤
- 初始化大地图,只有0和1的状态。其中,0和1分别代表道路和墙体,注意四周皆墙
- 靠近边缘随机选取状态为1的道路点,作为起点 a
- 在起点 a 的上下左右四个方向,跨两个寻找同样为1的道路点 c
- 如果找到,则将它们之间的墙体 b 打通,然后将 c 作为新的起点,然后继续进行第2步
- 如果四个方向都没有找到,则不断回退到上一点,直到找到有一点其他方向满足条件,然后继续查询
- 当查无可查的时候,迷宫也就填充完毕了
-
效果
-
这种算法生成的迷宫会有比较明显的主路
-
-
UE4 实现主要代码
-
头文件
点击查看代码
//@本文原创地址 https://www.cnblogs.com/shiroe/p/15506909.html public: UPROPERTY(VisibleAnywhere) USceneComponent* rootComp; UPROPERTY() TArray<UStaticMeshComponent*> roadrMeshs; UPROPERTY(EditAnywhere) UStaticMesh* PlaneMesh; UPROPERTY(EditAnywhere) UStaticMesh* BlockMesh; UPROPERTY(EditAnywhere) uint32 width; UPROPERTY(EditAnywhere) uint32 height; UPROPERTY(EditAnywhere) float playRate=0.2f; // 演示速度 UPROPERTY(EditAnywhere) int32 creationMode=0; // 生成迷宫的方式,三选一 UPROPERTY() FTimerHandle timerHandle; std::vector<std::vector<int32>> roadArr; //迷宫矩阵 TQueue<TTuple<int32, int32>> taskQueue; //绘制队列 public: //改变mesh 生成道路 void ChangeMesh(); // 按照间隔规则,DFS算法生成迷宫 // 此时 roadArr 中,0代表墙,1代表道路点,2代表已经确认过的道路点 void Simple_CreateMaze(); void Simple_DFSFindPath(int32 x, int32 y); bool Simple_checkPointValid(int32 x, int32 y);
-
CPP
//@本文原创地址 https://www.cnblogs.com/shiroe/p/15506909.html // 创建迷宫 void AMazeCreator::Simple_CreateMaze() { roadArr = std::vector<std::vector<int32>>(width, std::vector<int32>(height, 0)); //初始化 for (int32 i = 0; i < (int32)width; i++) { for (int32 j = 0; j < (int32)height; j++) { UStaticMeshComponent* smComp = NewObject<UStaticMeshComponent>(this); smComp->SetupAttachment(rootComp); smComp->RegisterComponent(); smComp->SetRelativeScale3D(FVector(1.0f)); smComp->SetRelativeLocation(FVector((j - (int32)height / 2) * 100, (i - (int32)width / 2) * 100, 0)); //判断是否可以作为道路点 bool bCanBeRoad = (i % 2) && (j % 2) && i * j != 0 && i != height - 1 && j != width - 1; roadArr[i][j] = (int32)bCanBeRoad; smComp->SetStaticMesh(bCanBeRoad ? PlaneMesh : BlockMesh); roadrMeshs.Add(smComp); } } Simple_DFSFindPath(1, 1); // 搜索 GetWorld()->GetTimerManager().SetTimer(timerHandle,this, &AMazeCreator::ChangeMesh, playRate, true,1); } // 此时 roadArr 中,0代表墙,1代表道路点,2代表已经确认过的道路点 void AMazeCreator::Simple_DFSFindPath(int32 x, int32 y) { roadArr[x][y] = 2; taskQueue.Enqueue(TTuple<int32, int32>(x, y)); TArray<int32> offsetX = { -1, 0, 1, 0 }; TArray<int32> offsetY = { 0, -1, 0, 1 }; int32 randomInt = UKismetMathLibrary::RandomInteger(4); //随机某个方向开始 for (int32 j = randomInt; j < randomInt + 4; ++j) { int32 i = j % 4; if (Simple_checkPointValid(x + offsetX[i] * 2, y + offsetY[i] * 2)) { roadArr[x + offsetX[i]][y + offsetY[i]] = 2; taskQueue.Enqueue(TTuple<int32, int32>(x + offsetX[i], y + offsetY[i])); Simple_DFSFindPath(x + offsetX[i] * 2, y + offsetY[i] * 2); } } } bool AMazeCreator::Simple_checkPointValid(int32 x, int32 y) { return (0 < x && x < (int32)width - 1) && (0 < y && y < (int32)height - 1) && roadArr[x][y] == 1; } void AMazeCreator::ChangeMesh() { TTuple<int32, int32> point; if (taskQueue.Dequeue(point) && roadrMeshs.Num() > 0) { roadrMeshs[point.Get<0>() * height + point.Get<1>()]->SetStaticMesh(PlaneMesh); roadrMeshs[point.Get<0>() * height + point.Get<1>()]->AddRelativeLocation(FVector(0, 0, -50)); } } void AMazeCreator::BeginPlay() { Super::BeginPlay(); switch (creationMode) { case 0:Simple_CreateMaze(); break; case 1:Prim_CreateMaze(); break; case 2:Kruskal_CreateMaze(); break; } }
-
随机Prim算法
-
主要步骤
- 初始化大地图,只有0和1的状态。其中,0和1分别代表道路和墙体,注意四周皆墙
- 靠近边缘随机选取状态为1的道路点,作为起点 a
- 然后将 a 点周围所有的墙体点标记为待检测点,加入到待检测集合
- 从待检测集合随机取一个点 b ,判断顺着它方向的下一个点 c,是否是道路
- 如果是,则将这个待检测点墙体打通,将其移出待检测集合;将下一个点 c作为新的起点,重新执行第3步
- 如果不是就把这个待检测点移出待检测集合,重新作为墙体点
- 不断重复,直到待检测集合全部检查过,重新为空。
-
效果及小结
-
该算法不会出现明显的主路,迷宫相对比较自然,但迷宫的分岔路会比较多
-
-
UE4 实现主要代码
-
头文件
//@本文原创地址 https://www.cnblogs.com/shiroe/p/15506909.html // 按照间隔规则,随机Prim算法生成迷宫 // 此时 roadArr 中,0代表墙,1代表道路点,2代表待确认的道路点,3代表已经确认过的道路点 TArray<TTuple<int32, int32,int32, int32>> CheckCache; //待确定点和其再往外一点的坐标 void Prim_CreateMaze(); void Prim_FindPath(int32 x, int32 y); bool Prim_checkPointValid(int32 x, int32 y, int32 checkState); std::vector<std::vector<int32>> roadArr; //迷宫矩阵 TQueue<TTuple<int32, int32>> taskQueue; //绘制队列
-
CPP
//@本文原创地址 https://www.cnblogs.com/shiroe/p/15506909.html // 创建迷宫 void AMazeCreator::Prim_CreateMaze() { roadArr = std::vector<std::vector<int32>>(width, std::vector<int32>(height, 0)); //初始化 for (int32 i = 0; i < (int32)width; i++) { for (int32 j = 0; j < (int32)height; j++) { UStaticMeshComponent* smComp = NewObject<UStaticMeshComponent>(this); smComp->SetupAttachment(rootComp); smComp->RegisterComponent(); smComp->SetRelativeScale3D(FVector(1.0f)); smComp->SetRelativeLocation(FVector((j - (int32)height / 2) * 100, (i - (int32)width / 2) * 100, 0)); //判断是否可以作为道路点 bool bCanBeRoad = (i % 2) && (j % 2) && i * j != 0 && i != width - 1 && j != height - 1; roadArr[i][j] = (int32)bCanBeRoad; smComp->SetStaticMesh(bCanBeRoad ? PlaneMesh : BlockMesh); roadrMeshs.Add(smComp); } } Prim_FindPath(1, 1); // 搜索 GetWorld()->GetTimerManager().SetTimer(timerHandle,this, &AMazeCreator::ChangeMesh, playRate, true,1); } // 此时 roadArr 中,0代表墙,1代表道路点,2代表待确认的道路点,3代表已经确认过的道路点 void AMazeCreator::Prim_FindPath(int32 x, int32 y) { roadArr[x][y] = 3; taskQueue.Enqueue(TTuple<int32, int32>(x, y)); TArray<int32> offsetX = { -1, 0, 1, 0 }; TArray<int32> offsetY = { 0, -1, 0, 1 }; for (int32 i = 0; i < 4; ++i) { if (Prim_checkPointValid(x + offsetX[i], y + offsetY[i], 0)){ //判断周边一格是否是墙 roadArr[x + offsetX[i]][y + offsetY[i]] = 2; //设为待确定点 CheckCache.Add(TTuple<int32, int32, int32, int32>(x + offsetX[i], y + offsetY[i], x + offsetX[i] * 2, y + offsetY[i] * 2)); //将其放入待确定点集合 } } while (CheckCache.Num() > 0) { int32 idx = UKismetMathLibrary::RandomInteger(CheckCache.Num()); //随机选取待确定点 int32 x1 = CheckCache[idx].Get<0>(); int32 y1 = CheckCache[idx].Get<1>(); int32 x2 = CheckCache[idx].Get<2>(); int32 y2 = CheckCache[idx].Get<3>(); if (Prim_checkPointValid(x2, y2, 1)){ //判断待确定点向外一点是否是道路点 roadArr[x1][y1] = 3; //待确定点转为确定点 taskQueue.Enqueue(TTuple<int32, int32>(x1, y1)); //将该点压入改变mesh队列 CheckCache.Swap(idx, CheckCache.Num() - 1); //与最后一个元素交换,并移出 CheckCache.Pop(); Prim_FindPath(x2, y2); //递归,往外一点作为下次监测点 } else { roadArr[x1][y1] = 0; //待确定点重新转为墙 CheckCache.Swap(idx, CheckCache.Num() - 1); //与最后一个元素交换,并移出 CheckCache.Pop(); } } } bool AMazeCreator::Prim_checkPointValid(int32 x, int32 y, int32 checkState) { return (0 < x && x < (int32)width - 1) && (0 < y && y < (int32)height - 1) && roadArr[x][y] == checkState; }
-
随机Kruskal算法 (并查集)
-
主要步骤
- 创建所有墙的列表(除了四边),并且创建所有单元的集合,每个集合中只包含一个单元。
- 随机从墙的列表中选取一个,取该墙两边分隔的两个单元
- 两个单元属于不同的集合,则将去除当前的墙,把分隔的两个单元连同当前墙三个点作为一个单元集合;并将当前选中的墙移出列表
- 如果属于同一个集合,则直接将当前选中的墙移出列表
- 不断重复第 2 步,直到所有墙都检测过
-
效果及小结
-
该算法同样不会出现明显的主路,岔路也比较多
-
-
UE4 实现主要代码
-
头文件
//@本文原创地址 https://www.cnblogs.com/shiroe/p/15506909.html //随机Kruskal算法 (并查集) TMap<TTuple<int32, int32>, TTuple<int32, int32>> rootArr; //根节点 TMap<TTuple<int32, int32>, int32> rank; //深度 TArray<TTuple<int32, int32>> blockArr; //墙的列表 TTuple<int32, int32>& Kruskal_Find(const TTuple<int32, int32>& coord);//查找根 void Kruskal_Union(const TTuple<int32, int32>& coord1, const TTuple<int32, int32>& coord2); void Kruskal_CreateMaze(); void Kruskal_FindPath(); std::vector<std::vector<int32>> roadArr; //迷宫矩阵 TQueue<TTuple<int32, int32>> taskQueue; //绘制队列
-
CPP
//@本文原创地址 https://www.cnblogs.com/shiroe/p/15506909.html // 创建迷宫 void AMazeCreator::Kruskal_CreateMaze() { roadArr = std::vector<std::vector<int32>>(width, std::vector<int32>(height, 0)); //初始化 for (int32 i = 0; i < (int32)width; i++) { for (int32 j = 0; j < (int32)height; j++) { UStaticMeshComponent* smComp = NewObject<UStaticMeshComponent>(this); smComp->SetupAttachment(rootComp); smComp->RegisterComponent(); smComp->SetRelativeScale3D(FVector(1.0f)); //判断是否可以作为道路点 bool bCanBeRoad = (i % 2) && (j % 2) && i * j != 0 && i != width - 1 && j != height - 1; roadArr[i][j] = (int32)bCanBeRoad; smComp->SetStaticMesh(bCanBeRoad ? PlaneMesh : BlockMesh); smComp->SetRelativeLocation(FVector((j - (int32)height / 2) * 100, (i - (int32)width / 2) * 100, (int32)bCanBeRoad*-50)); roadrMeshs.Add(smComp); //并查集初始化,不含边界 if (i * j != 0 && i != width - 1 && j != height - 1) { TTuple<int, int> coord = TTuple<int, int>(i, j); rootArr.Emplace(coord, coord); //初始化集合,每个集合的根都是自己 rank.Add(coord, 1); if (!bCanBeRoad) { //记录墙体 blockArr.Add(coord); } } } } Kruskal_FindPath(); // 搜索 GetWorld()->GetTimerManager().SetTimer(timerHandle,this, &AMazeCreator::ChangeMesh, playRate, true,1); } void AMazeCreator::Kruskal_FindPath() { while (blockArr.Num()>0) { int32 idx = UKismetMathLibrary::RandomInteger(blockArr.Num()); //随机选取墙 auto coords= blockArr[idx]; int32 x = coords.Get<0>(); int32 y = coords.Get<1>(); //取墙体两边的点 TTuple<int32, int32> p1 = x % 2 ? TTuple<int32, int32>(x, y - 1) : TTuple<int32, int32>(x - 1, y); TTuple<int32, int32> p2 = x % 2 ? TTuple<int32, int32>(x, y + 1) : TTuple<int32, int32>(x + 1, y); if (roadArr[p1.Get<0>()][p1.Get<1>()] == 1 //被墙隔离的两条道路是否相交 && roadArr[p2.Get<0>()][p2.Get<1>()] == 1 && Kruskal_Find(p1) != Kruskal_Find(p2)) { Kruskal_Union(p1, p2); //合并两条无交集的道路 rootArr[coords] = p1; //该墙的根节点也应该变化 roadArr[x][y] = 1; taskQueue.Enqueue(coords); } blockArr.Swap(idx, blockArr.Num() - 1); blockArr.Pop(); } } TTuple<int32, int32>& AMazeCreator::Kruskal_Find(const TTuple<int32, int32>& coord) { if (rootArr[coord] != coord) //路径压缩 rootArr[coord] = Kruskal_Find(rootArr[coord]); return rootArr[coord]; } void AMazeCreator::Kruskal_Union(const TTuple<int32, int32>& coord1, const TTuple<int32, int32>& coord2) { auto& root1 = Kruskal_Find(coord1); auto& root2 = Kruskal_Find(coord2); if (rank[root1] <= rank[root2]) rootArr[root1] = root2; else rootArr[root2] = root1; if (rank[root1] == rank[root2] && root1 != root2) rank[root2]++; }
-
其他参考
作者:砥才人
出处:https://www.cnblogs.com/shiroe
本系列文章为笔者整理原创,只发表在博客园上,欢迎分享本文链接,如需转载,请注明出处!