【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]++;
      }
      

其他參考


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM