塔防中的路徑查找

原文鏈接:http://www.redblobgames.com/pathfinding/tower-defense/

在塔防遊戲中,有許多敵人都把矛頭指向同一個地方。在許多塔防遊戲中,有的是預先定下的路徑,或者可數的幾條路徑.其中一些,像經典的桌面塔防,你可以放置塔們在任何地方,它們扮演着障礙物的角色去影響殺來的敵人.試試看,點擊地圖切換出牆壁來看看效果:

我們該怎樣實現這個呢?

圖搜索算法像A*,常常被用在查找從一個點到另一個點的最短路徑.你可以用這個算法到每個敵人身上去查找到達目標的路徑.有大量的不同的可供使用的圖搜索算法在典型的遊戲中.典型的有:

  1. One source, one destination(一源一目標):
  2. One source, all destinations(一源多目標), or all sources, one destination(多源一目標):
  3. All sources, all destinations(多源多目標):

像桌面塔防遊戲中,擁有大量的敵人點(源點)以及一個它們的共同目標點.這樣的情形剛好匹配多源一目標策略算法.不採用A*算法給每個敵人進行路徑計算,我們可以運行一個算法一次,就可以計算出所有敵人想要的路徑了.甚至更好的化,可以給每一個位置計算出最短路徑,所以當敵人前仆後繼着好多或者新的敵人創建的時候,它們移動的路徑早已經被計算出來了.

讓我們探索下廣度優先搜索,有時候也稱爲"泛洪"(FIFO變體).雖然圖搜索可以工作在任何節點-邊緣的圖中,我這裏例子中用的是方格表.表格是圖中特別的案例.每一個網格瓷磚都是一個圖節點,瓷磚間的邊界就是圖的邊.我將在其他文章中探索非表格圖的應用.

廣度優先搜索從一個點出發並且重複訪問鄰居節點們.關鍵的概念是前驅隊列(“frontier”),位於訪問過的跟未訪問過的區域中間.前驅隊列從起始點開始朝外擴張,直到探索完整個圖.

前驅隊列是前驅:圖的一個數組或者列表中的節點需要被分析.從只包含一個單個元素(起始節點)開始.在每一個節點上的"訪問過標記"從我們看到後就一直保持跟蹤記錄.開始時所有的一切都是False狀態(未訪問).通過滑塊來觀察前驅隊列的擴張:


這個算法是怎麼工作的呢?在每一步中,從前驅隊列中取出來一個節點,稱它爲當前節點.然後查看每個當前節點的鄰居節點(變量名叫next).如果之前沒有訪問過的化就把現在這個next節點添加到前驅隊列中去.這裏是對應的python代碼:

  1. frontier = Queue()
  2. frontier.put(start)
  3. visited = {}
  4. visited[start] = True
  5. while not frontier.empty():
  6.   current = frontier.get()
  7.   for next in graph.neighbors(current):
  8.      if next not in visited:
  9.         frontier.put(next)
  10.         visited[next] = True

現在你已經看過代碼了,嘗試一步一步前進動畫.注意前驅隊列的變化情況,當前節點,以及鄰居節點(next nodes).在每一步中,前驅隊列中的一個元素變成當前節點,鄰居節點被打上標記,其他的未訪問的鄰居加入到前驅隊列中.一些鄰居如果已經被訪問過就不需要加入到前驅隊列中了.

它是相對簡單的算法,對於所有有序的東西都是有用的,包括AI.有三個主要的方式我曾用到的:

  1. 標記所以可到達的節點.如果你的地圖不是完全連通的話,浙江是很有用的,或許你想要知道哪些是可到達的.那就是我們上面所實現的,用已訪問過標記來記錄.

  2. 尋找從一個節點到其他所有節點的路徑,或者從所有節點到一個節點的路徑.這是我在頂層頁中使用到的.

  3. 測量從一個節點到其他節點的距離.這對於想知道距離一個怪獸有多遠是非常有用的.

  4. 測量從一個節點到其他節點們的距離.

如果你正在生成路徑,你將想要知道每個節點來自於哪裏.當你訪問鄰居節點的時候,順便記下鄰居來自於哪裏.重命名visited表爲came_from,用它去跟蹤記錄當前節點來自於哪裏:

  1. frontier = Queue()
  2. frontier.put(start)
  3. came_from = {}
  4. came_from[start] = None
  5. while not frontier.empty():
  6.   current = frontier.get()
  7.   for next in graph.neighbors(current):
  8.      if next not in came_from:
  9.         frontier.put(next)
  10.         came_from[next] = current

讓我們看看這個算法跑起來像什麼:


如果你需要距離,你可以弄一個計時器,初值設置爲0,每訪問一個鄰居你就將該值+1.重命名visited 表 爲distance,並且用它去保存到達它的距離:

  1. frontier = Queue()
  2. frontier.put(start)
  3. distance = {}
  4. distance[start] = 0
  5. while not frontier.empty():
  6.   current = frontier.get()
  7.   for next in graph.neighbors(current):
  8.      if next not in distance:
  9.         frontier.put(next)
  10.         distance[next] = 1 + distance[current]


如果你想保存距離和路徑,你可以弄倆變量進行保存記錄.

所以說那是廣度優先搜索.對於塔防風格的遊戲,我曾經用它去查找從一個給定點到所有位置的路徑,而不是用A*算法重複去爲每個敵人查找路徑.也曾經用它去爲一個指定攻擊範圍的怪獸查找到所有位置的路徑.還將它用在程序地圖生成中.Minecraft用它去可視化剔除計算.

下一步:

  • 我已經用python 和C++實現了該算法.

  • 如果你想要從一個位置出發的所有路徑,而不是到一個位置,翻轉came_from指針就可以了.

  • If you want paths to one of several locations instead of a single location, you can add edges to your graphs from each of your destinations to an extra destination node. The extra node won’t show up on the grid, but in the graph it will represent the destination.

  • 早點退出來:如果你尋找到一個位置或者從一個位置出發的路徑,你可以在找到路徑後就停止搜索了.

  • 加權邊:如果你需要有代價的移動,廣度優先搜索需要變成Dijkstra算法.我在A*文章中有描述.

  • 啓發式:如果你添加了一個方式去引導搜索到目標,廣度優先搜索改爲貪婪搜索算法.同樣在A*文章中有描述.

  • 如果你用廣度優先搜索啓動搜索,並且添加了及早退出策略,權重邊,以及啓發函數,你就可以用A*了.正如你想的那樣,下句話當然還是在A*文章中有描述.



發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章