關於尋路演算法的一些思考(7):地圖表示

hf_cherish發表於2015-08-11

在本系列文件大部分內容中,我都假設A*用於某種網格上,其中的“節點”是一個個網格的位置,“邊”是從某個網格位置出發的各個方向。然而,A*可用於任意圖形,不僅僅是網格,有很多種地圖表示都可以使用A*演算法。

地圖表示可能對效能和路徑的質量產生很大影響。

尋路演算法不是線性的,而是越來越差。如果需要行進的距離翻倍了,那麼會消耗超過兩倍的時間來找路徑。你可以想象尋路演算法是在搜尋一個類似圓的區域,當圓的直徑加倍時,區域變成原來的四倍。一般來說,在地圖表示中,節點越少,A*演算法越快。而且節點越匹配角色單元將要移向的位置,路徑質量越好。

遊戲中,用於尋路的地圖表示不需要和用於其他用途的地圖表示一樣。但是採用相同的表示是一個不錯的起點,直到你發現需要更好的路徑或更高的效能。

網格(Grid)

網格圖將世界(world)均勻分割為小的規則圖形,這些圖形有時被稱為“圖塊(tile)”。常用的網格有正方形、三角形和六邊形。網格很簡單,也很容易瞭解,很多遊戲都採用它來表示世界,因此本文中我重點關注網格。

在《BlobCity》遊戲中我採用網格表示,因為在每個網格位置,移動成本都不同。如果在一大片空間內,移動成本都是均勻的(正如前文我舉過的例子),那麼用網格可能就相當浪費。因為此時,A*無需一次走一步,它可以跳過一大片區域走到另一端。在網格上尋路,得到的也是網格上的路徑,可以通過後期處理,消除鋸齒狀移動。但是如果你的角色單元沒有限制必須在網格上移動,或者你的世界甚至不採用網格,那麼在網格上尋路可能不是最好的選擇。

圖塊移動(Tile movement)

即使在網格中,也可以選擇沿圖塊、邊或頂點移動。圖塊是預設選項,尤其是角色單元只能移動到圖塊中心的那些遊戲。在上圖中,在A處的單元可以移到所有標B的位置。你也許還允許對角線移動,代價相同或者更高。

如果你採用網格尋路,但角色單元不限制僅沿網格移動,並且移動成本是均勻的的,那麼你可能想要拉直路徑,即如果某兩個節點之間沒有障礙,可以從一個節點沿直線移動到另一個遠處的節點。

邊移動(Edge movement)

 

如果角色單元可以移動到一個網格內的任何一點,或者圖塊很大,就應該考慮,你的應用是否選擇邊尋路或頂點尋路更好。

一個單元通常從一個邊(一般是邊的中點)進入圖塊,並從另一個邊離開那個圖塊。圖塊尋路中,單元移到圖塊中心,但邊尋路中,單元直接從一個邊移到另一個邊。我寫了一個java applet,演示繪製邊之間的道路,可能幫助說明邊是如何使用的。
頂點移動(Vertex movement)

網格圖中的障礙通常在頂點處有拐角。在障礙物周圍,最短的路徑就是要繞過這些拐角。頂點尋路中,角色單元從一個拐角移到另一個拐角。這是一種最節省開銷的移動,但是要依據角色單元的大小調整路徑。

多邊形地圖(Polygonal maps)

除了網格,最常用的是多邊形表示。如果一大片區域的移動成本是均勻的,並且角色單元可以沿直線移動,而不是沿網格移動,你可能想採用一種非網格的表示。在你的遊戲中,即使其他東西採用網格,尋路也可以使用非網格的圖形表示。這裡有個簡單的例子,是一種多邊形的地圖表示。這個例子中,角色單元需要繞過兩個障礙。

想象在這個地圖中角色單元如何移動。最短路將在這些障礙的拐角點之間,因此我們選擇這些拐角點(紅色圓點)作為A*演算法的關鍵“導航點”;每次地圖改變,就計算一次這些點。如果障礙和網格對齊,那麼導航點和網格頂點對齊。此外,尋路的起點和終點應當標示在圖中;每次呼叫A*,都需要將這兩個點加到圖上。

