三大迷宮生成算法 (Maze generation algorithm) -- 深度優先,隨機Prim,遞歸分割

本文主要講解的迷宮生成算法有三種:

1.Recursive backtracker ( 遞歸回溯,也是深度優先算法)

2.Randomized Prim's algorithm(隨機Prim算法,讓我想起了最小生成樹的Prim算法)

3.Recursive division (遞歸分割算法)

首先,爲了方便後續處理,默認的迷宮元素表示爲[x,y,w]

1.我們的迷宮爲常規的矩形,因此可以用二維表示一個迷宮單元, 每個迷宮單元表示爲一個二維數組元素[x,y]。

3.每個迷宮單元包含左上右下四個屬性, 用w表示,分別表示迷宮單元四個面的牆,牆不佔據迷宮單元

下面,我們一一介紹這三種算法:


首先是深度優先(遞歸回溯)算法,這個算法可以表示爲(根據維基百科):

  1. 1.Make the initial cell the current cell and mark it as visited  
  2. 2.While there are unvisited cells  
  3.     1.If the current cell has any neighbours which have not been visited  
  4.         1.Choose randomly one of the unvisited neighbours  
  5.         2.Push the current cell to the stack  
  6.         3.Remove the wall between the current cell and the chosen cell  
  7.         4.Make the chosen cell the current cell and mark it as visited  
  8.     2.Else if stack is not empty  
  9.         1.Pop a cell from the stack  
  10.         2.Make it the current cell  
如果翻譯一下,就是:

  1. 1.將起點作爲當前迷宮單元並標記爲已訪問  
  2. 2.當還存在未標記的迷宮單元,進行循環  
  3.     1.如果當前迷宮單元有未被訪問過的的相鄰的迷宮單元  
  4.         1.隨機選擇一個未訪問的相鄰迷宮單元  
  5.         2.將當前迷宮單元入棧  
  6.         3.移除當前迷宮單元與相鄰迷宮單元的牆  
  7.         4.標記相鄰迷宮單元並用它作爲當前迷宮單元  
  8.     2.如果當前迷宮單元不存在未訪問的相鄰迷宮單元,並且棧不空  
  9.         1.棧頂的迷宮單元出棧  
  10.         2.令其成爲當前迷宮單元  


深度優先構建迷宮的思想就是,每次把新找到的未訪問迷宮單元作爲優先,尋找其相鄰的未訪問過的迷宮單元,直到所有的單元都被訪問到。通俗的說,就是從起點開始隨機走,走不通了就返回上一步,從下一個能走的地方再開始隨機走。一般來說,深度優先法生成的迷宮極度扭曲,有着一條明顯的主路。我們使用python語言+matplotlib生成的20*30的迷宮如圖所示:


我們參考維基百科,使用python語言,深度優先迷宮算法如下,代碼中都已經加了註釋,需要注意的是生成迷宮的算法我們將每個迷宮單元又分成了10*10的着色單元,迷宮單元爲8的單位,作色爲白色,牆寬2個單元,着色爲黑色:

