在塔防遊戲中,有許多敵人向著同一目標前進。在很多塔防遊戲當中,有一條或幾條事先預定好的路徑。在一些中,比如經典的《Desktop Tower Defense》,你可以將塔放在任何位置,它們充當障礙影響敵人選擇的路徑。試一試,點選地圖來移動牆壁:
(原文中的可播放的動畫,點選跳轉檢視)
我們如何來實現這種效果?
像A*這樣的圖搜尋演算法經常被用來尋找兩點之間的最短路徑。你可以用這個來為每一個敵人找到前往目標的路徑。在這種型別的遊戲當中,我們有很多不同的圖搜尋演算法來。這是一些經典方法
單源,單目標:
單源多目標或多源單目標
- 廣度優先演算法-無加權邊
- Dijkstra演算法-有加權邊
- Bellman-Ford演算法-支援負權重
多源多目標
像《Desktop Tower Defense》這樣的遊戲會有很多個敵人(源)和一個共同的目的地。這使得它被歸為多源單目標一類。我們可以執行一個演算法,一次算出所有敵人的路徑,而不是為每個敵人執行一次A*演算法。更好的是,我們可以計算出每個位置的最短路徑,所以當敵人擠在一塊或者新敵人被建立時,他們的路徑已經被計算好了。
我們先來看看有時也被稱作“洪水填充法”(FIFO變種)的廣度優先演算法。雖然圖搜尋演算法是適用於任何由節點和邊構成的圖,但是我還是使用方形網格來表示這些例子。網格是圖的一個特例。每個網格瓦片是圖節點,網格瓷磚之間的邊界是圖的邊。我會在另一篇文章當中探討非網格圖。
廣度優先搜尋始於一個節點,並訪問鄰居節點。關鍵的概念是“邊界”,在已探索和未開發的區域之間的邊界。邊界從原始節點向外擴充套件,直到探索了整張圖。
邊界佇列是一個圖節點(網格瓦片)是否需要被分析的列表/陣列。它最開始僅僅包含一個元素,起始節點。每個節點上的訪問標誌追蹤我們是否採訪過該節點。開始的時候除了起始節點都標誌為FALSE。使用滑塊來檢視邊界是如何擴充套件的:
(原文中的可播放的動畫,點選跳轉檢視)
這個演算法是如何工作的?在每一步,獲得一個元素的邊界並把它命名為current。然後尋找current的每個鄰居,next。如果他們還沒有被訪問過的話,將他們都新增到邊界佇列裡面。下面是一些python程式碼:
1 2 3 4 5 6 7 8 9 10 11 |
frontier = Queue() frontier.put(start) visited = {} visited[start] = True while not frontier.empty(): current = frontier.get() for next in graph.neighbors(current): if next not in visited: frontier.put(next) visited[next] = True |
現在已經看見程式碼了,試著步進上面的動畫。注意邊界佇列,關於current的程式碼,還有next節點的集合。在每一步,有一個邊界元素成為current節點,它的鄰居節點會被標註,並且未被拜訪過的鄰居節點會被新增到邊界佇列。有一些鄰居節點可能已經被訪問過,他們就不需要被新增到邊界佇列裡面了。
這是一個相對簡單的演算法,並且對於包括AI在內的很多事情都是有用的。我有三種主要使用它的辦法:
1.標識所有可達的點。這在你的圖不是完全連線的,並且想知道哪些點是可達的時候是很有用的。這就是我再上面用visited這部分所做的。
2.尋找從一個點到所有其他點或者所有點到一個點的路徑。我在文章開始部分的動畫demo裡面使用了它。
3.測量從一個點到所有其他點的距離。這在想知道一個移動中的怪物的距離時是很有用的。
如果你正在生成路徑,你可能會想知道從每個點移動的方向。當你訪問一個鄰居節點的時候,要記得你是從哪個節點過來的。讓我們把visited重新命名為came_from並且用它來儲存之前位置的軌跡:
1 2 3 4 5 6 7 8 9 10 11 |
frontier = Queue() frontier.put(start) came_from = {} came_from[start] = None while not frontier.empty(): current = frontier.get() for next in graph.neighbors(current): if next not in came_from: frontier.put(next) came_from[next] = current |
我們來看看它看起來是怎樣的:
(原文中的可播放的動畫,點選跳轉檢視)
如果你需要距離,你可以在起始節點講一個計數器設定為0,並在每次訪問鄰居節點的時候將它加一。讓我們把visitd重新命名為distance,並且用它來儲存一個計數器:
1 2 3 4 5 6 7 8 9 10 11 |
frontier = Queue() frontier.put(start) distance = {} distance[start] = 0 while not frontier.empty(): current = frontier.get() for next in graph.neighbors(current): if next not in distance: frontier.put(next) distance[next] = 1 + distance[current] |
我們來看看它看起來是怎樣的:
(原文中的可播放的動畫,點選跳轉檢視)
如果你想同時計算路徑和距離,你可以使用兩個變數。
這就是廣度優先檢索演算法。對於塔防風格的遊戲,我用它來計算所有位置到一個指定位置的路徑,而不是重複使用A*演算法為每個敵人分開計算路徑。我用它來尋找一個怪物指定行動距離內所有的位置。我也是用它來進行程式化的地圖生成。Minecraft使用它來進行可見性提出。由此可見這是一個不錯的演算法。
下一步
- 我有python和c++程式碼的實現。
- 如果你想要找到從一個點出發而不是到達一個點的路徑,只需要在檢索路徑的時候翻轉came_from指標。
- 如果你想要知道一些點而不是一個點的路徑,你可以在圖的邊緣為你的每個目標點新增一個額外的點。額外的點不會出現在網格中,但是它會表示在圖中的目標位置。
- 提前退出:如果你是在尋找一個到達某一點或從某一點出發,。我在A*演算法的文章當中描述了這種情況。
- 加權邊:如果你需要不同的移動成本,廣度優先搜尋可以替換為為Dijkstra演算法。我在A*演算法的文章當中描述了這種情況。
- 啟發:如果你需要新增一種指導尋找目標的方法,廣度優先演算法可以替換為最佳優先演算法。我在A*演算法的文章當中描述了這種情況。
- 如果你從廣度優先演算法,並且加上了提前退出,加權邊和啟發,你會得到A*。如你所想,我在A*演算法的文章當中描述了這種情況。