本次使用的演算法概念是Depth-first search(https://en.wikipedia.org/wiki/Maze_generation_algorithm),也有看到好像叫Growing tree,也就是先亂數從陣列中取一個格子作為起始點,然後從這個起始點開始隨機往四周某一個方向開一個洞到下一格,再從下一格隨機往四周開一個洞過去,一直到四周已經無法開洞或是已經走過的格子就停止,此時倒回上一格在嘗試往另一個方向做同樣的事;雖然講起來有點繁瑣,但也就是一個遞迴的概念了。
實作測試(WebGL build,似乎會載入久一點),點擊Generate產生迷宮,點Enter可以進入隨機位置,用鍵盤的前後左右按鍵移動跟旋轉。
首先假設每一個格子都有四個方向牆壁,這邊使用方形格子,同樣也可以改為六面格或其他類型的格子,因此做一個類別來包裝四個牆壁的數值跟座標,之後再用這個展生一個陣列來用。
private class Cell { //使用一個陣列來設定四個牆的屬性,0是有牆面,1則是打通的通道 public int[] wall = new int[4]; //陣列四個依序為N, E, S, W public int x, y; }
接著需要一些基本的數值,像是陣列的長寬以及陣列的值。
//長寬以及陣列,或是其他的數值,像是方向的權重比,增加權重在亂數選取方向的時候讓該方向機率比較高 // 這樣也可以讓產生出來的迷宮(水平/垂直)的線條比較多 public int width = 10; public int height = 10; private Cell[][] cellArr;
然後就是簡單使用遞迴去跑出路徑,一個一個格子往下檢查,到達死路後開始往回退,一直退到該格子還有其他方向還沒檢查過的,再從那個方向檢查下去,最後這個遞迴會退回到起始格,到此全部的陣列格子就都跑過一遍了。
//概念 void CreatePath(...) { //先把這格加入已經踩過的列表 visitedList.Add(); //接著亂數四個方向,四個方向不重複 int[] direction = new int[4] {a, b, c, d}; //接著開始依序往亂數的四個方向檢查,這個方向必須是牆壁,而且這個方向的下一個格子是還沒踩過的,如果是就跳過 for(int i = 0; i < 4; ++i) { //取得方向 int dir = directions[i]; //檢查是不是牆壁或下一格有沒有踩過 if(isWall = false && visitedList.Contain() = true) { //如果不是牆壁,或是已經踩過,就跳過這個方向 continue; } //打通這個方向的牆壁 start.wall[dir] = 1; //進入這個方向的下一個格子重複相同步驟 CreatePath (nextCell, visitedList); } }
跑完後迷宮也就產生完了,不過這個是用遞迴一次跑完,所以在產生的時候會頓一下,根據陣列的大小停頓的時間也有長短,所以最好是在遊戲建立的時候跑,不然就是使用Coroutine的方法,一格一格跑同時不會卡死其他的運作。
參考Component,不過要注意的是這個Component只是單純產生完Cell[][]的陣列資料,要如何運用這個資料還是需要看自己的需求去建立場景或圖片等等。
public class DepthFirstSearchMaze : MonoBehaviour { private class Cell { public int[] wall = new int[4]; //n,e,s,w 0=wall, 1=passage public int x, y; } public int width = 10; //陣列的寬度 public int height = 10; //陣列的長度 private Cell[][] cellArr; void Start () { CreateMaze(width, height); //建立資料 } void CreateMaze(int width, int height) { //初始化陣列大小 cellArr = new Cell[height][]; for(int i = 0; i < height; ++i) { cellArr[i] = new Cell[width]; for(int j = 0; j < width; ++j) { cellArr[i][j] = new Cell() {y = i, x = j}; } } //亂數取一個格子作為起始點 Cell startCell = cellArr[Random.Range(0,height)][Random.Range(0,width)]; //開始建立迷宮 CreatePath(startCell, new List<Cell>()); } void CreatePath(Cell start, List<Cell> visitedList) { //把此格加入已踩過列表 visitedList.Add(start); //取得可以使用的方向列表,該方向是牆面,並且沒有超出陣列大小 List<int> directions = new List<int>(); if(start.wall[0] == 0 && start.y != height-1) directions.Add(0); if(start.wall[1] == 0 && start.x != width-1) directions.Add(1); if(start.wall[2] == 0 && start.y != 0) directions.Add(2); if(start.wall[3] == 0 && start.x != 0) directions.Add(3); int count = directions.Count; for(int i = 0; i < count; ++i) { //從方向列表亂數取一個方向 int dir = directions[Random.Range(0, directions.Count)]; directions.Remove(dir); //把該方向移出列表 //取得下一個方向的Cell資料 int addY = (dir == 0) ? 1 : (dir == 2) ? -1 : 0; int addX = (dir == 1) ? 1 : (dir == 3) ? -1 : 0; int nextY = start.y + addY; int nextX = start.x + addX; Cell nextCell = cellArr[nextY][nextX]; //檢查是否踩過 if(visitedList.Contains(nextCell)) { continue; //Skip this direction } //建立通道 start.wall[dir] = 1; nextCell.wall[(dir+2)%4] = 1; //到下一個方向的Cell繼續 CreatePath (nextCell, visitedList); } } }
3 comments:
請問生成迷宮牆的GameObject物件要放哪,文章中沒提到GameObject物件要放哪,還有DepthFirstSearchMaze
是要把這段程式放哪裡使用?
我上面有提這個Component只是單純產生迷宮Cell[][]的陣列資料,只是做為參考用的,你可以把裡面一些private的field或method改成public讓外部去呼叫或讀取,或是把你要的功能(例如建立迷宮object)直接加在這個component裡面,這個component沒有包含Instantiate的流程,需要自己去做
Post a Comment