[python] view plain copy
  1. # Code by jollysoul  
  2.   
  3. import random  
  4. import numpy as np  
  5. from matplotlib import pyplot as plt  
  6. import matplotlib.cm as cm  
  7.   
  8. num_rows = int(input("Rows: ")) # number of rows  
  9. num_cols = int(input("Columns: ")) # number of columns  
  10.   
  11. # The array M is going to hold the array information for each cell.  
  12. # The first four coordinates tell if walls exist on those sides   
  13. # and the fifth indicates if the cell has been visited in the search.  
  14. # M(LEFT, UP, RIGHT, DOWN, CHECK_IF_VISITED)  
  15. M = np.zeros((num_rows,num_cols,5), dtype=np.uint8)  
  16.   
  17. # The array image is going to be the output image to display  
  18. image = np.zeros((num_rows*10,num_cols*10), dtype=np.uint8)  
  19.   
  20. # Set starting row and column  
  21. r = 0  
  22. c = 0  
  23. history = [(r,c)] # The history is the stack of visited locations  
  24.   
  25. # Trace a path though the cells of the maze and open walls along the path.  
  26. # We do this with a while loop, repeating the loop until there is no history,   
  27. # which would mean we backtracked to the initial start.  
  28. while history:   
  29.     M[r,c,4] = 1 # designate this location as visited  
  30.     # check if the adjacent cells are valid for moving to  
  31.     check = []  
  32.     if c > 0 and M[r,c-1,4] == 0:  
  33.         check.append('L')    
  34.     if r > 0 and M[r-1,c,4] == 0:  
  35.         check.append('U')  
  36.     if c < num_cols-1 and M[r,c+1,4] == 0:  
  37.         check.append('R')  
  38.     if r < num_rows-1 and M[r+1,c,4] == 0:  
  39.         check.append('D')      
  40.       
  41.     if len(check): # If there is a valid cell to move to.  
  42.         # Mark the walls between cells as open if we move  
  43.         history.append([r,c])  
  44.         move_direction = random.choice(check)  
  45.         if move_direction == 'L':  
  46.             M[r,c,0] = 1  
  47.             c = c-1  
  48.             M[r,c,2] = 1  
  49.         if move_direction == 'U':  
  50.             M[r,c,1] = 1  
  51.             r = r-1  
  52.             M[r,c,3] = 1  
  53.         if move_direction == 'R':  
  54.             M[r,c,2] = 1  
  55.             c = c+1  
  56.             M[r,c,0] = 1  
  57.         if move_direction == 'D':  
  58.             M[r,c,3] = 1  
  59.             r = r+1  
  60.             M[r,c,1] = 1  
  61.     else# If there are no valid cells to move to.  
  62.     # retrace one step back in history if no move is possible  
  63.         r,c = history.pop()  
  64.       
  65.            
  66. # Open the walls at the start and finish  
  67. M[0,0,0] = 1  
  68. M[num_rows-1,num_cols-1,2] = 1  
  69.       
  70. # Generate the image for display  
  71. for row in range(0,num_rows):  
  72.     for col in range(0,num_cols):  
  73.         cell_data = M[row,col]  
  74.         for i in range(10*row+2,10*row+8):  
  75.             image[i,range(10*col+2,10*col+8)] = 255  
  76.         if cell_data[0] == 1:   
  77.             image[range(10*row+2,10*row+8),10*col] = 255  
  78.             image[range(10*row+2,10*row+8),10*col+1] = 255  
  79.         if cell_data[1] == 1:   
  80.             image[10*row,range(10*col+2,10*col+8)] = 255  
  81.             image[10*row+1,range(10*col+2,10*col+8)] = 255  
  82.         if cell_data[2] == 1:   
  83.             image[range(10*row+2,10*row+8),10*col+9] = 255  
  84.             image[range(10*row+2,10*row+8),10*col+8] = 255  
  85.         if cell_data[3] == 1:   
  86.             image[10*row+9,range(10*col+2,10*col+8)] = 255  
  87.             image[10*row+8,range(10*col+2,10*col+8)] = 255  
  88.           
  89.   
  90. # Display the image  
  91. plt.imshow(image, cmap = cm.Greys_r, interpolation='none')  
  92. plt.show()  

然後是隨機Prim,隨機Prim算法生成的迷宮岔路較多,整體上較爲自然而又複雜,算法核心爲(根據維基百科)。

  1. 1.Start with a grid full of walls.  
  2. 2.Pick a cell, mark it as part of the maze. Add the walls of the cell to the wall list.  
  3. 3.While there are walls in the list:  
  4.     1.Pick a random wall from the list. If only one of the two cells that the wall divides is visited, then:  
  5.         2.Make the wall a passage and mark the unvisited cell as part of the maze.  
  6.         3.Add the neighboring walls of the cell to the wall list.  
  7.     2.Remove the wall from the list.  
  8.   
  9. 1.讓迷宮全是牆.  
  10. 2.選一個單元格作爲迷宮的通路,然後把它的鄰牆放入列表  
  11. 3.當列表裏還有牆時  
  12.     1.從列表裏隨機選一個牆,如果這面牆分隔的兩個單元格只有一個單元格被訪問過  
  13.         1.那就從列表裏移除這面牆,即把牆打通,讓未訪問的單元格成爲迷宮的通路  
  14.         2.把這個格子的牆加入列表  
  15.     2.如果牆兩面的單元格都已經被訪問過,那就從列表裏移除這面牆  
