本文主要講解的迷宮生成算法有三種:
1.Recursive backtracker ( 遞歸回溯,也是深度優先算法)
2.Randomized Prim's algorithm(隨機Prim算法,讓我想起了最小生成樹的Prim算法)
3.Recursive division (遞歸分割算法)
首先,爲了方便後續處理,默認的迷宮元素表示爲[x,y,w]:
1.我們的迷宮爲常規的矩形,因此可以用二維表示一個迷宮單元, 每個迷宮單元表示爲一個二維數組元素[x,y]。
3.每個迷宮單元包含左上右下四個屬性, 用w表示,分別表示迷宮單元四個面的牆,牆不佔據迷宮單元。
下面,我們一一介紹這三種算法:
首先是深度優先(遞歸回溯)算法,這個算法可以表示爲(根據維基百科):
- 1.Make the initial cell the current cell and mark it as visited
- 2.While there are unvisited cells
- 1.If the current cell has any neighbours which have not been visited
- 1.Choose randomly one of the unvisited neighbours
- 2.Push the current cell to the stack
- 3.Remove the wall between the current cell and the chosen cell
- 4.Make the chosen cell the current cell and mark it as visited
- 2.Else if stack is not empty
- 1.Pop a cell from the stack
- 2.Make it the current cell
- 1.將起點作爲當前迷宮單元並標記爲已訪問
- 2.當還存在未標記的迷宮單元,進行循環
- 1.如果當前迷宮單元有未被訪問過的的相鄰的迷宮單元
- 1.隨機選擇一個未訪問的相鄰迷宮單元
- 2.將當前迷宮單元入棧
- 3.移除當前迷宮單元與相鄰迷宮單元的牆
- 4.標記相鄰迷宮單元並用它作爲當前迷宮單元
- 2.如果當前迷宮單元不存在未訪問的相鄰迷宮單元,並且棧不空
- 1.棧頂的迷宮單元出棧
- 2.令其成爲當前迷宮單元
我們參考維基百科,使用python語言,深度優先迷宮算法如下,代碼中都已經加了註釋,需要注意的是生成迷宮的算法我們將每個迷宮單元又分成了10*10的着色單元,迷宮單元爲8的單位,作色爲白色,牆寬2個單元,着色爲黑色:
- # Code by jollysoul
- import random
- import numpy as np
- from matplotlib import pyplot as plt
- import matplotlib.cm as cm
- num_rows = int(input("Rows: ")) # number of rows
- num_cols = int(input("Columns: ")) # number of columns
- # The array M is going to hold the array information for each cell.
- # The first four coordinates tell if walls exist on those sides
- # and the fifth indicates if the cell has been visited in the search.
- # M(LEFT, UP, RIGHT, DOWN, CHECK_IF_VISITED)
- M = np.zeros((num_rows,num_cols,5), dtype=np.uint8)
- # The array image is going to be the output image to display
- image = np.zeros((num_rows*10,num_cols*10), dtype=np.uint8)
- # Set starting row and column
- r = 0
- c = 0
- history = [(r,c)] # The history is the stack of visited locations
- # Trace a path though the cells of the maze and open walls along the path.
- # We do this with a while loop, repeating the loop until there is no history,
- # which would mean we backtracked to the initial start.
- while history:
- M[r,c,4] = 1 # designate this location as visited
- # check if the adjacent cells are valid for moving to
- check = []
- if c > 0 and M[r,c-1,4] == 0:
- check.append('L')
- if r > 0 and M[r-1,c,4] == 0:
- check.append('U')
- if c < num_cols-1 and M[r,c+1,4] == 0:
- check.append('R')
- if r < num_rows-1 and M[r+1,c,4] == 0:
- check.append('D')
- if len(check): # If there is a valid cell to move to.
- # Mark the walls between cells as open if we move
- history.append([r,c])
- move_direction = random.choice(check)
- if move_direction == 'L':
- M[r,c,0] = 1
- c = c-1
- M[r,c,2] = 1
- if move_direction == 'U':
- M[r,c,1] = 1
- r = r-1
- M[r,c,3] = 1
- if move_direction == 'R':
- M[r,c,2] = 1
- c = c+1
- M[r,c,0] = 1
- if move_direction == 'D':
- M[r,c,3] = 1
- r = r+1
- M[r,c,1] = 1
- else: # If there are no valid cells to move to.
- # retrace one step back in history if no move is possible
- r,c = history.pop()
- # Open the walls at the start and finish
- M[0,0,0] = 1
- M[num_rows-1,num_cols-1,2] = 1
- # Generate the image for display
- for row in range(0,num_rows):
- for col in range(0,num_cols):
- cell_data = M[row,col]
- for i in range(10*row+2,10*row+8):
- image[i,range(10*col+2,10*col+8)] = 255
- if cell_data[0] == 1:
- image[range(10*row+2,10*row+8),10*col] = 255
- image[range(10*row+2,10*row+8),10*col+1] = 255
- if cell_data[1] == 1:
- image[10*row,range(10*col+2,10*col+8)] = 255
- image[10*row+1,range(10*col+2,10*col+8)] = 255
- if cell_data[2] == 1:
- image[range(10*row+2,10*row+8),10*col+9] = 255
- image[range(10*row+2,10*row+8),10*col+8] = 255
- if cell_data[3] == 1:
- image[10*row+9,range(10*col+2,10*col+8)] = 255
- image[10*row+8,range(10*col+2,10*col+8)] = 255
- # Display the image
- plt.imshow(image, cmap = cm.Greys_r, interpolation='none')
- plt.show()
然後是隨機Prim,隨機Prim算法生成的迷宮岔路較多,整體上較爲自然而又複雜,算法核心爲(根據維基百科)。
- 1.Start with a grid full of walls.
- 2.Pick a cell, mark it as part of the maze. Add the walls of the cell to the wall list.
- 3.While there are walls in the list:
- 1.Pick a random wall from the list. If only one of the two cells that the wall divides is visited, then:
- 2.Make the wall a passage and mark the unvisited cell as part of the maze.
- 3.Add the neighboring walls of the cell to the wall list.
- 2.Remove the wall from the list.
- 1.讓迷宮全是牆.
- 2.選一個單元格作爲迷宮的通路,然後把它的鄰牆放入列表
- 3.當列表裏還有牆時
- 1.從列表裏隨機選一個牆,如果這面牆分隔的兩個單元格只有一個單元格被訪問過
- 1.那就從列表裏移除這面牆,即把牆打通,讓未訪問的單元格成爲迷宮的通路
- 2.把這個格子的牆加入列表
- 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的迷宮生成算法代碼如下:
- import random
- import numpy as np
- from matplotlib import pyplot as plt
- import matplotlib.cm as cm
- num_rows = int(input("Rows: ")) # number of rows
- num_cols = int(input("Columns: ")) # number of columns
- # The array M is going to hold the array information for each cell.
- # The first four coordinates tell if walls exist on those sides
- # and the fifth indicates if the cell has been visited in the search.
- # M(LEFT, UP, RIGHT, DOWN, CHECK_IF_VISITED)
- M = np.zeros((num_rows,num_cols,5), dtype=np.uint8)
- # The array image is going to be the output image to display
- image = np.zeros((num_rows*10,num_cols*10), dtype=np.uint8)
- # Set starting row and column
- r = 0
- c = 0
- history = [(r,c)] # The history is the stack of visited locations
- # Trace a path though the cells of the maze and open walls along the path.
- # We do this with a while loop, repeating the loop until there is no history,
- # which would mean we backtracked to the initial start.
- while history:
- #random choose a candidata cell from the cell set histroy
- r,c = random.choice(history)
- M[r,c,4] = 1 # designate this location as visited
- history.remove((r,c))
- check = []
- # If the randomly chosen cell has multiple edges
- # that connect it to the existing maze,
- if c > 0:
- if M[r,c-1,4] == 1:
- check.append('L')
- elif M[r,c-1,4] == 0:
- history.append((r,c-1))
- M[r,c-1,4] = 2
- if r > 0:
- if M[r-1,c,4] == 1:
- check.append('U')
- elif M[r-1,c,4] == 0:
- history.append((r-1,c))
- M[r-1,c,4] = 2
- if c < num_cols-1:
- if M[r,c+1,4] == 1:
- check.append('R')
- elif M[r,c+1,4] == 0:
- history.append((r,c+1))
- M[r,c+1,4] = 2
- if r < num_rows-1:
- if M[r+1,c,4] == 1:
- check.append('D')
- elif M[r+1,c,4] == 0:
- history.append((r+1,c))
- M[r+1,c,4] = 2
- # select one of these edges at random.
- if len(check):
- move_direction = random.choice(check)
- if move_direction == 'L':
- M[r,c,0] = 1
- c = c-1
- M[r,c,2] = 1
- if move_direction == 'U':
- M[r,c,1] = 1
- r = r-1
- M[r,c,3] = 1
- if move_direction == 'R':
- M[r,c,2] = 1
- c = c+1
- M[r,c,0] = 1
- if move_direction == 'D':
- M[r,c,3] = 1
- r = r+1
- M[r,c,1] = 1
- # Open the walls at the start and finish
- M[0,0,0] = 1
- M[num_rows-1,num_cols-1,2] = 1
- # Generate the image for display
- for row in range(0,num_rows):
- for col in range(0,num_cols):
- cell_data = M[row,col]
- for i in range(10*row+2,10*row+8):
- image[i,range(10*col+2,10*col+8)] = 255
- if cell_data[0] == 1:
- image[range(10*row+2,10*row+8),10*col] = 255
- image[range(10*row+2,10*row+8),10*col+1] = 255
- if cell_data[1] == 1:
- image[10*row,range(10*col+2,10*col+8)] = 255
- image[10*row+1,range(10*col+2,10*col+8)] = 255
- if cell_data[2] == 1:
- image[range(10*row+2,10*row+8),10*col+9] = 255
- image[range(10*row+2,10*row+8),10*col+8] = 255
- if cell_data[3] == 1:
- image[10*row+9,range(10*col+2,10*col+8)] = 255
- image[10*row+8,range(10*col+2,10*col+8)] = 255
- # Display the image
- plt.imshow(image, cmap = cm.Greys_r, interpolation='none')
- plt.show()
最後是遞歸分割,遞歸分割法生成的迷宮較爲簡單,有點像四叉樹,直路多且不扭曲。通俗的說,就是把空間用十字分成四個子空間,然後在三面牆上挖洞(爲了確保連通),之後對每個子空間繼續做這件事直到空間不足以繼續分割爲止。此算法十分高效。
源代碼如下:
- # Code by jollysoul
- import random
- import numpy as np
- from matplotlib import pyplot as plt
- import matplotlib.cm as cm
- #這個函數將當前區域劃分爲四個小區域,並隨機的在三個區域挖洞,
- #讓四個區域彼此聯通,分隔與挖洞點都是隨機生成的。
- def Recursive_division(r1, r2, c1, c2, M, image):
- if r1 < r2 and c1 < c2:
- rm = random.randint(r1, r2-1)
- cm = random.randint(c1, c2-1)
- cd1 = random.randint(c1,cm)
- cd2 = random.randint(cm+1,c2)
- rd1 = random.randint(r1,rm)
- rd2 = random.randint(rm+1,r2)
- d = random.randint(1,4)
- if d == 1:
- M[rd2, cm, 2] = 1
- M[rd2, cm+1, 0] = 1
- M[rm, cd1, 3] = 1
- M[rm+1, cd1, 1] = 1
- M[rm, cd2, 3] = 1
- M[rm+1, cd2, 1] = 1
- elif d == 2:
- M[rd1, cm, 2] = 1
- M[rd1, cm+1, 0] = 1
- M[rm, cd1, 3] = 1
- M[rm+1, cd1, 1] = 1
- M[rm, cd2, 3] = 1
- M[rm+1, cd2, 1] = 1
- elif d == 3:
- M[rd1, cm, 2] = 1
- M[rd1, cm+1, 0] = 1
- M[rd2, cm, 2] = 1
- M[rd2, cm+1, 0] = 1
- M[rm, cd2, 3] = 1
- M[rm+1, cd2, 1] = 1
- elif d == 4:
- M[rd1, cm, 2] = 1
- M[rd1, cm+1, 0] = 1
- M[rd2, cm, 2] = 1
- M[rd2, cm+1, 0] = 1
- M[rm, cd1, 3] = 1
- M[rm+1, cd1, 1] = 1
- Recursive_division(r1, rm, c1, cm, M, image)
- Recursive_division(r1, rm, cm+1, c2, M, image)
- Recursive_division(rm+1, r2, cm+1, c2, M, image)
- Recursive_division(rm+1, r2, c1, cm, M, image)
- elif r1 < r2:
- rm = random.randint(r1, r2-1)
- M[rm,c1,3] = 1
- M[rm+1,c1,1] = 1
- Recursive_division(r1, rm, c1, c1, M, image)
- Recursive_division(rm+1, r2, c1, c1, M, image)
- elif c1 < c2:
- cm = random.randint(c1,c2-1)
- M[r1,cm,2] = 1
- M[r1,cm+1,0] = 1
- Recursive_division(r1, r1, c1, cm, M, image)
- Recursive_division(r1, r1, cm+1, c2, M, image)
- num_rows = int(input("Rows: ")) # number of rows
- num_cols = int(input("Columns: ")) # number of columns
- r1 = 0
- r2 = num_rows-1
- c1 = 0
- c2 = num_cols-1
- # The array M is going to hold the array information for each cell.
- # The first four coordinates tell if walls exist on those sides
- # and the fifth indicates if the cell has been visited in the search.
- # M(LEFT, UP, RIGHT, DOWN, CHECK_IF_VISITED)
- M = np.zeros((num_rows,num_cols,5), dtype=np.uint8)
- # The array image is going to be the output image to display
- image = np.zeros((num_rows*10,num_cols*10), dtype=np.uint8)
- Recursive_division(r1, r2, c1, c2, M, image)
- # Open the walls at the start and finish
- M[0,0,0] = 1
- M[num_rows-1,num_cols-1,2] = 1
- # Generate the image for display
- for row in range(0,num_rows):
- for col in range(0,num_cols):
- cell_data = M[row,col]
- for i in range(10*row+2,10*row+8):
- image[i,range(10*col+2,10*col+8)] = 255
- if cell_data[0] == 1:
- image[range(10*row+2,10*row+8),10*col] = 255
- image[range(10*row+2,10*row+8),10*col+1] = 255
- if cell_data[1] == 1:
- image[10*row,range(10*col+2,10*col+8)] = 255
- image[10*row+1,range(10*col+2,10*col+8)] = 255
- if cell_data[2] == 1:
- image[range(10*row+2,10*row+8),10*col+9] = 255
- image[range(10*row+2,10*row+8),10*col+8] = 255
- if cell_data[3] == 1:
- image[10*row+9,range(10*col+2,10*col+8)] = 255
- image[10*row+8,range(10*col+2,10*col+8)] = 255
- # Display the image
- plt.imshow(image, cmap = cm.Greys_r, interpolation='none')
- plt.show()
總結來說,這三種算法分別適合不同的迷宮情況,深度優先適合於那種主線支線明顯的遊戲(如RPG),而遞歸分割則適合轉角較少的遊戲(也許是FPS和ACT),至於prim,似乎適合最標準的迷宮遊戲(因爲很難走)。
基本就這些了……
部分參考自:維基百科 以及 http://bbs.9ria.com/thread-156150-1-1.html