物體的移動演算法似乎顯得很簡單,然而尋路規劃問題卻十分複雜。考慮下面這個例子:
這個單位的初始位置在地圖的下方,想要到達地圖的頂部。如果物體所能偵測到的地方(粉色部分所示)並沒有障礙,那麼物體就會直接向上走到它的目標位置。但在距離頂端較近的位置時,物體偵測到了障礙,因而改變了方向。該物體將不得不行進一個“U”形的路徑繞過障礙物(如紅色路徑所示)。通過對比可知,尋路系統能夠通過搜尋一個更大的範圍(如藍色區域所示),並尋找一個更短的路線(如藍色路徑所示),使物體避免繞這條由凹陷障礙物造成的遠路。
當然,可以通過改進物體的移動演算法解決上圖所示的陷阱。即要麼避免在地圖上建立有凹陷的物體,要麼標記整個凹陷物體的整個凸包為危險區域(即除非目標在該區域內,否則避免進入該區域),如下圖所示:
而尋路系統則會讓路徑的決定提前,而不是像上圖一樣,物體直到移動到最後一刻才發現問題所在。對於“改進物體移動演算法”和“使用尋路系統規劃路徑”兩種方式有以下的折中:規劃路徑一般來說更慢,但效果更好;改進移動演算法則會快一些,但有時候會卡住。如果遊戲地圖經常改變,那麼路徑規劃的方式可能就意義不大了。我建議兩者都使用:在更大的尺度、緩慢變換的地圖和更長的路徑上進行尋路規劃,而對於區域性區域、快速更改的地圖和短的路徑則使用改進的物體移動演算法。
演算法
普通教科書上的尋路演算法往往只應用在數學意義上的“圖”上,即由頂點集合和邊集合互相連線組成的結構。因此我們需要將一個柵格化的遊戲地圖轉化為一個“圖”:地圖上的每一格可以作為一個頂點,而相鄰的格子則各有一條邊,如圖所示:
我們只考慮二維網格。如果你沒有關於圖的背景知識,可以參見此連結。之後我會討論如何在遊戲世界中建立其他型別的圖。
大部分在AI和演算法領域的尋路演算法都是針對作為數學結構的“圖”本身,而並非針對這種網格化遊戲地圖。我們希望尋找一種能利用遊戲地圖自身特徵的方法。其實有些在二維網格圖中我們認為是常識的事情,一些在普通圖上使用的尋路演算法本身可能並沒有考慮到,例如如果兩個物體距離較遠,那麼可能從一個物體到另一個物體的移動的時間和路徑會較長(當然,假設空間中沒有蟲洞存在)。對於方向來說,如果方向是朝東,那麼最優路徑的路徑也應當是大體往東走,而不是向西去。在網格中還可以從對稱中獲取資訊,即先向北再向西,大部分情況下和先向西再向北等價。這些額外的資訊可以讓尋路演算法更加快速。
Dijkstra演算法和最好優先搜尋(best-first search)
Dijkstra演算法簡單說來,就是從起始點訪問其他臨近節點,並將該節點加入待檢查節點集合中,使用鬆弛演算法更新待檢查節點的路徑長度值。只要圖不存在負權值的邊,Dijkstra演算法能夠確保找到最短路徑。在下面的圖中,粉色的方格為起始點,藍紫色的方格為目標點,青綠色的方格則為Dijkstra演算法所掃描的節點。淡色的節點是距離起始點較遠的節點。
貪心最好優先搜尋演算法大體與之類似,不同的是該演算法對目標點的距離有一個估計值(啟發值)。該演算法並不在待檢查節點集合中選取距離起始點近的節點進行下一步的計算,而是選擇距離目標點近的節點。貪心最好優先搜尋演算法並不能保證尋找到最優路徑,然而卻能大大提高尋路速度,因為它使用了啟發式方法引導了路徑的走向。舉例來說,如果目標節點在起始點的南方,那麼貪心最好優先搜尋演算法會將注意力集中在向南的路徑上。下圖中的黃色節點指示了具有高啟發值的節點(即到目標節點可能花費較大的節點),而黑色則是低啟發值的節點(即到目標節點的花費較小的節點)。下圖說明了相比於Dijkstra演算法,貪心最好優先演算法能夠更加快速地尋路。
然而上述的例子僅僅是最簡單的:即地圖上沒有障礙物。考慮前文中我們曾經提到的凹陷障礙物,Dijkstra演算法仍然能夠尋找到最短路徑:
貪心最好優先演算法雖然做了較少的計算,但卻並不能找到一條較好的路徑。
問題在於最好優先搜尋演算法的貪心屬性。由於演算法僅僅考慮從目前節點到最終節點的花費,而忽略之前路徑已經進行的耗費,因此即使在路徑可能錯誤的情況下仍然要移動物體。
1968年提出的A*演算法結合了貪心最好優先搜尋演算法和Dijsktra演算法的優點。A*演算法不僅擁有髮式演算法的快速,同時,A*演算法建立在啟發式之上,能夠保證在啟發值無法保證最優的情況下,生成確定的最短路徑。
A*演算法
下面我們主要討論A*演算法。A*是目前最流行的尋路演算法,因為它十分靈活,能夠被應用於各種需要尋路的場景中。
與Dijkstra演算法相似的是,A*演算法也能保證找到最短路徑。同時A*演算法也像貪心最好優先搜尋演算法一樣,使用一種啟發值對演算法進行引導。在剛才的簡單尋路問題中,它能夠像貪心最好優先搜尋演算法一樣快:
而在後面的具有凹陷障礙物的地圖中,A*演算法也能夠找到與Dijkstra演算法所找到的相同的最短路徑。
該演算法的祕訣在於,它結合了Dijkstra演算法使用的節點資訊(傾向於距離起點較近的節點),以及貪心最好優先搜尋演算法的資訊(傾向於距離目標較近的節點)。之後在討論A*演算法時,我們使用g(n)表示從起點到任意節點n的路徑花費,h(n)表示從節點n到目標節點路徑花費的估計值(啟發值)。在上面的圖中,黃色體現了節點距離目標較遠,而青色體現了節點距離起點較遠。A*演算法在物體移動的同時平衡這兩者的值。定義f(n)=g(n)+h(n),A*演算法將每次檢測具有最小f(n)值的節點。
之後的系列文章將主要探討啟發值設計、具體實現、地圖表示等,並討論與遊戲中尋路問題相關的一系列話題。
——————
本系列:
- 關於尋路演算法的一些思考(1):A* 演算法介紹
- 關於尋路演算法的一些思考(2):Heuristics 函式
- 關於尋路演算法的一些思考(3):A* 演算法的實現
- 關於尋路演算法的一些思考(4):A* 演算法的變體
- 關於尋路演算法的一些思考(5):處理移動中的障礙物
- 關於尋路演算法的一些思考(6):預先計算好的路徑的所用空間
- 關於尋路演算法的一些思考(7):地圖表示
- 關於尋路演算法的一些思考(8):長期和短期目標
- 關於尋路演算法的一些思考(9):尋路者的移動成本
- 關於尋路演算法的一些思考(10):最短路徑的使用者體驗
- 關於尋路演算法的一些思考(11):尋路演算法的其他應用
- 關於尋路演算法的一些思考(12):AI 技術