除了導航點,A*需要知道哪些點是連通的。一個簡單的演算法是構建可檢視:互相可見的點對。這個簡單演算法可能滿足你的需求,尤其是當遊戲中地圖不常改變時。但是如果這個演算法太慢,你可能需要一個更復雜的演算法。另外,在圖上新增起點和終點後,對任意兩個頂點(包括起點和終點),如果兩點可見,新增一條這兩點連線而成的邊。

A*需要的第三條資訊是點之間的行進時間。如果在網格上移動,時間就是曼哈頓距離manhattan distance)或對角網格距離diagonal grid distance)。如果可以在導航點之間直接移動,時間就是直線距離

接下來,A*考慮從導航點到導航點的路徑,圖中粉色線就是其中一條路徑。如果導航點很少,而網格位置很多,這種方法要比從網格點到網格點的尋路快得多。如果路上沒有障礙,A*效能非常好——起點和終點將由邊連線,無需擴充套件任何導航點,A*可以立即找到路徑。即使有障礙,A*也將從一個拐角點跳到另一個拐角點,直到找到最優路徑,這將仍然比在網格位置間尋路要快得多。

維基百科中有更多關於機器人文學中的可檢視

複雜性管理(Managing complexity)

上邊的例子非常簡單,圖也很合理。然而在一些有很多開放區域或長廊的地圖中,可檢視的一個弊端就顯現出來了。連線每對障礙拐角點的一個主要缺點是,如果有N個拐角點(頂點),則至多有N2條邊。下圖展示了這個問題:

這些額外的邊主要影響記憶體使用。相比網格,這些邊提供一種捷徑,大大加快了尋路。雖然有演算法可以刪除冗餘的邊,以簡化圖形,但刪除之後仍然有很多邊。

可檢視的另一個缺陷是,每次呼叫A*,都要新增起點和終點,以及以它們為頂點的新邊,然後在找到路徑之後,刪除這些新增的東西。節點很好加,但增加邊需要考慮從這些新節點到所有已有節點的可見情況,如果地圖很大,這可能會很慢。一種優化方案是隻看附近的節點,或者也可以用簡化可檢視,刪除和兩個頂點都不相切的邊(這種邊永遠不會出現在最短路中)。

導航網(Navigation Meshes)

不用多邊形表示障礙,而是將可行區域用不重疊的多邊形表示,這也被稱為導航網。這些可行區域還可以附有一些資訊(如“要求游泳”或“移動成本為2”)。這種表示法不需要儲存障礙。

前面的例子就變成了下圖這樣:

我們可以像處理網格一樣處理這個,同樣的,可以選擇多邊形中心點、邊或頂點作為導航點。

多邊形移動(Polygon movement)

同網格一樣,每個多邊形的中心提供了一個合理的尋路節點集。此外,還要新增起點和終點,以及這兩個點與所在區域中心點所連成的邊。下圖中,黃色路徑是沿多邊形中心點尋路所得的路徑,粉色路徑是理想路徑。

可檢視表示可以產生那條粉色理想路徑。採用導航網使地圖易於管理了,但是路徑的質量受到影響。我們可以消除路徑,使其看起來更好一些。

多邊形邊移動(Polygon edge movement)

移到多邊形的中心通常是不必要的。相反,我們可以穿過相鄰多邊形的邊而移動。下面這個例子中,我選擇每條邊的中點。黃色路徑是沿邊中點尋路所得的路徑,相比理想的粉紅色路徑,是一條不錯的路徑。

 

你也可以增加成本,在邊上選更多的點,來產生更好的路徑,

多邊形頂點移動(Polygon vertex movement)

繞過障礙的最短路徑是繞過其拐角,這也是為什麼我們在可檢視表示中採用拐角點,在這裡即是導航網的頂點:

 

