1. 程式人生 > >【尋路演算法】A*演算法入門筆記(一)

【尋路演算法】A*演算法入門筆記(一)

總結A*演算法在遊戲開發中的實現和設計

在閱讀了以下有關A*演算法的入門級介紹之後,自己打算再整理一遍,以便消化和進一步理解。

【英文原帖連結】Here
【針對原帖的翻譯帖連結】Here

A*演算法描述

1.將起始點新增至待處理集合A(原文中的OpenList)
2.迴圈重複以下步驟
a)尋找待處理集合A中F值最低的節點b;
b)將節點b從待處理集合A中移除,並新增至已處理集合B(原文中的ClosedList)
c)檢測與節點b鄰接的節點,進行以下處理:

①如果節點已經在集合B中,則不做任何處理;
②如果節點不在集合A中,則按照以下步驟處理:
——操作1:標記節點的父節點為當前節點b,並計算F/G/H的值;
——操作2:將節點新增至集合A。
③如果節點已經存在集合A中,則臨時計算從當前節點b到該節點的F/G值,如果F值更小,則更新該節點的資訊,既:更新節點的F/G,並修改其父節點為當前節點b(這表明從節點b到達該點為當前更好的路徑)。

3.迴圈終止的條件
a)目標節點已經被新增至集合A(也可以控制成被新增至集合B作為條件)

b)還未找到目標節點時,集合A已經被掏空(集合A沒有備選項了),此時表明這樣的路徑不存在。
4.儲存路徑
從目標路徑的父節點開始一層一層向上直到起始點。

演算法細節註釋

1.迴圈工作的第一步:挑選代價消耗F最小的節點
公式F=G+H

*G:代表了從生成路徑前一個節點移動到該節點需要花費的行動代價(movement cost)
*H:代表了從該節點到最後目標節點的預估行動代價(estimated movement cost)。這裡指的是通過啟發式的引導,進行代價估算。

2.迴圈工作的第二步:新增到已處理集合B的含義
被步驟一挑選過後的點,都確定了其最短路徑,故而,新增至集合B表明今後這些點都不需要被重新計算。
3.迴圈工作的第三步:新增、更新節點的含義
a)新增沿著路徑通過當前點能夠訪問到的新的節點;
b)更新沿著當前的路徑是否能以更小代價到達之前能到達的點。

實現相關

資料結構

地圖 基本結構:

主要屬性 解釋
accessible 是否可達(如障礙物不可達)
type 可以利用標註型別來指明從相鄰節點到達該節點的代價(如平地到山丘花費的代價)
isVisited 標記是否在“待處理集合A(OpenList)”中
isShortest 標記是否在“已處理集合B(ClosedList)”中
Value_F 代表按照已記錄的路徑到達該節點花費的總代價
Value_G 代表按照已記錄的路徑到達該節點花費的行動代價
Value_H 代表當前節點到目標節點的預估代價
Node* father 指向父親節點的指標

子演算法

1.流程(2.a)的過程實際上能理解成不斷從一個集合中尋找最小值

相關的演算法有很多,可以根據實際情況加以挑選。

  • 簡單遍歷,逐一掃描:效率低,但適合解決小地圖尋路問題
  • 維護有序的集合:每次尋找都直接讀取最小值,但是插入以後需要調整(比遍歷方法效果更好)
  • Binary heap:在大多數場合比一般的方法快2~3倍,並且在長路徑問題中效率呈幾何級增長(十倍以上)

設計相關

地形間移動代價
通常在遊戲中會涉及到多種地形,例如溼地、山丘等,這些地形不算障礙物,但通常會設計較高的內移動代價。更復雜的還有,不同地形切換的消耗代價也許也會不同(例如,從水路切換到陸地也許需要支付很大的代價)。
我們可以簡單的通過給地形增加型別,用以進一步標明相關代價消耗的關係。在計算行動代價G的值時,將其考慮在內即可。
影響對映(influence mapping)
這個概念類似於戰棋遊戲中ZOC(zone of control)的概念。
舉個例子:

在戰棋遊戲中,原本你控制的一支部隊能在平原上移動10步,但是由於在通向目的地的路徑周圍存在敵軍部隊,所以在經過敵方部隊相鄰(敵方控制區內)的路徑格時,會花費巨大的額外代價,導致尋路結果的改變。

-
通過這種方式,還可以避免出現AI尋路時一味追求最短路徑而忽略可能存在的危險,讓遊戲看上更合理。
那麼如果考慮到這一點因素,計算總代價F值時,可以加入ZOC的影響因子。
多個單位的路徑重疊
A*演算法的一個缺點在於,當多個單位試圖抵達位置接近的目標時,會出現多個單位採用相同或相似的路徑。這種情況會讓單位擁堵在幾條“熱門的捷徑”上,顯然不符合遊戲模擬。
那麼如何解決這個問題呢?

可以給已經被單位”claimed”(此路是我開)的路徑增加懲罰因子(penalty),在一定程度上分散減少各個單位的碰撞(通過增加路徑消耗的代價,減少別的單位選取該路徑的機率)。注意,懲罰因子的設計要合理,以避免路徑不可達的情況(代價太大以至於幾乎沒有別的單位會選取該路徑)。

