游戏中常用的寻路算法(6):地图表示
在本系列文檔大部分內(nèi)容中,我都假設A*用于某種網(wǎng)格上,其中的“節(jié)點”是一個個網(wǎng)格的位置,“邊”是從某個網(wǎng)格位置出發(fā)的各個方向。然而,A*可用于任意圖形,不僅僅是網(wǎng)格,有很多種地圖表示都可以使用A*算法。
地圖表示可能對性能和路徑的質(zhì)量產(chǎn)生很大影響。
尋路算法不是線性的,而是越來越差。如果需要行進的距離翻倍了,那么會消耗超過兩倍的時間來找路徑。你可以想象尋路算法是在搜索一個類似圓的區(qū)域,當圓的直徑加倍時,區(qū)域變成原來的四倍。一般來說,在地圖表示中,節(jié)點越少,A*算法越快。而且節(jié)點越匹配角色單元將要移向的位置,路徑質(zhì)量越好。
游戲中,用于尋路的地圖表示不需要和用于其他用途的地圖表示一樣。但是采用相同的表示是一個不錯的起點,直到你發(fā)現(xiàn)需要更好的路徑或更高的性能。
網(wǎng)格(Grid)
網(wǎng)格圖將世界(world)均勻分割為小的規(guī)則圖形,這些圖形有時被稱為“圖塊(tile)”。常用的網(wǎng)格有正方形、三角形和六邊形。網(wǎng)格很簡單,也很容易了解,很多游戲都采用它來表示世界,因此本文中我重點關(guān)注網(wǎng)格。
?
在《BlobCity》游戲中我采用網(wǎng)格表示,因為在每個網(wǎng)格位置,移動成本都不同。如果在一大片空間內(nèi),移動成本都是均勻的(正如前文我舉過的例子),那么用網(wǎng)格可能就相當浪費。因為此時,A*無需一次走一步,它可以跳過一大片區(qū)域走到另一端。在網(wǎng)格上尋路,得到的也是網(wǎng)格上的路徑,可以通過后期處理,消除鋸齒狀移動。但是如果你的角色單元沒有限制必須在網(wǎng)格上移動,或者你的世界甚至不采用網(wǎng)格,那么在網(wǎng)格上尋路可能不是最好的選擇。
圖塊移動(Tile movement)
?
即使在網(wǎng)格中,也可以選擇沿圖塊、邊或頂點移動。圖塊是默認選項,尤其是角色單元只能移動到圖塊中心的那些游戲。在上圖中,在A處的單元可以移到所有標B的位置。你也許還允許對角線移動,代價相同或者更高。
如果你采用網(wǎng)格尋路,但角色單元不限制僅沿網(wǎng)格移動,并且移動成本是均勻的的,那么你可能想要拉直路徑,即如果某兩個節(jié)點之間沒有障礙,可以從一個節(jié)點沿直線移動到另一個遠處的節(jié)點。
邊移動(Edge movement)
?
如果角色單元可以移動到一個網(wǎng)格內(nèi)的任何一點,或者圖塊很大,就應該考慮,你的應用是否選擇邊尋路或頂點尋路更好。
一個單元通常從一個邊(一般是邊的中點)進入圖塊,并從另一個邊離開那個圖塊。圖塊尋路中,單元移到圖塊中心,但邊尋路中,單元直接從一個邊移到另一個邊。我寫了一個java applet,演示繪制邊之間的道路,可能幫助說明邊是如何使用的。
頂點移動(Vertex movement)
?
網(wǎng)格圖中的障礙通常在頂點處有拐角。在障礙物周圍,最短的路徑就是要繞過這些拐角。頂點尋路中,角色單元從一個拐角移到另一個拐角。這是一種最節(jié)省開銷的移動,但是要依據(jù)角色單元的大小調(diào)整路徑。
多邊形地圖(Polygonal maps)
除了網(wǎng)格,最常用的是多邊形表示。如果一大片區(qū)域的移動成本是均勻的,并且角色單元可以沿直線移動,而不是沿網(wǎng)格移動,你可能想采用一種非網(wǎng)格的表示。在你的游戲中,即使其他東西采用網(wǎng)格,尋路也可以使用非網(wǎng)格的圖形表示。這里有個簡單的例子,是一種多邊形的地圖表示。這個例子中,角色單元需要繞過兩個障礙。
?
想象在這個地圖中角色單元如何移動。最短路將在這些障礙的拐角點之間,因此我們選擇這些拐角點(紅色圓點)作為A*算法的關(guān)鍵“導航點”;每次地圖改變,就計算一次這些點。如果障礙和網(wǎng)格對齊,那么導航點和網(wǎng)格頂點對齊。此外,尋路的起點和終點應當標示在圖中;每次調(diào)用A*,都需要將這兩個點加到圖上。
除了導航點,A*需要知道哪些點是連通的。一個簡單的算法是構(gòu)建可視圖:互相可見的點對。這個簡單算法可能滿足你的需求,尤其是當游戲中地圖不常改變時。但是如果這個算法太慢,你可能需要一個更復雜的算法。另外,在圖上添加起點和終點后,對任意兩個頂點(包括起點和終點),如果兩點可見,添加一條這兩點連接而成的邊。
A*需要的第三條信息是點之間的行進時間。如果在網(wǎng)格上移動,時間就是曼哈頓距離(manhattan distance)或?qū)蔷W(wǎng)格距離(diagonal grid distance)。如果可以在導航點之間直接移動,時間就是直線距離。
接下來,A*考慮從導航點到導航點的路徑,圖中粉色線就是其中一條路徑。如果導航點很少,而網(wǎng)格位置很多,這種方法要比從網(wǎng)格點到網(wǎng)格點的尋路快得多手游賬號買號平臺。如果路上沒有障礙,A*性能非常好——起點和終點將由邊連接,無需擴展任何導航點,A*可以立即找到路徑。即使有障礙,A*也將從一個拐角點跳到另一個拐角點,直到找到最優(yōu)路徑,這將仍然比在網(wǎng)格位置間尋路要快得多。
維基百科中有更多關(guān)于機器人文學中的可視圖。
復雜性管理(Managing complexity)
上邊的例子非常簡單,圖也很合理。然而在一些有很多開放區(qū)域或長廊的地圖中,可視圖的一個弊端就顯現(xiàn)出來了。連接每對障礙拐角點的一個主要缺點是,如果有N個拐角點(頂點),則至多有N2條邊。下圖展示了這個問題:
這些額外的邊主要影響內(nèi)存使用。相比網(wǎng)格,這些邊提供一種捷徑,大大加快了尋路。雖然有算法可以刪除冗余的邊,以簡化圖形,但刪除之后仍然有很多邊。
可視圖的另一個缺陷是,每次調(diào)用A*,都要添加起點和終點,以及以它們?yōu)轫旤c的新邊,然后在找到路徑之后,刪除這些添加的東西。節(jié)點很好加,但增加邊需要考慮從這些新節(jié)點到所有已有節(jié)點的可見情況,如果地圖很大,這可能會很慢。一種優(yōu)化方案是只看附近的節(jié)點,或者也可以用簡化可視圖,刪除和兩個頂點都不相切的邊(這種邊永遠不會出現(xiàn)在最短路中)。
導航網(wǎng)(Navigation Meshes)
不用多邊形表示障礙,而是將可行區(qū)域用不重疊的多邊形表示,這也被稱為導航網(wǎng)。這些可行區(qū)域還可以附有一些信息(如“要求游泳”或“移動成本為2”)。這種表示法不需要存儲障礙。
前面的例子就變成了下圖這樣:
?
我們可以像處理網(wǎng)格一樣處理這個,同樣的,可以選擇多邊形中心點、邊或頂點作為導航點。
多邊形移動(Polygon movement)
同網(wǎng)格一樣,每個多邊形的中心提供了一個合理的尋路節(jié)點集。此外,還要添加起點和終點,以及這兩個點與所在區(qū)域中心點所連成的邊。下圖中,黃色路徑是沿多邊形中心點尋路所得的路徑,粉色路徑是理想路徑。
?
可視圖表示可以產(chǎn)生那條粉色理想路徑。采用導航網(wǎng)使地圖易于管理了,但是路徑的質(zhì)量受到影響。我們可以消除路徑,使其看起來更好一些。
多邊形邊移動(Polygon edge movement)
移到多邊形的中心通常是不必要的。相反,我們可以穿過相鄰多邊形的邊而移動。下面這個例子中,我選擇每條邊的中點。黃色路徑是沿邊中點尋路所得的路徑,相比理想的粉紅色路徑,是一條不錯的路徑。
?
你也可以增加成本,在邊上選更多的點,來產(chǎn)生更好的路徑,
多邊形頂點移動(Polygon vertex movement)
繞過障礙的最短路徑是繞過其拐角,這也是為什么我們在可視圖表示中采用拐角點,在這里即是導航網(wǎng)的頂點:
?
上圖中,路上只有一個障礙。當要繞過障礙時,黃色路徑會穿過一個頂點,粉色路徑(理想路徑)也一樣。然而,可視圖方法將直接連線起點和障礙拐角點,導航圖則要更多步。這些步驟不應該沿頂點走,因此路徑看起來不自然,有“抱墻”行為。
混合式移動(Hybrid movement)
對于多邊形的哪個部分可以用作尋路導航點,我們并沒有任何約束。你可以在一條邊上多加一些點,頂點也不錯,多邊形的中心點則基本沒用。下圖是采用了邊中點和頂點的一種混合方案:
?
注意要繞過障礙,需要穿過一個頂點,但在其他地方,則可以穿過邊中點。
路徑消除(Path smoothing)
只要移動成本是固定的,路徑消除相當容易。算法很簡單:如果點i到點i+2可見,刪除點i+1,循環(huán)直到鄰節(jié)點之間都不可見。剩下的只有繞過障礙物拐角點的導航點。這些點都是導航網(wǎng)的頂點。因此如果使用路徑消除,就不需要采用邊中點或多邊形中心點作為導航點,只要頂點就可以了
上面的例子中,路徑消除可以將黃色路徑變成粉紅色路徑。然而,尋路算法并不知道這些更短的路徑,因此它的決策不會優(yōu)化。導航網(wǎng)是一種近似地圖表示,而可視圖是一種精確地圖表示。縮短在導航網(wǎng)中找到的路,結(jié)果并不總能像通過可視圖找到的路一樣好。
分層(Hierarchical)
平面地圖只有一層。而游戲很少只有一層——往往有一個“圖塊”層,然后有一個“子圖塊”層,物體可以在圖塊中移動。然而我們通常只在高層尋路。你也可以添加更高的層,如“房間”。
在地圖表示中,節(jié)點越少,尋路越快。還有一種加快尋路速度的方法是多層次搜索。例如,要從你家到另一個城市的某個位置,你會找到一條路,從你的椅子到你的車,從車到街道,從街道到高速公路,從高速公路到城市邊緣,再從那兒到另一個城市,然后到一個街道,到一個停車場,最終到達目的地門前。此時,有下述幾層搜索:
街道層,你從一個位置走到附近的某個位置,但不會走出這條街。
城市層,你從一條街道走到另一條,直到找到高速公路。你無需擔心進入建筑物或停車場,也不用擔心在高速公路上行駛。
州層,在高速公路上,你從一個城市到另一個城市。在到達目的城市之前,你無需擔心城市內(nèi)的街道。
將問題分層,可以忽略很多選項。例如當從一個城市到另一個城市時,考慮路上每個城市的每條街道是很乏味的。相反,你可以忽略它們,只考慮高速公路,問題就變得很小且易于管理,解決也變得很快。
分層地圖在表示上有很多層。異構(gòu)層次結(jié)構(gòu)(heterogenous hierarchy)通常有固定層數(shù),各有不同特點。例如,《Ultima 5》有一個“世界”地圖,上邊有城市和地牢。你可以進入一個城市或地牢,這就進入地圖的第二層。另外,世界之上還有世界,從而是一個三層結(jié)構(gòu)。這些層可以是不同的類型(圖塊網(wǎng)格、可視圖、導航網(wǎng)、路標)。而同質(zhì)層次結(jié)構(gòu)(homogeneous hierarchy)層數(shù)任意,每層都有相同的特性。四叉樹和八叉樹就是同質(zhì)層次結(jié)構(gòu)的。
分層地圖中,尋路可能發(fā)生在幾個層次。例如,假設一個 1024×1024 的世界被劃分為 64×64 個“區(qū)域”,則可以這樣找到一條路徑,從玩家位置到區(qū)域邊緣,然后從一個區(qū)域到另一個區(qū)域,直到到達目的區(qū)域,然后從那個區(qū)域的邊緣到達目的位置。粗級別上,更容易找到長路徑,因為尋路算法沒有考慮所有的細節(jié)。當玩家穿過每個區(qū)域時,可以再次調(diào)用尋路算法,找一個短路徑。保證問題規(guī)模很小,尋路算法就可以運行得更快。
你可以結(jié)合使用分層和圖搜索算法,如A*,但是不需要每一層都采用一樣的算法。對一些小的層級,你可以預算所有節(jié)點間的最短路(用Floyd-Warshall或其他算法)。在分層地圖中,通常找不到最優(yōu)路徑,但一般都接近最優(yōu)。
還有個類似的方法是改變分辨率。首先,繪制低分辨率路徑。當接近一個點時,用高分辨率精化路徑。這個方法可以結(jié)合路徑拼接使用,以避免移動障礙。
一些文章:《“龍騰世紀:起源”中的尋路算法》解釋某商業(yè)游戲中使用的幾種分層方法,《采用線性預處理的超快最短路查詢》在道路圖中使用“運輸節(jié)點”[PDF],《游戲網(wǎng)格地圖的運輸節(jié)點》、《分層A*:有效搜索抽象層》、《道路網(wǎng)路線規(guī)劃》(Dominic Schulte的博士論文),逐層注解A*(第一部分,第二部分,源代碼)。
環(huán)繞式地圖(Wraparound maps)
如果你的世界是球形或環(huán)形的,物體可以從地圖的一端繞到另一端。最短路可能在任一方向,因此必須探索所有的方向。如果用網(wǎng)格,環(huán)繞時可以用啟發(fā)式方法。此時,我們不用abs(x1 – x2),而采用min(abs(x1 – x2), (x1+mapsize) – x2, (x2+mapsize) – x1),即考慮三種情況的最小值:待在地圖上不繞行,x1在左邊時繞行,或x2在左邊時繞行。繞行每個軸時都這樣做。本質(zhì)上來說,你計算啟發(fā)值時,假設地圖與其副本鄰接。
連通組件(Connected Components)
有些游戲地圖中,起點和終點之間根本就無路可通。如果用A*找路,它會探索圖的很大一個子集,才能確定根本沒路。如果可以預先分析地圖,用不同的標記標識出所有的連通子圖,那么在找路之前,首先檢查起點和終點是否都在同一個子圖中。如果不在,那么這兩者之間無路可通。另外分層尋路在這也可以用,特別是子圖之間有單向邊時。
道路圖(Road maps)
如果你的角色單元只能在道路上走,你可能需要提供A*道路和交叉口的信息。每個交叉口是圖上的一個節(jié)點,每條路是圖上一條邊。A*找從交叉口到交叉口的路,這比用網(wǎng)格表示要快得多。
有時,角色單元的起點和終點可能不在交叉口。此時,每次運行A*時,都要修改點/邊圖(和可視圖和導航圖采用的技術(shù)一樣),將起點和終點作為新節(jié)點加到圖中,然后在這兩個點和最近的交叉點之間連線。尋路結(jié)束后,再刪除這些額外的節(jié)點和邊,這樣圖在下次調(diào)用A*時還可以使用。
?
上圖中,交叉口是尋路圖中的節(jié)點,節(jié)點之間的道路是邊,且每條邊都應給定道路行駛距離。在這個框架中,你可以把單向道路作為圖上的單向邊。
如果你想給轉(zhuǎn)向分配成本,你可以稍稍擴展這個框架:將原來單一的位置節(jié)點,變?yōu)?lt;位置,方向>節(jié)點(靜態(tài)空間的一個點),其中方向指你到達那個位置后所面向的方向;將原來從X到Y(jié)的邊,換成從<X,方向>到<Y,方向>的邊(代表直行),和從<X,方向1>到<Y,方向2>的邊(代表轉(zhuǎn)向)。每條邊都或者代表直行,或者代表轉(zhuǎn)向,不可能兩者都是。然后你可以給代表轉(zhuǎn)向的邊分配成本。
如果你還要考慮轉(zhuǎn)向限制,如“只能右轉(zhuǎn)”,你可以改變上述框架,即結(jié)合使用那兩種邊,每個轉(zhuǎn)向邊之后都是直行。例如,在這個框架中,你想表示一個限制“只能右轉(zhuǎn)”:添加一個直行邊<X,北>到<Y,北>,一個轉(zhuǎn)向邊<X,北>到<Z,東>,轉(zhuǎn)向之后是直行。不要添加<X,北>到任何向西的邊,因為這意味著左轉(zhuǎn),也不要添加任何向南的邊,因為這意味著掉頭。
利用上述框架,可以對一個大型市中心建模,其中有單向道路,特定路口轉(zhuǎn)向限制(通常禁止掉頭,有時禁止左轉(zhuǎn)),以及轉(zhuǎn)向成本(建模在右轉(zhuǎn)之前減速和等待行人)。相比網(wǎng)格圖,道路圖中A*找路相當快,因為每個節(jié)點處的選擇很少,而且圖上的節(jié)點也相對較少。
跳躍鏈(Skip links)
基于網(wǎng)格創(chuàng)建的尋路圖,一般給每個位置分配一個頂點,給相鄰位置之間的每個可能的移動分配一條邊。然而邊不一定必須是相鄰頂點之間的,“跳躍鏈”或“快捷鏈”就是非相鄰點之間的邊,它可以加快尋路進程。
跳躍鏈的移動成本怎么算呢?有兩種方法:
使成本匹配最優(yōu)路徑的移動成本。這保留了A*的優(yōu)良特性,如尋找最優(yōu)路徑。為了將A*推向正確的方向,要打破跳躍鏈和常規(guī)鏈之間的關(guān)聯(lián),即將跳躍鏈的成本減少1%左右。
使成本匹配啟發(fā)成本。這對性能有很大影響,但是放棄了最優(yōu)路徑。
添加跳躍鏈類似于分層地圖,花費更少的精力,但往往能給你一樣的性能。
對于有地下室和走廊的網(wǎng)格圖,矩形對稱性縮減和跳躍點搜索提供兩種方法建立跳躍鏈。矩形對稱性縮減(Rectangular Symmetry Reduction)靜態(tài)建立附加邊(他們稱為宏邊),然后調(diào)用標準的圖搜索算法。跳躍點搜索(Jump Point Search)動態(tài)建立長邊,是圖搜索算法的一部分。對于道路圖和其他類型的圖,抽象分層值得一看。
路標(Waypoints)
路標是路徑上的一個點。路標可以具體到每條路,或者是游戲地圖的一部分。路標可以手動輸入或自動計算。很多實時策略游戲中,玩家點擊就可以手動添加特定路徑的路標。當自動計算時,路標可以簡化路徑表示。地圖設計者可以在地圖上人工添加路標(或“信標燈”),以標識好路徑的位置,或者也可以用算法自動標識。
使用跳躍鏈是為了加快尋路,因此跳躍鏈應當放在設計者設置的路標之間,這可以使其利益最大化。
如果路標不是很多,可以預算每對路標之間的最短路(用全對最短路算法,不用A*)。常見的情況就是,一個角色單元先按照自己的路徑到達一個路標,然后按照預算的路標間的最短路走,最后離開路標這個高速路,走自己的路到達目標。
如果路標或跳躍鏈的成本錯誤,可能導致找到次優(yōu)路徑。有時我們可以通過后期處理或在移動中,消除一個糟糕的路徑。
圖形格式建議(Graph Format Recommendations)
一開始你在已使用的游戲世界表示中尋路,如果你不滿意,考慮將游戲世界轉(zhuǎn)換為方便尋路的另一種表示形式。
很多網(wǎng)格游戲中,地圖的很多大塊區(qū)域移動成本都是均勻的,但是A*不知道這些,并且浪費時間來探索它們。創(chuàng)建一個簡單圖(導航網(wǎng),可視圖,或者網(wǎng)格圖的分層表示)可以幫助A*。
移動成本固定時,可視圖產(chǎn)生的是最優(yōu)路徑,且A*運行很快,但是邊存儲耗費大量內(nèi)存。網(wǎng)格允許移動成本有細微改變(地形,斜坡,懲罰用的危險區(qū)域等),邊存儲耗費內(nèi)存少,但點存儲耗費很多內(nèi)存,而且A*可能很慢。導航網(wǎng)居于兩者之間。在大塊區(qū)域移動成本均勻時,它效果很好,而且允許移動成本的些微改變,還能產(chǎn)生合理的路徑。這些路徑并不總?cè)缈梢晥D產(chǎn)生的一樣短,但是通常都是合理的。分層地圖采用多層表示來處理長距離內(nèi)的大致路徑,以及短距離內(nèi)的詳細路徑。
你可以讀讀這篇很形象的文章,了解更多關(guān)于導航網(wǎng)的知識。注意這篇文章比較了:(a)僅保持可行多邊形和僅保持導航點,(b)沿頂點走和沿多邊形中心點走。這些大多是正交的。保持可行多邊形有利于后期動態(tài)調(diào)整路徑,但是并非所有的游戲都需要。使用頂點更有利于避免障礙,并且如果你采用了路徑消除,還不會影響路徑質(zhì)量。如果沒有路徑消除,邊的效果可能更好,所以考慮邊或是邊+頂點。
除了給網(wǎng)格地圖構(gòu)建一個獨立的非網(wǎng)格表示,你也可以改變A*,使其更好得理解成本均勻分布的網(wǎng)格地圖。可以參照跳點搜索在方格上加速A*的方法,以及Theta*在網(wǎng)格上生成非網(wǎng)格移動的方法。
總結(jié)
以上是生活随笔為你收集整理的游戏中常用的寻路算法(6):地图表示的全部內(nèi)容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 游戏编程设计模式-state
- 下一篇: 游戏中常用的寻路算法(5)预先计算好的路