上圖中,路上只有一個障礙。當要繞過障礙時,黃色路徑會穿過一個頂點,粉色路徑(理想路徑)也一樣。然而,可檢視方法將直接連線起點和障礙拐角點,導航圖則要更多步。這些步驟不應該沿頂點走,因此路徑看起來不自然,有“抱牆”行為。

混合式移動(Hybrid movement)

對於多邊形的哪個部分可以用作尋路導航點,我們並沒有任何約束。你可以在一條邊上多加一些點,頂點也不錯,多邊形的中心點則基本沒用。下圖是採用了邊中點和頂點的一種混合方案:

 

注意要繞過障礙,需要穿過一個頂點,但在其他地方,則可以穿過邊中點。

路徑消除(Path smoothing)

只要移動成本是固定的,路徑消除相當容易。演算法很簡單:如果點i到點i+2可見,刪除點i+1,迴圈直到鄰節點之間都不可見。剩下的只有繞過障礙物拐角點的導航點。這些點都是導航網的頂點。因此如果使用路徑消除,就不需要採用邊中點或多邊形中心點作為導航點,只要頂點就可以了

上面的例子中,路徑消除可以將黃色路徑變成粉紅色路徑。然而,尋路演算法並不知道這些更短的路徑,因此它的決策不會優化。導航網是一種近似地圖表示,而可檢視是一種精確地圖表示。縮短在導航網中找到的路,結果並不總能像通過可檢視找到的路一樣好。

分層(Hierarchical)

平面地圖只有一層。而遊戲很少只有一層——往往有一個“圖塊”層,然後有一個“子圖塊”層,物體可以在圖塊中移動。然而我們通常只在高層尋路。你也可以新增更高的層,如“房間”。

在地圖表示中,節點越少,尋路越快。還有一種加快尋路速度的方法是多層次搜尋。例如,要從你家到另一個城市的某個位置,你會找到一條路,從你的椅子到你的車,從車到街道,從街道到高速公路,從高速公路到城市邊緣,再從那兒到另一個城市,然後到一個街道,到一個停車場,最終到達目的地門前。此時,有下述幾層搜尋:

  • 街道層,你從一個位置走到附近的某個位置,但不會走出這條街。
  • 城市層,你從一條街道走到另一條,直到找到高速公路。你無需擔心進入建築物或停車場,也不用擔心在高速公路上行駛。
  • 州層,在高速公路上,你從一個城市到另一個城市。在到達目的城市之前,你無需擔心城市內的街道。

將問題分層,可以忽略很多選項。例如當從一個城市到另一個城市時,考慮路上每個城市的每條街道是很乏味的。相反,你可以忽略它們,只考慮高速公路,問題就變得很小且易於管理,解決也變得很快。

分層地圖在表示上有很多層。異構層次結構(heterogenous hierarchy)通常有固定層數,各有不同特點。例如,《Ultima 5》有一個“世界”地圖,上邊有城市和地牢。你可以進入一個城市或地牢,這就進入地圖的第二層。另外,世界之上還有世界,從而是一個三層結構。這些層可以是不同的型別(圖塊網格、可檢視、導航網、路標)。而同質層次結構(homogeneous hierarchy)層數任意,每層都有相同的特性。四叉樹和八叉樹就是同質層次結構的。

分層地圖中,尋路可能發生在幾個層次。例如,假設一個 1024×1024 的世界被劃分為 64×64 個“區域”,則可以這樣找到一條路徑,從玩家位置到區域邊緣,然後從一個區域到另一個區域,直到到達目的區域,然後從那個區域的邊緣到達目的位置。粗級別上,更容易找到長路徑,因為尋路演算法沒有考慮所有的細節。當玩家穿過每個區域時,可以再次呼叫尋路演算法,找一個短路徑。保證問題規模很小,尋路演算法就可以執行得更快。

你可以結合使用分層和圖搜尋演算法,如A*,但是不需要每一層都採用一樣的演算法。對一些小的層級,你可以預算所有節點間的最短路(用Floyd-Warshall或其他演算法)。在分層地圖中,通常找不到最優路徑,但一般都接近最優。

