算法學習自 作者eastecho 在IndieNova上發表的文章 簡單的使用回溯法生成 Tile Based 迷宮 ,
我只是簡單做了一下Unity的實現。
基礎算法的簡單說明
簡單說明一下算法步驟:
- 先生成基礎地圖。
- 選擇基礎地圖的一個點(一個格子),將它標記爲已訪問過的,並將這個點加入到堆棧中儲存起來。
- 取出堆棧頂的這個點作爲基準點,檢查附近有沒有未訪問過的點。
- 如果附近有未訪問過的點,則隨機選取其中的一個點,將它標記爲已訪問過的,並加入到堆棧中儲存。
- 重複第二步,直到出現一個點,基於這個點在附近找不到未訪問過的點了。
- 將這個堆棧頂部的點移除,基於新的頂部的點重複第二步。
- 當堆棧爲空時,說明迷宮已經生成完畢。
經過以上步驟,我們就可以得到一個完美迷宮,從迷宮中的任何一點都可以到達迷宮中的另外一點。
這個算法就像是忒修斯走米諾斯迷宮,進入迷宮時將毛線頭系在迷宮入口,然後隨意走向能走的地方,直到走到了死衚衕,就順着毛線返回到上一個還能走的地方。
而我們的算法則像是在空地順着路線擺磚塊,直到擺不了磚塊了便返回上一個可以擺磚塊的地方繼續擺磚塊。
適合Tile Based地圖的改進算法
我們先假設我們希望生成的迷宮中有兩種大小相同的部件,牆 和 地面。
牆是不能移動到的部分,地面是玩家可以移動的部分。
假如未被訪問的點是牆,已被訪問的點是地面,我們的算法也可以看作是一個人在佈滿牆的空間裏挖路。
這個時候我們發現,如果使用上一個算法,找到身邊可以挖開的牆壁然後挖開,最後所有的牆壁都會被我們鑿開。
所以我們需要爲我們的牆壁預留空間。
我們稍加改良。
簡單說明一下改良後的算法步驟:
- 先生成基礎地圖。
- 選擇基礎地圖的一個點(一個格子),將它標記爲已訪問過的,並將這個點加入到堆棧中儲存起來。
- 取出堆棧頂的這個點作爲基準點,檢查有沒有與這個點相隔一個格子距離,同時又未訪問過的點。
- 如果有這樣的點,則隨機選取其中的一個點,將它標記爲已訪問過的,並加入到堆棧中儲存。
- 將這兩個點之間的也標記爲已訪問過的點,但不將這個點放入堆棧。所以需注意,這個時候堆棧頂部的點是剛纔檢查到的,距離一個格子的點,而不是附近的這個點。將中間這個點設置爲已訪問的作用是連通當前點和下一個目標點。
- 重複第二步,直到出現一個點,基於這個點在附近距離一個格子的範圍找不到未訪問過的點了。
- 將這個堆棧頂部的點移除,基於新的頂部的點重複第二步。
- 當堆棧爲空時,說明迷宮已經生成完畢。
不同的地方在於查找下一個目標點的時候,跳過了一個格子。最後只要將已訪問過的點設置爲路面,將未訪問過的點設置爲牆面就可以完成迷宮的生成了。
代碼實現
首先我們先說明一下變量
c#
//需要生成地圖的行數
public int row = 35;
//需要生成地圖的列數
public int column = 30;
//生成地圖的基準點
public Vector2 originPoint;
//格子之間的偏移
public float offset;
//地面格子預設
public GameObject floorPrefab;
//牆壁格子預設
public GameObject wallPrefab;
//迷宮的邏輯地圖
private int[,] _maze;
//根據邏輯地圖生成的實際地圖
private GameObject[,] _map;
//儲存目標點的容器
private List<Vector2> _moves = new List<Vector2>();
首先我們初始化邏輯地圖和實際地圖兩個地圖,然後以(0,0)爲起始點開始找尋下一個目標點。即,我們從(0,0)開始挖牆。
c#
void Start()
{
//初始化地形
InitTerrain();
}
void InitTerrain()
{
//初始化邏輯地圖
_maze = new int[row, column];
//初始化實際地圖
_map = new GameObject[row, column];
//以(0,0)爲基準點開始查找目標點生成迷宮
QueryRoad(0, 0);
}
接下來我們來看看關鍵的挖牆部分
c#
void QueryRoad(int r, int c)
{
string dirs = "";
//檢查北面的格子是否被訪問
if ((r - 2 >= 0) && (_maze[r - 2, c] == 0)) dirs += "N";
//檢查西面的格子是否被訪問
if ((c - 2 >= 0) && (_maze[r, c - 2] == 0)) dirs += "W";
//檢查南面的格子是否被訪問
if ((r + 2 < row) && (_maze[r + 2, c] == 0)) dirs += "S";
//檢查東面的格子是否被訪問
if ((c + 2 < column) && (_maze[r, c + 2] == 0)) dirs += "E";
//如果方位爲空,則說明沒有未訪問的格子了
if (dirs == "")
{
//刪除頂上的這個格子
_moves.RemoveAt(_moves.Count - 1);
if (_moves.Count == 0)
{
//如果容器空了,說明迷宮生成完畢,可以開始繪製迷宮了
DrawTerrain();
}
else
{
//否則基於新的點,繼續查找下一個目標點
QueryRoad((int)_moves[_moves.Count - 1].x, (int)_moves[_moves.Count - 1].y);
}
}
else
{
//隨機一個可以被訪問的點
int ran = Random.Range(0, dirs.Length);
char dir = dirs[ran];
//連通目標點和當前點之間的這個點
switch (dir)
{
case 'E':
//將中間這個點設置爲已訪問的
_maze[r, c + 1] = 1;
c = c + 2;
break;
case 'S':
//將中間這個點設置爲已訪問的
_maze[r + 1, c] = 1;
r = r + 2;
break;
case 'W':
//將中間這個點設置爲已訪問的
_maze[r, c - 1] = 1;
c = c - 2;
break;
case 'N':
//將中間這個點設置爲已訪問的
_maze[r - 1, c] = 1;
r = r - 2;
break;
}
//將這個新的目標點設置爲已訪問的
_maze[r, c] = 1;
//將這個新的目標點加入容器
_moves.Add(new Vector2(r, c));
//基於新的點,繼續查找下一個目標點
QueryRoad(r, c);
}
}
算法原理和之前敘述的是一樣的。
最後只需要將實際地圖根據邏輯地圖繪製出來就好了。
c#
void DrawTerrain()
{
for (int i = 0; i < row; i++)
{
for (int j = 0; j < column; j++)
{
switch (_maze[i, j])
{
case 1:
if (_map[i, j] != null)
{
if (_map[i, j].tag == "Floor")
{
continue;
}else if (_map[i, j].tag == "Wall")
{
Destroy(_map[i, j]);
_map[i, j] = null;
}
}
_map[i, j] = Instantiate(floorPrefab, originPoint + new Vector2(j * offset, i * offset), Quaternion.identity);
break;
case 0:
if (_map[i, j] != null)
{
if (_map[i, j].tag == "Wall")
{
continue;
}
else if (_map[i, j].tag == "Floor")
{
Destroy(_map[i, j]);
_map[i, j] = null;
}
}
_map[i, j] = Instantiate(wallPrefab, originPoint + new Vector2(j * offset, i * offset), Quaternion.identity);
break;
}
}
}
}
如何讓繪製迷宮的過程可見
迷宮一下就出現難免有些無趣,畢竟看着空無一物的地圖逐漸被道路充滿也是一種樂趣。我真心覺得看着迷宮漸漸生成非常令人愉悅。所以提供一種讓迷宮的生成可見的方法。
c#
//用來儲存目標點
private Vector2 _currentPoint;
這是關鍵變量,將之前的遞歸改爲一幀一次,相當於每一幀挖一塊牆,獲取到了新的目標點之後不會立刻找下一個,而是等到下一幀再執行。
記得增加執行條件,以免無限執行查找。
我這裏設置成如果這個點是(-1,-1)則不執行查找。當我在繪製完成最後的實際地圖後會將這個點設置成(-1,-1)。
那麼首先我們對初始化的部分進行一點修改。
c#
void InitTerrain()
{
_maze = new int[row, column];
_map = new GameObject[row, column];
//初始化時先將目標點設置爲(0,0)
_currentPoint = new Vector2(0, 0);
}
然後我們每一幀根據當前的目標點查找下一個目標點。
c#
void Update()
{
//如果當前目標點_currentPoint是合法的話就查找下一個目標點
if (_currentPoint != new Vector2(-1, -1))
{
QueryRoad((int) _currentPoint.x, (int) _currentPoint.y);
}
}
然後我們的查找目標點也從遞歸改成僅僅查找下一個目標點。
c#
//修改查找部分,取消遞歸。
void QueryRoad(int x, int y)
{
string dirs = "";
if ((x - 2 >= 0) && (_maze[x - 2, y] == 0)) dirs += "N";
if ((y - 2 >= 0) && (_maze[x, y - 2] == 0)) dirs += "W";
if ((x + 2 < row) && (_maze[x + 2, y] == 0)) dirs += "S";
if ((y + 2 < column) && (_maze[x, y + 2] == 0)) dirs += "E";
if (dirs == "")
{
_moves.RemoveAt(_moves.Count - 1);
if (_moves.Count == 0)
{
DrawTerrain();
//這是最後一個點了,這個點以外已經沒有符合條件的點,所以在這次繪製完畢後將不再執行查找。
_currentPoint = new Vector2(-1, -1);
}
else
{
//這裏取消遞歸,改爲設置目標點,下一幀再處理這個目標點
//QueryRoad((int) _moves[_moves.Count - 1].x, (int) _moves[_moves.Count - 1].y);
//因爲這個目標點附近已經沒有符合要求的點了,所以找到上一個點,並將其設置爲新的目標基點。
_currentPoint = _moves[_moves.Count - 1];
}
}
else
{
int ran = Random.Range(0, dirs.Length);
char dir = dirs[ran];
switch (dir)
{
case 'E':
_maze[x, y + 1] = 1;
y = y + 2;
break;
case 'S':
_maze[x + 1, y] = 1;
x = x + 2;
break;
case 'W':
_maze[x, y - 1] = 1;
y = y - 2;
break;
case 'N':
_maze[x - 1, y] = 1;
x = x - 2;
break;
}
_maze[x, y] = 1;
_moves.Add(new Vector2(x, y));
//這裏依然取消遞歸
//QueryRoad(x, y);
//這裏將目標基點_currentPoint設置爲新的目標點。
_currentPoint = new Vector2(x, y);
//每一次找到了新的目標點都要繪製一次實際地圖
DrawTerrain();
}
}
每一次找到了新的目標點都要繪製一次實際地圖,這樣才能把你挖通的道路顯示出來。
下一步就是繪製地圖部分,這一部分沒有什麼變化。
c#
void DrawTerrain()
{
for (int i = 0; i < row; i++)
{
for (int j = 0; j < column; j++)
{
switch (_maze[i, j])
{
case 1:
if (_map[i, j] != null)
{
if (_map[i, j].tag == "Floor")
{
continue;
}else if (_map[i, j].tag == "Wall")
{
Destroy(_map[i, j]);
_map[i, j] = null;
}
}
_map[i, j] = Instantiate(floorPrefab, originPoint + new Vector2(j * offset, i * offset), Quaternion.identity);
break;
case 0:
if (_map[i, j] != null)
{
if (_map[i, j].tag == "Wall")
{
continue;
}
else if (_map[i, j].tag == "Floor")
{
Destroy(_map[i, j]);
_map[i, j] = null;
}
}
_map[i, j] = Instantiate(wallPrefab, originPoint + new Vector2(j * offset, i * offset), Quaternion.identity);
break;
}
}
}
}
最後
大家可以自己嘗試一下這種生成迷宮的方法。
如果大家有什麼問題或者指教,也歡迎來與我交流。