在操作過程中,如果把牆放到列表中,比較複雜,維基裏面提到了改進策略:

Although the classical Prim's algorithm keeps a list of edges, for maze generation we could instead maintain a list of adjacent cells. If the randomly chosen cell has multiple edges that connect it to the existing maze, select one of these edges at random. This will tend to branch slightly more than the edge-based version above.

解釋一下就是:我們可以維護一個迷宮單元格的列表,而不是邊的列表。在這個迷宮單元格列表裏面存放了未訪問的單元格,我們在單元格列表中隨機挑選一個單元格,如果這個單元格有多面牆聯繫着已存在的迷宮通路,我們就隨機選擇一面牆打通。這會比基於邊的版本分支稍微多一點。

相對於深度優先的算法,Prim隨機算法不是優先選擇最近選中的單元格,而是隨機的從所有的列表中的單元格進行選擇,新加入的單元格和舊加入的單元格同樣概率會被選擇,新加入的單元格沒有有優先權。因此其分支更多,生成的迷宮更復雜,難度更大,也更自然。生成的迷宮如圖所示:


基於隨機Prim的迷宮生成算法代碼如下:

[python] view plain copy
  1. import random  
  2. import numpy as np  
  3. from matplotlib import pyplot as plt  
  4. import matplotlib.cm as cm  
  5.   
  6. num_rows = int(input("Rows: ")) # number of rows  
  7. num_cols = int(input("Columns: ")) # number of columns  
  8.   
  9. # The array M is going to hold the array information for each cell.  
  10. # The first four coordinates tell if walls exist on those sides   
  11. # and the fifth indicates if the cell has been visited in the search.  
  12. # M(LEFT, UP, RIGHT, DOWN, CHECK_IF_VISITED)  
  13. M = np.zeros((num_rows,num_cols,5), dtype=np.uint8)  
  14.   
  15. # The array image is going to be the output image to display  
  16. image = np.zeros((num_rows*10,num_cols*10), dtype=np.uint8)  
  17.   
  18. # Set starting row and column  
  19. r = 0  
  20. c = 0  
  21. history = [(r,c)] # The history is the stack of visited locations  
  22.   
  23. # Trace a path though the cells of the maze and open walls along the path.  
  24. # We do this with a while loop, repeating the loop until there is no history,   
  25. # which would mean we backtracked to the initial start.  
  26. while history:   
  27.     #random choose a candidata cell from the cell set histroy  
  28.     r,c = random.choice(history)  
  29.     M[r,c,4] = 1 # designate this location as visited  
  30.     history.remove((r,c))  
  31.     check = []  
  32.     # If the randomly chosen cell has multiple edges   
  33.     # that connect it to the existing maze,   
  34.     if c > 0:  
  35.         if M[r,c-1,4] == 1:  
  36.             check.append('L')  
  37.         elif M[r,c-1,4] == 0:  
  38.             history.append((r,c-1))  
  39.             M[r,c-1,4] = 2  
  40.     if r > 0:  
  41.         if M[r-1,c,4] == 1:   
  42.             check.append('U')   
  43.         elif M[r-1,c,4] == 0:  
  44.             history.append((r-1,c))  
  45.             M[r-1,c,4] = 2  
  46.     if c < num_cols-1:  
  47.         if M[r,c+1,4] == 1:   
  48.             check.append('R')  
  49.         elif M[r,c+1,4] == 0:  
  50.             history.append((r,c+1))  
  51.             M[r,c+1,4] = 2   
  52.     if r < num_rows-1:  
  53.         if M[r+1,c,4] == 1:   
  54.             check.append('D')   
  55.         elif  M[r+1,c,4] == 0:  
  56.             history.append((r+1,c))  
  57.             M[r+1,c,4] = 2  
  58.   
  59.     # select one of these edges at random.  
  60.     if len(check):  
  61.         move_direction = random.choice(check)  
  62.         if move_direction == 'L':  
  63.             M[r,c,0] = 1  
  64.             c = c-1  
  65.             M[r,c,2] = 1  
  66.         if move_direction == 'U':  
  67.             M[r,c,1] = 1  
  68.             r = r-1  
  69.             M[r,c,3] = 1  
  70.         if move_direction == 'R':  
  71.             M[r,c,2] = 1  
  72.             c = c+1  
  73.             M[r,c,0] = 1  
  74.         if move_direction == 'D':  
  75.             M[r,c,3] = 1  
  76.             r = r+1  
  77.             M[r,c,1] = 1  
  78.            
  79. # Open the walls at the start and finish  
  80. M[0,0,0] = 1  
  81. M[num_rows-1,num_cols-1,2] = 1  
  82.       
  83. # Generate the image for display  
  84. for row in range(0,num_rows):  
  85.     for col in range(0,num_cols):  
  86.         cell_data = M[row,col]  
  87.         for i in range(10*row+2,10*row+8):  
  88.             image[i,range(10*col+2,10*col+8)] = 255  
  89.         if cell_data[0] == 1:   
  90.             image[range(10*row+2,10*row+8),10*col] = 255  
  91.             image[range(10*row+2,10*row+8),10*col+1] = 255  
  92.         if cell_data[1] == 1:   
  93.             image[10*row,range(10*col+2,10*col+8)] = 255  
  94.             image[10*row+1,range(10*col+2,10*col+8)] = 255  
  95.         if cell_data[2] == 1:   
  96.             image[range(10*row+2,10*row+8),10*col+9] = 255  
  97.             image[range(10*row+2,10*row+8),10*col+8] = 255  
  98.         if cell_data[3] == 1:   
  99.             image[10*row+9,range(10*col+2,10*col+8)] = 255  
  100.             image[10*row+8,range(10*col+2,10*col+8)] = 255  
  101.           
  102.   
  103. # Display the image  
  104. plt.imshow(image, cmap = cm.Greys_r, interpolation='none')  
  105. plt.show()  