還有個類似的方法是改變解析度。首先,繪製低解析度路徑。當接近一個點時,用高解析度精化路徑。這個方法可以結合路徑拼接使用,以避免移動障礙。

一些文章:《“龍騰世紀:起源”中的尋路演算法》解釋某商業遊戲中使用的幾種分層方法,《採用線性預處理的超快最短路查詢》在道路圖中使用“運輸節點”[PDF],《遊戲網格地圖的運輸節點》、《分層A*:有效搜尋抽象層》、《道路網路線規劃》(Dominic Schulte的博士論文),逐層註解A*(第一部分第二部分原始碼)。

環繞式地圖(Wraparound maps)

如果你的世界是球形或環形的,物體可以從地圖的一端繞到另一端。最短路可能在任一方向,因此必須探索所有的方向。如果用網格,環繞時可以用啟發式方法。此時,我們不用abs(x1 – x2),而採用min(abs(x1 – x2), (x1+mapsize) – x2, (x2+mapsize) – x1),即考慮三種情況的最小值:待在地圖上不繞行,x1在左邊時繞行,或x2在左邊時繞行。繞行每個軸時都這樣做。本質上來說,你計算啟發值時,假設地圖與其副本鄰接。

連通元件(Connected Components)

有些遊戲地圖中,起點和終點之間根本就無路可通。如果用A*找路,它會探索圖的很大一個子集,才能確定根本沒路。如果可以預先分析地圖,用不同的標記標識出所有的連通子圖,那麼在找路之前,首先檢查起點和終點是否都在同一個子圖中。如果不在,那麼這兩者之間無路可通。另外分層尋路在這也可以用,特別是子圖之間有單向邊時。

道路圖(Road maps)

如果你的角色單元只能在道路上走,你可能需要提供A*道路和交叉口的資訊。每個交叉口是圖上的一個節點,每條路是圖上一條邊。A*找從交叉口到交叉口的路,這比用網格表示要快得多。

有時,角色單元的起點和終點可能不在交叉口。此時,每次執行A*時,都要修改點/邊圖(和可檢視和導航圖採用的技術一樣),將起點和終點作為新節點加到圖中,然後在這兩個點和最近的交叉點之間連線。尋路結束後,再刪除這些額外的節點和邊,這樣圖在下次呼叫A*時還可以使用。

上圖中,交叉口是尋路圖中的節點,節點之間的道路是邊,且每條邊都應給定道路行駛距離。在這個框架中,你可以把單向道路作為圖上的單向邊。

如果你想給轉向分配成本,你可以稍稍擴充套件這個框架:將原來單一的位置節點,變為<位置,方向>節點(靜態空間的一個點),其中方向指你到達那個位置後所面向的方向;將原來從X到Y的邊,換成從<X,方向>到<Y,方向>的邊(代表直行),和從<X,方向1>到<Y,方向2>的邊(代表轉向)。每條邊都或者代表直行,或者代表轉向,不可能兩者都是。然後你可以給代表轉向的邊分配成本。

如果你還要考慮轉向限制,如“只能右轉”,你可以改變上述框架,即結合使用那兩種邊,每個轉向邊之後都是直行。例如,在這個框架中,你想表示一個限制“只能右轉”:新增一個直行邊<X,北>到<Y,北>,一個轉向邊<X,北>到<Z,東>,轉向之後是直行。不要新增<X,北>到任何向西的邊,因為這意味著左轉,也不要新增任何向南的邊,因為這意味著掉頭。

利用上述框架,可以對一個大型市中心建模,其中有單向道路,特定路口轉向限制(通常禁止掉頭,有時禁止左轉),以及轉向成本(建模在右轉之前減速和等待行人)。相比網格圖,道路圖中A*找路相當快,因為每個節點處的選擇很少,而且圖上的節點也相對較少。

如果是大型道路圖,一定要讀讀Goldberg和Harrelson發表在ALT(A*, Landmarks, Triangle inequality)上的文章PDF,或者這篇論文)。

跳躍鏈(Skip links)

