【UE4 C++】迷宫生成——DFS、Prim、Kruskal算法实现


DFS 算法

  • 主要步骤

    1. 初始化大地图,只有0和1的状态。其中,0和1分别代表道路和墙体,注意四周皆墙
    2. 靠近边缘随机选取状态为1的道路点,作为起点 a
    3. 在起点 a 的上下左右四个方向,跨两个寻找同样为1的道路点 c
      1. 如果找到,则将它们之间的墙体 b 打通,然后将 c 作为新的起点,然后继续进行第2步
      2. 如果四个方向都没有找到,则不断回退到上一点,直到找到有一点其他方向满足条件,然后继续查询
    4. 当查无可查的时候,迷宫也就填充完毕了

  • 效果

    • 这种算法生成的迷宫会有比较明显的主路

  • 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算法

  • 主要步骤

    1. 初始化大地图,只有0和1的状态。其中,0和1分别代表道路和墙体,注意四周皆墙
    2. 靠近边缘随机选取状态为1的道路点,作为起点 a
    3. 然后将 a 点周围所有的墙体点标记为待检测点,加入到待检测集合
    4. 从待检测集合随机取一个点 b ,判断顺着它方向的下一个点 c,是否是道路
      1. 如果是,则将这个待检测点墙体打通,将其移出待检测集合;将下一个点 c作为新的起点,重新执行第3步
      2. 如果不是就把这个待检测点移出待检测集合,重新作为墙体点
    5. 不断重复,直到待检测集合全部检查过,重新为空。

  • 效果及小结

    • 该算法不会出现明显的主路,迷宫相对比较自然,但迷宫的分岔路会比较多

  • 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算法 (并查集)

  • 主要步骤

    1. 创建所有墙的列表(除了四边),并且创建所有单元的集合,每个集合中只包含一个单元。
    2. 随机从墙的列表中选取一个,取该墙两边分隔的两个单元
      1. 两个单元属于不同的集合,则将去除当前的墙,把分隔的两个单元连同当前墙三个点作为一个单元集合;并将当前选中的墙移出列表
      2. 如果属于同一个集合,则直接将当前选中的墙移出列表
    3. 不断重复第 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]++;
      }
      

其他参考

posted @ 2021-11-04 10:02  砥才人  阅读(3364)  评论(0编辑  收藏  举报