最後是遞歸分割,遞歸分割法生成的迷宮較爲簡單,有點像四叉樹,直路多且不扭曲。通俗的說,就是把空間用十字分成四個子空間,然後在三面牆上挖洞(爲了確保連通),之後對每個子空間繼續做這件事直到空間不足以繼續分割爲止。此算法十分高效。


源代碼如下:

[python] view plain copy
  1. # Code by jollysoul  
  2.   
  3. import random  
  4. import numpy as np  
  5. from matplotlib import pyplot as plt  
  6. import matplotlib.cm as cm  
  7.   
  8. #這個函數將當前區域劃分爲四個小區域,並隨機的在三個區域挖洞,  
  9. #讓四個區域彼此聯通,分隔與挖洞點都是隨機生成的。  
  10. def Recursive_division(r1, r2, c1, c2, M, image):  
  11.     if r1 < r2 and c1 < c2:  
  12.         rm = random.randint(r1, r2-1)  
  13.         cm = random.randint(c1, c2-1)  
  14.         cd1 = random.randint(c1,cm)  
  15.         cd2 = random.randint(cm+1,c2)  
  16.         rd1 = random.randint(r1,rm)  
  17.         rd2 = random.randint(rm+1,r2)  
  18.         d = random.randint(1,4)  
  19.         if d == 1:  
  20.             M[rd2, cm, 2] = 1  
  21.             M[rd2, cm+10] = 1  
  22.             M[rm, cd1, 3] = 1  
  23.             M[rm+1, cd1, 1] = 1  
  24.             M[rm, cd2, 3] = 1  
  25.             M[rm+1, cd2, 1] = 1  
  26.         elif d == 2:  
  27.             M[rd1, cm, 2] = 1  
  28.             M[rd1, cm+10] = 1  
  29.             M[rm, cd1, 3] = 1  
  30.             M[rm+1, cd1, 1] = 1  
  31.             M[rm, cd2, 3] = 1  
  32.             M[rm+1, cd2, 1] = 1  
  33.         elif d == 3:  
  34.             M[rd1, cm, 2] = 1  
  35.             M[rd1, cm+10] = 1  
  36.             M[rd2, cm, 2] = 1  
  37.             M[rd2, cm+10] = 1  
  38.             M[rm, cd2, 3] = 1  
  39.             M[rm+1, cd2, 1] = 1  
  40.         elif d == 4:  
  41.             M[rd1, cm, 2] = 1  
  42.             M[rd1, cm+10] = 1  
  43.             M[rd2, cm, 2] = 1  
  44.             M[rd2, cm+10] = 1  
  45.             M[rm, cd1, 3] = 1  
  46.             M[rm+1, cd1, 1] = 1  
  47.   
  48.         Recursive_division(r1, rm, c1, cm, M, image)  
  49.         Recursive_division(r1, rm, cm+1, c2, M, image)  
  50.         Recursive_division(rm+1, r2, cm+1, c2, M, image)  
  51.         Recursive_division(rm+1, r2, c1, cm, M, image)  
  52.   
  53.     elif r1 < r2:  
  54.         rm = random.randint(r1, r2-1)  
  55.         M[rm,c1,3] = 1  
  56.         M[rm+1,c1,1] = 1  
  57.         Recursive_division(r1, rm, c1, c1, M, image)  
  58.         Recursive_division(rm+1, r2, c1, c1, M, image)  
  59.     elif c1 < c2:  
  60.         cm = random.randint(c1,c2-1)  
  61.         M[r1,cm,2] = 1  
  62.         M[r1,cm+1,0] = 1  
  63.         Recursive_division(r1, r1, c1, cm, M, image)  
  64.         Recursive_division(r1, r1, cm+1, c2, M, image)  
  65.   
  66.   
  67. num_rows = int(input("Rows: ")) # number of rows  
  68. num_cols = int(input("Columns: ")) # number of columns  
  69. r1 = 0  
  70. r2 = num_rows-1  
  71. c1 = 0  
  72. c2 = num_cols-1  
  73.   
  74. # The array M is going to hold the array information for each cell.  
  75. # The first four coordinates tell if walls exist on those sides   
  76. # and the fifth indicates if the cell has been visited in the search.  
  77. # M(LEFT, UP, RIGHT, DOWN, CHECK_IF_VISITED)  
  78. M = np.zeros((num_rows,num_cols,5), dtype=np.uint8)  
  79.   
  80. # The array image is going to be the output image to display  
  81. image = np.zeros((num_rows*10,num_cols*10), dtype=np.uint8)  
  82.    
  83. Recursive_division(r1, r2, c1, c2, M, image)    
  84.   
  85. # Open the walls at the start and finish  
  86. M[0,0,0] = 1  
  87. M[num_rows-1,num_cols-1,2] = 1  
  88.       
  89. # Generate the image for display  
  90. for row in range(0,num_rows):  
  91.     for col in range(0,num_cols):  
  92.         cell_data = M[row,col]  
  93.         for i in range(10*row+2,10*row+8):  
  94.             image[i,range(10*col+2,10*col+8)] = 255  
  95.         if cell_data[0] == 1:   
  96.             image[range(10*row+2,10*row+8),10*col] = 255  
  97.             image[range(10*row+2,10*row+8),10*col+1] = 255  
  98.         if cell_data[1] == 1:   
  99.             image[10*row,range(10*col+2,10*col+8)] = 255  
  100.             image[10*row+1,range(10*col+2,10*col+8)] = 255  
  101.         if cell_data[2] == 1:   
  102.             image[range(10*row+2,10*row+8),10*col+9] = 255  
  103.             image[range(10*row+2,10*row+8),10*col+8] = 255  
  104.         if cell_data[3] == 1:   
  105.             image[10*row+9,range(10*col+2,10*col+8)] = 255  
  106.             image[10*row+8,range(10*col+2,10*col+8)] = 255  
  107.           
  108.   
  109. # Display the image  
  110. plt.imshow(image, cmap = cm.Greys_r, interpolation='none')  
  111. plt.show()  


總結來說,這三種算法分別適合不同的迷宮情況,深度優先適合於那種主線支線明顯的遊戲(如RPG),而遞歸分割則適合轉角較少的遊戲(也許是FPS和ACT),至於prim,似乎適合最標準的迷宮遊戲(因爲很難走)。

基本就這些了……


部分參考自:維基百科 以及 http://bbs.9ria.com/thread-156150-1-1.html

轉自:https://blog.csdn.net/juzihongle1/article/details/73135920
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章