基於網格建立的尋路圖,一般給每個位置分配一個頂點,給相鄰位置之間的每個可能的移動分配一條邊。然而邊不一定必須是相鄰頂點之間的,“跳躍鏈”或“快捷鏈”就是非相鄰點之間的邊,它可以加快尋路程式。

跳躍鏈的移動成本怎麼算呢?有兩種方法:

  • 使成本匹配最優路徑的移動成本。這保留了A*的優良特性,如尋找最優路徑。為了將A*推向正確的方向,要打破跳躍鏈和常規鏈之間的關聯,即將跳躍鏈的成本減少1%左右。
  • 使成本匹配啟發成本。這對效能有很大影響,但是放棄了最優路徑。

新增跳躍鏈類似於分層地圖,花費更少的精力,但往往能給你一樣的效能。

對於有地下室和走廊的網格圖,矩形對稱性縮減和跳躍點搜尋提供兩種方法建立跳躍鏈。矩形對稱性縮減(Rectangular Symmetry Reduction)靜態建立附加邊(他們稱為巨集邊),然後呼叫標準的圖搜尋演算法。跳躍點搜尋(Jump Point Search)動態建立長邊,是圖搜尋演算法的一部分。對於道路圖和其他型別的圖,抽象分層值得一看。

路標(Waypoints)

路標是路徑上的一個點。路標可以具體到每條路,或者是遊戲地圖的一部分。路標可以手動輸入或自動計算。很多實時策略遊戲中,玩家點選就可以手動新增特定路徑的路標。當自動計算時,路標可以簡化路徑表示。地圖設計者可以在地圖上人工新增路標(或“信標燈”),以標識好路徑的位置,或者也可以用演算法自動標識。

使用跳躍鏈是為了加快尋路,因此跳躍鏈應當放在設計者設定的路標之間,這可以使其利益最大化。

如果路標不是很多,可以預算每對路標之間的最短路(用全對最短路演算法,不用A*)。常見的情況就是,一個角色單元先按照自己的路徑到達一個路標,然後按照預算的路標間的最短路走,最後離開路標這個高速路,走自己的路到達目標。

如果路標或跳躍鏈的成本錯誤,可能導致找到次優路徑。有時我們可以通過後期處理或在移動中,消除一個糟糕的路徑。

圖形格式建議(Graph Format Recommendations)

一開始你在已使用的遊戲世界表示中尋路,如果你不滿意,考慮將遊戲世界轉換為方便尋路的另一種表示形式。

很多網格遊戲中,地圖的很多大塊區域移動成本都是均勻的,但是A*不知道這些,並且浪費時間來探索它們。建立一個簡單圖(導航網,可檢視,或者網格圖的分層表示)可以幫助A*。

移動成本固定時,可檢視產生的是最優路徑,且A*執行很快,但是邊儲存耗費大量記憶體。網格允許移動成本有細微改變(地形斜坡懲罰用的危險區域等),邊儲存耗費記憶體少,但點儲存耗費很多記憶體,而且A*可能很慢。導航網居於兩者之間。在大塊區域移動成本均勻時,它效果很好,而且允許移動成本的些微改變,還能產生合理的路徑。這些路徑並不總如可檢視產生的一樣短,但是通常都是合理的。分層地圖採用多層表示來處理長距離內的大致路徑,以及短距離內的詳細路徑。

你可以讀讀這篇很形象的文章,瞭解更多關於導航網的知識。注意這篇文章比較了:(a)僅保持可行多邊形和僅保持導航點,(b)沿頂點走和沿多邊形中心點走。這些大多是正交的。保持可行多邊形有利於後期動態調整路徑,但是並非所有的遊戲都需要。使用頂點更有利於避免障礙,並且如果你採用了路徑消除,還不會影響路徑質量。如果沒有路徑消除,邊的效果可能更好,所以考慮邊或是邊+頂點。

除了給網格地圖構建一個獨立的非網格表示,你也可以改變A*,使其更好得理解成本均勻分佈的網格地圖。可以參照跳點搜尋在方格上加速A*的方法,以及Theta*在網格上生成非網格移動的方法。

相關文章