本次使用的演算法概念是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