如果有必要的話,也可以僅給接近目標點的部分路徑格子增加懲罰因子,而不是路徑上的所有節點。這樣更貼近現實情況:大家從鄉間小路聚向市中心,離市中心越近才越堵,很遠的地方几乎不會相互影響。

模擬未探索的區域
在遊戲中可能會碰到這樣的情況:AI似乎總是知道前往目的地的最佳路徑,而其實這條路徑所在的範圍還“未被探索”(處於戰爭迷霧中)。
這樣“聰明”到不真實的體驗的確很糟糕。
我們可以為每個玩家增加”knownWalkability”陣列,用以包含已探索的區域資訊。使用這種方法,在考慮其單位尋路時,可以模擬出“探索”的效果,而當目標完全暴露 在已知區域時,A*演算法又能正常執行。
更光滑的路徑
A*演算法生成的最短路徑的確是最小代價的路徑,但有的時候,我們可能出於各種理由,想要一條較為光滑的路徑。
有幾種方法可以解決這個問題。
例如,在計算路徑時可以增加轉向懲罰因子,以減小選取走斜邊的概率。另外,也可以分析生成的路徑,選取鄰接的節點替換斜邊節點以實現更光滑的效果(這種情況相當於犧牲了部分所謂的“行動力”換取更優雅的路線)。
非方形搜尋區域
在作者給的例子裡,使用的簡單的2D方形圖。這樣簡單的設定使得描述地形單位的關係變得十分簡單。但是,我們也可以使用不規則形狀的區域。

設想一下在冒險棋的遊戲中,不同國家間的移動。你可以設計一個像那樣的尋路關卡,為此,可能需要建立一個國家相鄰關係的表格以及和從一個國家移動到另一個國家的G值。同時也要考慮如何估計H。

與此類似,你也可以為固定地形的地圖建立航路點系統(waypoint system)。航路點可以橫穿一條路徑,在地牢遊戲裡可能代表 了一條路或隧道。遊戲的設計者通常會預設好這些航路點,當兩個航路點之間的直線路徑上不存在障礙時,視為該兩點鄰接。

在應用中的不足

1.會穿透路徑中碰撞到的其他單位
在多個單位都在移動的情況下,往往需要處理在移動過程中碰到其他單位的情況。這種情況下需要設計新的方法生成路徑,如簡單的上下左右繞過等。即,為了補足這種缺陷,需要單獨再新增碰撞檢測的程式碼以及相關新路徑生成方法。
2.大量尋路單位佔據了大量CPU時間

這幾乎是尋路問題的通病。但是可以通過以下策略緩解:

  • 使用更小的地圖或控制尋路單位的數量
  • 不要讓多個單位同時尋路,取而代之的方法是將他們加入佇列,將各個尋路過程分散在幾個遊戲週期中。如果遊戲以40週期每秒的速度執行,沒人能察覺到。但當大量尋路單位計算自己的路徑的時候,玩家會發覺遊戲的速度變慢了。
  • 使用更大的地圖分塊,以降低尋路中需要搜尋的網格總數。(Consider using larger squares (or whatever shape you are using) for your map. This reduces the total number of nodes searched to find the path)。我的理解是,在一定條件小,可以把幾個小格當成一個大格來處理,而移動到鄰接的網格就相當於移動到剛才幾個小格的中心位置。如果有必要,可以設計兩個尋路系統,這通常也是專業的尋路做法:使用大塊作為長路徑選擇,而當接近目標時,使用適用更小的塊的另一種方法以產生更精細(小範圍內看出最短)的路徑。
  • 考慮常用的長路徑,可以預先計算並儲存好。在需要使用時,直接查詢。
  • 預設好那些不可達的點。並在呼叫A*演算法尋路前,檢驗目標點是否可達。
  • 在一些類似“迷宮”遊戲的環境下,設計地圖時,可以人為標註一些只會到達“死角”的網格,以此暗示AI此路不通,不必嘗試通過此格的路徑。當然,如果開發者足夠有把握,也能加入自動標註類似路徑網格的演算法。

與Dijkstra’s Algorithm比較

Dijkstra演算法與A*演算法十分類似:不過在Dijkstra演算法中不用考慮H,所以該演算法屬於非啟發式演算法。它沿著外圍各個方向逐步擴張搜尋,所以通常Dijkstra在到達目標位置前會搜尋大量的區域,故而比A*演算法效率低。
那麼什麼情況下,我們更願意選擇Dijkstra演算法呢?

設想一下你現在建立了一個採集資源的單位,它從它的位置開始搜尋離它最近的資源,此時地圖上有多個目標點,但它只想找到距離最近的點。此時,Dijkstra演算法比A*演算法表現更好,因為在使用A*演算法時,搜尋前我們無法固定一個啟發式的值H,而只能通過迴圈的方式對於每一個資源都找到一條最短路徑,最後再從中選取一條最短的路徑,而這效率明顯不高。