A*算法介绍
1?導(dǎo)言
移動(dòng)一個(gè)簡(jiǎn)單的物體(object)看起來(lái)是容易的。而路徑搜索是復(fù)雜的。為什么涉及到路徑搜索就產(chǎn)生麻煩了?考慮以下情況:
?
?
物體(unit)最初位于地圖的底端并且嘗試向頂部移動(dòng)。物體掃描的區(qū)域中(粉紅色部分)沒(méi)有任何東西顯示它不能向上移動(dòng),因此它持續(xù)向上移動(dòng)。在靠近頂部時(shí),它探測(cè)到一個(gè)障礙物然后改變移動(dòng)方向。然后它沿著U形障礙物找到它的紅色的路徑。相反的,一個(gè)路徑搜索器(pathfinder)將會(huì)掃描一個(gè)更大的區(qū)域(淡藍(lán)色部分),但是它能做到不讓物體(unit)走向凹形障礙物而找到一條更短的路徑(藍(lán)色路徑)。
然而你可以擴(kuò)展一個(gè)運(yùn)動(dòng)算法,用于對(duì)付上圖所示的障礙物。或者避免制造凹形障礙,或者把凹形出口標(biāo)識(shí)為危險(xiǎn)的(只有當(dāng)目的地在里面時(shí)才進(jìn)去):
?
?
比起一直等到最后一刻才發(fā)現(xiàn)問(wèn)題,路徑搜索器讓你提前作出計(jì)劃。不帶路徑搜索的運(yùn)動(dòng)(movement)可以在很多種情形下工作,同時(shí)可以擴(kuò)展到更多的情形,但是路徑搜索是一種更常用的解決更多問(wèn)題的方法。
1.1?算法
計(jì)算機(jī)科學(xué)教材中的路徑搜索算法在數(shù)學(xué)視角的圖上工作——由邊聯(lián)結(jié)起來(lái)的結(jié)點(diǎn)的集合。一個(gè)基于圖塊(tile)拼接的游戲地圖可以看成是一個(gè)圖,每個(gè)圖塊(tile)是一個(gè)結(jié)點(diǎn),并在每個(gè)圖塊之間畫一條邊:
?
?
目前,我會(huì)假設(shè)我們使用二維網(wǎng)格(grid)。稍后我將討論如何在你的游戲之外建立其他類型的圖。
許多AI領(lǐng)域或算法研究領(lǐng)域中的路徑搜索算法是基于任意(arbitrary)的圖設(shè)計(jì)的,而不是基于網(wǎng)格(grid-based)的圖。我們可以找到一些能使用網(wǎng)格地圖的特性的東西。有一些我們認(rèn)為是常識(shí),而算法并不理解。例如,我們知道一些和方向有關(guān)的東西:一般而言,如果兩個(gè)物體距離越遠(yuǎn),那么把其中一個(gè)物體向另一個(gè)移動(dòng)將花越多的時(shí)間;并且我們知道地圖中沒(méi)有任何秘密通道可以從一個(gè)地點(diǎn)通向另一個(gè)地點(diǎn)。(我假設(shè)沒(méi)有,如果有的話,將會(huì)很難找到一條好的路徑,因?yàn)槟悴⒉恢酪獜暮翁庨_始。)
1.2?Dijkstra算法與最佳優(yōu)先搜索
Dijkstra算法從物體所在的初始點(diǎn)開始,訪問(wèn)圖中的結(jié)點(diǎn)。它迭代檢查待檢查結(jié)點(diǎn)集中的結(jié)點(diǎn),并把和該結(jié)點(diǎn)最靠近的尚未檢查的結(jié)點(diǎn)加入待檢查結(jié)點(diǎn)集。該結(jié)點(diǎn)集從初始結(jié)點(diǎn)向外擴(kuò)展,直到到達(dá)目標(biāo)結(jié)點(diǎn)。Dijkstra算法保證能找到一條從初始點(diǎn)到目標(biāo)點(diǎn)的最短路徑,只要所有的邊都有一個(gè)非負(fù)的代價(jià)值。(我說(shuō)“最短路徑”是因?yàn)榻?jīng)常會(huì)出現(xiàn)許多差不多短的路徑。)在下圖中,粉紅色的結(jié)點(diǎn)是初始結(jié)點(diǎn),藍(lán)色的是目標(biāo)點(diǎn),而類菱形的有色區(qū)域(注:原文是teal areas)則是Dijkstra算法掃描過(guò)的區(qū)域。顏色最淡的區(qū)域是那些離初始點(diǎn)最遠(yuǎn)的,因而形成探測(cè)過(guò)程(exploration)的邊境(frontier):
?
?
最佳優(yōu)先搜索(BFS)算法按照類似的流程運(yùn)行,不同的是它能夠評(píng)估(稱為啟發(fā)式的)任意結(jié)點(diǎn)到目標(biāo)點(diǎn)的代價(jià)。與選擇離初始結(jié)點(diǎn)最近的結(jié)點(diǎn)不同的是,它選擇離目標(biāo)最近的結(jié)點(diǎn)。BFS不能保證找到一條最短路徑。然而,它比Dijkstra算法快的多,因?yàn)樗昧艘粋€(gè)啟發(fā)式函數(shù)(heuristic function)快速地導(dǎo)向目標(biāo)結(jié)點(diǎn)。例如,如果目標(biāo)位于出發(fā)點(diǎn)的南方,BFS將趨向于導(dǎo)向南方的路徑。在下面的圖中,越黃的結(jié)點(diǎn)代表越高的啟發(fā)式值(移動(dòng)到目標(biāo)的代價(jià)高),而越黑的結(jié)點(diǎn)代表越低的啟發(fā)式值(移動(dòng)到目標(biāo)的代價(jià)低)。這表明了與Dijkstra?算法相比,BFS運(yùn)行得更快。
?
?
然而,這兩個(gè)例子都僅僅是最簡(jiǎn)單的情況——地圖中沒(méi)有障礙物,最短路徑是直線的。現(xiàn)在我們來(lái)考慮前邊描述的凹型障礙物。Dijkstra算法運(yùn)行得較慢,但確實(shí)能保證找到一條最短路徑:
?
?
另一方面,BFS運(yùn)行得較快,但是它找到的路徑明顯不是一條好的路徑:
?
?
問(wèn)題在于BFS是基于貪心策略的,它試圖向目標(biāo)移動(dòng)盡管這不是正確的路徑。由于它僅僅考慮到達(dá)目標(biāo)的代價(jià),而忽略了當(dāng)前已花費(fèi)的代價(jià),于是盡管路徑變得很長(zhǎng),它仍然繼續(xù)走下去。
結(jié)合兩者的優(yōu)點(diǎn)不是更好嗎?1968年發(fā)明的A*算法就是把啟發(fā)式方法(heuristic approaches)如BFS,和常規(guī)方法如Dijsktra算法結(jié)合在一起的算法。有點(diǎn)不同的是,類似BFS的啟發(fā)式方法經(jīng)常給出一個(gè)近似解而不是保證最佳解。然而,盡管A*基于無(wú)法保證最佳解的啟發(fā)式方法,A*卻能保證找到一條最短路徑。
1.3?A*算法
我將集中討論A*算法。A*是路徑搜索中最受歡迎的選擇,因?yàn)樗喈?dāng)靈活,并且能用于多種多樣的情形之中。
和其它的圖搜索算法一樣,A*潛在地搜索圖中一個(gè)很大的區(qū)域。和Dijkstra一樣,A*能用于搜索最短路徑。和BFS一樣,A*能用啟發(fā)式函數(shù)(注:原文為heuristic)引導(dǎo)它自己。在簡(jiǎn)單的情況中,它和BFS一樣快。
?
?
在凹型障礙物的例子中,A*找到一條和Dijkstra算法一樣好的路徑:
?
?
成功的秘決在于,它把Dijkstra算法(靠近初始點(diǎn)的結(jié)點(diǎn))和BFS算法(靠近目標(biāo)點(diǎn)的結(jié)點(diǎn))的信息塊結(jié)合起來(lái)。在討論A*的標(biāo)準(zhǔn)術(shù)語(yǔ)中,g(n)表示從初始結(jié)點(diǎn)到任意結(jié)點(diǎn)n的代價(jià),h(n)表示從結(jié)點(diǎn)n到目標(biāo)點(diǎn)的啟發(fā)式評(píng)估代價(jià)(heuristic estimated cost)。在上圖中,yellow(h)表示遠(yuǎn)離目標(biāo)的結(jié)點(diǎn)而teal(g)表示遠(yuǎn)離初始點(diǎn)的結(jié)點(diǎn)。當(dāng)從初始點(diǎn)向目標(biāo)點(diǎn)移動(dòng)時(shí),A*權(quán)衡這兩者。每次進(jìn)行主循環(huán)時(shí),它檢查f(n)最小的結(jié)點(diǎn)n,其中f(n) = g(n) + h(n)。
2?啟發(fā)式算法
啟發(fā)式函數(shù)h(n)告訴A*從任意結(jié)點(diǎn)n到目標(biāo)結(jié)點(diǎn)的最小代價(jià)評(píng)估值。選擇一個(gè)好的啟發(fā)式函數(shù)是重要的。
2.1?A*對(duì)啟發(fā)式函數(shù)的使用
啟發(fā)式函數(shù)可以控制A*的行為:
- 一種極端情況,如果h(n)是0,則只有g(shù)(n)起作用,此時(shí)A*演變成Dijkstra算法,這保證能找到最短路徑。
- 如果h(n)經(jīng)常都比從n移動(dòng)到目標(biāo)的實(shí)際代價(jià)小(或者相等),則A*保證能找到一條最短路徑。h(n)越小,A*擴(kuò)展的結(jié)點(diǎn)越多,運(yùn)行就得越慢。
- 如果h(n)精確地等于從n移動(dòng)到目標(biāo)的代價(jià),則A*將會(huì)僅僅尋找最佳路徑而不擴(kuò)展別的任何結(jié)點(diǎn),這會(huì)運(yùn)行得非常快。盡管這不可能在所有情況下發(fā)生,你仍可以在一些特殊情況下讓它們精確地相等(譯者:指讓h(n)精確地等于實(shí)際值)。只要提供完美的信息,A*會(huì)運(yùn)行得很完美,認(rèn)識(shí)這一點(diǎn)很好。
- 如果h(n)有時(shí)比從n移動(dòng)到目標(biāo)的實(shí)際代價(jià)高,則A*不能保證找到一條最短路徑,但它運(yùn)行得更快。
- 另一種極端情況,如果h(n)比g(n)大很多,則只有h(n)起作用,A*演變成BFS算法。
所以我們得到一個(gè)很有趣的情況,那就是我們可以決定我們想要從A*中獲得什么。理想情況下(注:原文為At exactly the right point),我們想最快地得到最短路徑。如果我們的目標(biāo)太低,我們?nèi)詴?huì)得到最短路徑,不過(guò)速度變慢了;如果我們的目標(biāo)太高,那我們就放棄了最短路徑,但A*運(yùn)行得更快。
在游戲中,A*的這個(gè)特性非常有用。例如,你會(huì)發(fā)現(xiàn)在某些情況下,你希望得到一條好的路徑("good" path)而不是一條完美的路徑("perfect" path)。為了權(quán)衡g(n)和h(n),你可以修改任意一個(gè)。
注:在學(xué)術(shù)上,如果啟發(fā)式函數(shù)值是對(duì)實(shí)際代價(jià)的低估,A*算法被稱為簡(jiǎn)單的A算法(原文為simply A)。然而,我繼續(xù)稱之為A*,因?yàn)樵趯?shí)現(xiàn)上是一樣的,并且在游戲編程領(lǐng)域并不區(qū)別A和A*。
2.2?速度還是精確度?
A*改變它自己行為的能力基于啟發(fā)式代價(jià)函數(shù),啟發(fā)式函數(shù)在游戲中非常有用。在速度和精確度之間取得折衷將會(huì)讓你的游戲運(yùn)行得更快。在很多游戲中,你并不真正需要得到最好的路徑,僅需要近似的就足夠了。而你需要什么則取決于游戲中發(fā)生著什么,或者運(yùn)行游戲的機(jī)器有多快。
假設(shè)你的游戲有兩種地形,平原和山地,在平原中的移動(dòng)代價(jià)是1而在山地則是3。A* is going to search three times as far along flat land as it does along mountainous land.?這是因?yàn)橛锌赡苡幸粭l沿著平原到山地的路徑。把兩個(gè)鄰接點(diǎn)之間的評(píng)估距離設(shè)為1.5可以加速A*的搜索過(guò)程。然后A*會(huì)將3和1.5比較,這并不比把3和1比較差。It is not as dissatisfied with mountainous terrain, so it won't spend as much time trying to find a way around it. Alternatively, you can speed up up A*'s search by decreasing the amount it searches for paths around mountains―just tell A* that the movement cost on mountains is 2 instead of 3. Now it will search only twice as far along the flat terrain as along mountainous terrain. Either approach gives up ideal paths to get something quicker.
速度和精確度之間的選擇前不是靜態(tài)的。你可以基于CPU的速度、用于路徑搜索的時(shí)間片數(shù)、地圖上物體(units)的數(shù)量、物體的重要性、組(group)的大小、難度或者其他任何因素來(lái)進(jìn)行動(dòng)態(tài)的選擇。取得動(dòng)態(tài)的折衷的一個(gè)方法是,建立一個(gè)啟發(fā)式函數(shù)用于假定通過(guò)一個(gè)網(wǎng)格空間的最小代價(jià)是1,然后建立一個(gè)代價(jià)函數(shù)(cost function)用于測(cè)量(scales):
g’(n) = 1 + alpha * ( g(n) – 1?)
如果alpha是0,則改進(jìn)后的代價(jià)函數(shù)的值總是1。這種情況下,地形代價(jià)被完全忽略,A*工作變成簡(jiǎn)單地判斷一個(gè)網(wǎng)格可否通過(guò)。如果alpha是1,則最初的代價(jià)函數(shù)將起作用,然后你得到了A*的所有優(yōu)點(diǎn)。你可以設(shè)置alpha的值為0到1的任意值。
你也可以考慮對(duì)啟發(fā)式函數(shù)的返回值做選擇:絕對(duì)最小代價(jià)或者期望最小代價(jià)。例如,如果你的地圖大部分地形是代價(jià)為2的草地,其它一些地方是代價(jià)為1的道路,那么你可以考慮讓啟發(fā)式函數(shù)不考慮道路,而只返回2*距離。
速度和精確度之間的選擇并不是全局的。在地圖上的某些區(qū)域,精確度是重要的,你可以基于此進(jìn)行動(dòng)態(tài)選擇。例如,假設(shè)我們可能在某點(diǎn)停止重新計(jì)算路徑或者改變方向,則在接近當(dāng)前位置的地方,選擇一條好的路徑則是更重要的,因此為何要對(duì)后續(xù)路徑的精確度感到厭煩?或者,對(duì)于在地圖上的一個(gè)安全區(qū)域,最短路徑也許并不十分重要,但是當(dāng)從一個(gè)敵人的村莊逃跑時(shí),安全和速度是最重要的。(譯者注:譯者認(rèn)為這里指的是,在安全區(qū)域,可以考慮不尋找精確的最短路徑而取近似路徑,因此尋路快;但在危險(xiǎn)區(qū)域,逃跑的安全性和逃跑速度是重要的,即路徑的精確度是重要的,因此可以多花點(diǎn)時(shí)間用于尋找精確路徑。)
2.3?衡量單位
A*計(jì)算f(n) = g(n) + h(n)。為了對(duì)這兩個(gè)值進(jìn)行相加,這兩個(gè)值必須使用相同的衡量單位。如果g(n)用小時(shí)來(lái)衡量而h(n)用米來(lái)衡量,那么A*將會(huì)認(rèn)為g或者h(yuǎn)太大或者太小,因而你將不能得到正確的路徑,同時(shí)你的A*算法將運(yùn)行得更慢。
2.4?精確的啟發(fā)式函數(shù)
如果你的啟發(fā)式函數(shù)精確地等于實(shí)際最佳路徑(optimal path),如下一部分的圖中所示,你會(huì)看到此時(shí)A*擴(kuò)展的結(jié)點(diǎn)將非常少。A*算法內(nèi)部發(fā)生的事情是:在每一結(jié)點(diǎn)它都計(jì)算f(n) = g(n) + h(n)。當(dāng)h(n)精確地和g(n)匹配(譯者注:原文為match)時(shí),f(n)的值在沿著該路徑時(shí)將不會(huì)改變。不在正確路徑(right path)上的所有結(jié)點(diǎn)的f值均大于正確路徑上的f值(譯者注:正確路徑在這里應(yīng)該是指最短路徑)。如果已經(jīng)有較低f值的結(jié)點(diǎn),A*將不考慮f值較高的結(jié)點(diǎn),因此它肯定不會(huì)偏離最短路徑。
2.4.1?預(yù)計(jì)算的精確啟發(fā)式函數(shù)
構(gòu)造精確啟發(fā)函數(shù)的一種方法是預(yù)先計(jì)算任意一對(duì)結(jié)點(diǎn)之間最短路徑的長(zhǎng)度。在許多游戲的地圖中這并不可行。然后,有幾種方法可以近似模擬這種啟發(fā)函數(shù):
- Fit a coarse grid on top of the fine grid. Precompute the shortest path between any pair of coarse grid locations.
- Precompute the shortest path between any pair of?waypoints. This is a generalization of the coarse grid approach.
(譯者:此處不好翻譯,暫時(shí)保留原文)
然后添加一個(gè)啟發(fā)函數(shù)h’用于評(píng)估從任意位置到達(dá)鄰近導(dǎo)航點(diǎn)(waypoints)的代價(jià)。(如果愿意,后者也可以通過(guò)預(yù)計(jì)算得到。)最終的啟發(fā)式函數(shù)可以是:
h(n) = h'(n, w1) + distance(w1, w2), h'(w2, goal)
或者如果你希望一個(gè)更好但是更昂貴的啟發(fā)式函數(shù),則分別用靠近結(jié)點(diǎn)和目標(biāo)的所有的w1,w2對(duì)對(duì)上式進(jìn)行求值。(譯者注:原文為or if you want a better but more expensive heuristic, evaluate the above with all pairs w1, w2 that are close to the node and the goal, respectively.)
2.4.2?線性精確啟發(fā)式算法
在特殊情況下,你可以不通過(guò)預(yù)計(jì)算而讓啟發(fā)式函數(shù)很精確。如果你有一個(gè)不存在障礙物和slow地形,那么從初始點(diǎn)到目標(biāo)的最短路徑應(yīng)該是一條直線。
如果你正使用簡(jiǎn)單的啟發(fā)式函數(shù)(我們不知道地圖上的障礙物),則它應(yīng)該和精確的啟發(fā)式函數(shù)相符合(譯者注:原文為match)。如果不是這樣,則你會(huì)遇到衡量單位的問(wèn)題,或者你所選擇的啟發(fā)函數(shù)類型的問(wèn)題。
2.5?網(wǎng)格地圖中的啟發(fā)式算法
在網(wǎng)格地圖中,有一些眾所周知的啟發(fā)式函數(shù)。
2.5.1?曼哈頓距離
標(biāo)準(zhǔn)的啟發(fā)式函數(shù)是曼哈頓距離(Manhattan distance)。考慮你的代價(jià)函數(shù)并找到從一個(gè)位置移動(dòng)到鄰近位置的最小代價(jià)D。因此,我的游戲中的啟發(fā)式函數(shù)應(yīng)該是曼哈頓距離的D倍:
???????H(n) = D * (abs ( n.x – goal.x ) + abs ( n.y – goal.y ) )
你應(yīng)該使用符合你的代價(jià)函數(shù)的衡量單位。
?
?
(Note: the above image has a?tie-breaker?added to the heuristic.}
(譯者注:曼哈頓距離——兩點(diǎn)在南北方向上的距離加上在東西方向上的距離,即D(I,J)=|XI-XJ|+|YI-YJ|。對(duì)于一個(gè)具有正南正北、正東正西方向規(guī)則布局的城鎮(zhèn)街道,從一點(diǎn)到達(dá)另一點(diǎn)的距離正是在南北方向上旅行的距離加上在東西方向上旅行的距離因此曼哈頓距離又稱為出租車距離,曼哈頓距離不是距離不變量,當(dāng)坐標(biāo)軸變動(dòng)時(shí),點(diǎn)間的距離就會(huì)不同——百度知道)
2.5.2?對(duì)角線距離
如果在你的地圖中你允許對(duì)角運(yùn)動(dòng)那么你需要一個(gè)不同的啟發(fā)函數(shù)。(4 east, 4 north)的曼哈頓距離將變成8*D。然而,你可以簡(jiǎn)單地移動(dòng)(4 northeast)代替,所以啟發(fā)函數(shù)應(yīng)該是4*D。這個(gè)函數(shù)使用對(duì)角線,假設(shè)直線和對(duì)角線的代價(jià)都是D:
h(n) = D * max(abs(n.x - goal.x), abs(n.y - goal.y))
?
?
如果對(duì)角線運(yùn)動(dòng)的代價(jià)不是D,但類似于D2 = sqrt(2) * D,則上面的啟發(fā)函數(shù)不準(zhǔn)確。你需要一些更準(zhǔn)確(原文為sophisticated)的東西:
h_diagonal(n) = min(abs(n.x - goal.x), abs(n.y - goal.y))
h_straight(n) = (abs(n.x - goal.x) + abs(n.y - goal.y))
h(n) = D2 * h_diagonal(n) + D * (h_straight(n) - 2*h_diagonal(n)))
這里,我們計(jì)算h_diagonal(n):沿著斜線可以移動(dòng)的步數(shù);h_straight(n):曼哈頓距離;然后合并這兩項(xiàng),讓所有的斜線步都乘以D2,剩下的所有直線步(注意這里是曼哈頓距離的步數(shù)減去2倍的斜線步數(shù))都乘以D。
2.5.3?歐幾里得距離
如果你的單位可以沿著任意角度移動(dòng)(而不是網(wǎng)格方向),那么你也許應(yīng)該使用直線距離:
h(n) = D * sqrt((n.x-goal.x)^2 + (n.y-goal.y)^2)
然而,如果是這樣的話,直接使用A*時(shí)將會(huì)遇到麻煩,因?yàn)榇鷥r(jià)函數(shù)g不會(huì)match啟發(fā)函數(shù)h。因?yàn)闅W幾里得距離比曼哈頓距離和對(duì)角線距離都短,你仍可以得到最短路徑,不過(guò)A*將運(yùn)行得更久一些:
?
?
2.5.4?平方后的歐幾里得距離
我曾經(jīng)看到一些A*的網(wǎng)頁(yè),其中提到讓你通過(guò)使用距離的平方而避免歐幾里得距離中昂貴的平方根運(yùn)算:
h(n) = D * ((n.x-goal.x)^2 + (n.y-goal.y)^2)
不要這樣做!這明顯地導(dǎo)致衡量單位的問(wèn)題。當(dāng)A*計(jì)算f(n) = g(n) + h(n),距離的平方將比g的代價(jià)大很多,并且你會(huì)因?yàn)閱l(fā)式函數(shù)評(píng)估值過(guò)高而停止。對(duì)于更長(zhǎng)的距離,這樣做會(huì)靠近g(n)的極端情況而不再計(jì)算任何東西,A*退化成BFS:
?
?
2.5.5 Breaking ties?Breaking ties
導(dǎo)致低性能的一個(gè)原因來(lái)自于啟發(fā)函數(shù)的ties(注:這個(gè)詞實(shí)在不知道應(yīng)該翻譯為什么)。當(dāng)某些路徑具有相同的f值的時(shí)候,它們都會(huì)被搜索(explored),盡管我們只需要搜索其中的一條:
Ties in f values.
為了解決這個(gè)問(wèn)題,我們可以為啟發(fā)函數(shù)添加一個(gè)附加值(譯者注:原文為small tie breaker)。附加值對(duì)于結(jié)點(diǎn)必須是確定性的(也就是說(shuō),不能是隨機(jī)的數(shù)),而且它必須讓f值體現(xiàn)區(qū)別。因?yàn)锳*對(duì)f值排序,讓f值不同意味著只有一個(gè)"equivalent"的f值會(huì)被檢測(cè)。
一種添加附加值的方式是稍微改變(譯者注:原文為nudge)h的衡量單位。如果我們減少衡量單位(譯者注:原文為scale it downwards),那么當(dāng)我們朝著目標(biāo)移動(dòng)的時(shí)候f將逐漸增加。很不幸,這意味著A*傾向于擴(kuò)展到靠近初始點(diǎn)的結(jié)點(diǎn),而不是靠近目標(biāo)的結(jié)點(diǎn)。我們可以增加衡量單位(譯者注:原文為scale it downwards scale h upwards slightly)(甚至是0.1%),A*就會(huì)傾向于擴(kuò)展到靠近目標(biāo)的結(jié)點(diǎn)。
heuristic?*= (1.0 + p)
選擇因子p使得p <?移動(dòng)一步(step)的最小代價(jià)?/?期望的最長(zhǎng)路徑長(zhǎng)度。假設(shè)你不希望你的路徑超過(guò)1000步(step),你可以使p = 1 / 1000。添加這個(gè)附加值的結(jié)果是,A*比以前搜索的結(jié)點(diǎn)更少了。
Tie-breaking scaling added to heuristic.
當(dāng)存在障礙物時(shí),當(dāng)然仍要在它們周圍尋找路徑,但要意識(shí)到,當(dāng)繞過(guò)障礙物以后,A*搜索的區(qū)域非常少:
Tie-breaking scaling added to heuristic, works nicely with obstacles.
Steven van Dijk建議,一個(gè)更直截了當(dāng)?shù)姆椒ㄊ前裩傳遞到比較函數(shù)(comparison function)。當(dāng)f值相等時(shí),比較函數(shù)檢查h,然后添加附加值。
一個(gè)不同的添加附加值的方法是,傾向于從初始點(diǎn)到目標(biāo)點(diǎn)的連線(直線):
dx1 = current.x - goal.x
dy1 = current.y - goal.y
dx2 = start.x - goal.x
dy2 = start.y - goal.y
cross?= abs(dx1*dy2 - dx2*dy1)
heuristic?+= cross*0.001
這段代碼計(jì)算初始-目標(biāo)向量(start to goal vector)和當(dāng)前-目標(biāo)向量(current point to goal vector)的向量叉積(vector cross-product)。When these vectors don't line up, the cross product will be larger.結(jié)果是,這段代碼選擇的路徑稍微傾向于從初始點(diǎn)到目標(biāo)點(diǎn)的直線。當(dāng)沒(méi)有障礙物時(shí),A*不僅搜索很少的區(qū)域,而且它找到的路徑看起來(lái)非常棒:
Tie-breaking cross-product added to heuristic, produces pretty paths.
然而,因?yàn)檫@種附加值傾向于從初始點(diǎn)到目標(biāo)點(diǎn)的直線路徑,當(dāng)出現(xiàn)障礙物時(shí)將會(huì)出現(xiàn)奇怪的結(jié)果(注意這條路徑仍是最佳的,只是看起來(lái)很奇怪):
Tie-breaking cross-product added to heuristic, less pretty with obstacles.
為了交互地研究這種附加值方法的改進(jìn),請(qǐng)參考James Macgill的A*確applet(http://www.ccg.leeds.ac.uk/james/aStar/?)[如果鏈接無(wú)效,請(qǐng)使用這個(gè)鏡像(http://www.vision.ee.ethz.ch/~buc/astar/AStar.html)](譯者注:兩個(gè)鏈接均無(wú)效)。使用“Clear”以清除地圖,選擇地圖對(duì)角的兩個(gè)點(diǎn)。當(dāng)你使用“Classic A*”方法,你會(huì)看到附加值的效果。當(dāng)你使用“Fudge”方法,你會(huì)看到上面給啟發(fā)函數(shù)添加叉積后的效果。
然而另一種添加附加值的方法是,小心地構(gòu)造你的A*優(yōu)先隊(duì)列,使新插入的具有特殊f值的結(jié)點(diǎn)總是比那些以前插入的具有相同f值的舊結(jié)點(diǎn)要好一些。
你也許也想看看能夠更靈活地(譯者注:原文為sophisticated)添加附加值的AlphA*算法(http://home1.stofanet.dk/breese/papers.html),不過(guò)用這種算法得到的路徑是否能達(dá)到最佳仍在研究中。AlphA*具有較好的適應(yīng)性,而且可能比我在上面討論的附加值方法運(yùn)行得都要好。然而,我所討論的附加值方法非常容易實(shí)現(xiàn),所以從它們開始吧,如果你需要得到更好的效果,再去嘗試AlphA*。
2.5.6?區(qū)域搜索
如果你想搜索鄰近目標(biāo)的任意不確定結(jié)點(diǎn),而不是某個(gè)特定的結(jié)點(diǎn),你應(yīng)該建立一個(gè)啟發(fā)函數(shù)h’(x),使得h’(x)為h1(x), h2(x), h3(x)。。。的最小值,而這些h1, h2, h3是鄰近結(jié)點(diǎn)的啟發(fā)函數(shù)。然而,一種更快的方法是讓A*僅搜索目標(biāo)區(qū)域的中心。一旦你從OPEN集合中取得任意一個(gè)鄰近目標(biāo)的結(jié)點(diǎn),你就可以停止搜索并建立一條路徑了。
3 Implementation notes
3.1?概略
如果不考慮具體實(shí)現(xiàn)代碼,A*算法是相當(dāng)簡(jiǎn)單的。有兩個(gè)集合,OPEN集和CLOSED集。其中OPEN集保存待考查的結(jié)點(diǎn)。開始時(shí),OPEN集只包含一個(gè)元素:初始結(jié)點(diǎn)。CLOSED集保存已考查過(guò)的結(jié)點(diǎn)。開始時(shí),CLOSED集是空的。如果繪成圖,OPEN集就是被訪問(wèn)區(qū)域的邊境(frontier)而CLOSED集則是被訪問(wèn)區(qū)域的內(nèi)部(interior)。每個(gè)結(jié)點(diǎn)同時(shí)保存其父結(jié)點(diǎn)的指針因此我們可以知道它是如何被找到的。
在主循環(huán)中重復(fù)地從OPEN集中取出最好的結(jié)點(diǎn)n(f值最小的結(jié)點(diǎn))并檢查之。如果n是目標(biāo)結(jié)點(diǎn),則我們的任務(wù)完成了。否則,結(jié)點(diǎn)n被從OPEN集中刪除并加入CLOSED集。然后檢查它的鄰居n’。如果鄰居n’在CLOSED集中,那么它是已經(jīng)被檢查過(guò)的,所以我們不需要考慮它*;如果n’在OPEN集中,那么它是以后肯定會(huì)被檢查的,所以我們現(xiàn)在不考慮它*。否則,把它加入OPEN集,把它的父結(jié)點(diǎn)設(shè)為n。到達(dá)n’的路徑的代價(jià)g(n’),設(shè)定為g(n) + movementcost(n, n’)。
(*)這里我忽略了一個(gè)小細(xì)節(jié)。你確實(shí)需要檢查結(jié)點(diǎn)的g值是否更小了,如果是的話,需要重新打開(re-open)它。
OPEN = priority queue containing START
CLOSED = empty set
while?lowest rank in OPEN is not the GOAL:
??current?= remove lowest rank item from OPEN
??add?current to CLOSED
??for?neighbors of current:
????cost?= g(current) + movementcost(current, neighbor)
????if?neighbor in OPEN and cost less than g(neighbor):
??????remove?neighbor from OPEN, because new path is better
????if?neighbor in CLOSED and cost less than g(neighbor): **
??????remove?neighbor from CLOSED
????if?neighbor not in OPEN and neighbor not in CLOSED:
??????set?g(neighbor) to cost
??????add?neighbor to OPEN
??????set?priority queue rank to g(neighbor) + h(neighbor)
??????set?neighbor's parent to current
?
reconstruct?reverse path from goal to start
by?following parent pointers
(**) This should never happen if you have an admissible heuristic. However in games we often have inadmissible heuristics.
3.2?源代碼
我自己的(舊的)C++A*代碼是可用的:path.cpp (http://theory.stanford.edu/~amitp/ GameProgramming/path.cpp)和path.h (http://theory.stanford.edu/~amitp/GameProgramming/ path.h),但是不容易閱讀。還有一份更老的代碼(更慢的,但是更容易理解),和很多其它的A*實(shí)現(xiàn)一樣,它在Steve Woodcock'的游戲AI頁(yè)面(http://www.gameai.com/ai.html)。
在網(wǎng)上,你能找到C,C++,Visual Basic?,Java(http://www.cuspy.com/software/pathfinder/ doc/),Flash/Director/Lingo,?C#(http://www.codeproject.com/csharp/CSharpPathfind.asp), Delphi, Lisp, Python, Perl,?和Prolog?實(shí)現(xiàn)的A*代碼。一定的閱讀Justin Heyes-Jones的C++實(shí)現(xiàn)(http://www.geocities.com/jheyesjones/astar.html)。
3.3?集合的表示
你首先想到的用于實(shí)現(xiàn)OPEN集和CLOSED集的數(shù)據(jù)結(jié)構(gòu)是什么?如果你和我一樣,你可能想到“數(shù)組”。你也可能想到“鏈表”。我們可以使用很多種不同的數(shù)據(jù)結(jié)構(gòu),為了選擇一種,我們應(yīng)該考慮我們需要什么樣的操作。
在OPEN集上我們主要有三種操作:主循環(huán)重復(fù)選擇最好的結(jié)點(diǎn)并刪除它;訪問(wèn)鄰居結(jié)點(diǎn)時(shí)需要檢查它是否在集合里面;訪問(wèn)鄰居結(jié)點(diǎn)時(shí)需要插入新結(jié)點(diǎn)。插入和刪除最佳是優(yōu)先隊(duì)列(http://members.xoom.com/killough/heaps.html)的典型操作。
選擇哪種數(shù)據(jù)結(jié)構(gòu)不僅取決于操作,還取決于每種操作執(zhí)行的次數(shù)。檢查一個(gè)結(jié)點(diǎn)是否在集合中這一操作對(duì)每個(gè)被訪問(wèn)的結(jié)點(diǎn)的每個(gè)鄰居結(jié)點(diǎn)都執(zhí)行一次。刪除最佳操作對(duì)每個(gè)被訪問(wèn)的結(jié)點(diǎn)都執(zhí)行一次。被考慮到的絕大多數(shù)結(jié)點(diǎn)都會(huì)被訪問(wèn);不被訪問(wèn)的是搜索空間邊緣(fringe)的結(jié)點(diǎn)。當(dāng)評(píng)估數(shù)據(jù)結(jié)構(gòu)上面的這些操作時(shí),必須考慮fringe(F)的最大值。
另外,還有第四種操作,雖然執(zhí)行的次數(shù)相對(duì)很少,但還是必須實(shí)現(xiàn)的。如果正被檢查的結(jié)點(diǎn)已經(jīng)在OPEN集中(這經(jīng)常發(fā)生),并且如果它的f值比已經(jīng)在OPEN集中的結(jié)點(diǎn)要好(這很少見),那么OPEN集中的值必須被調(diào)整。調(diào)整操作包括刪除結(jié)點(diǎn)(f值不是最佳的結(jié)點(diǎn))和重插入。這兩個(gè)步驟必須被最優(yōu)化為一個(gè)步驟,這個(gè)步驟將移動(dòng)結(jié)點(diǎn)。
3.3.1?未排序數(shù)組或鏈表
最簡(jiǎn)單的數(shù)據(jù)結(jié)構(gòu)是未排序數(shù)組或鏈表。集合關(guān)系檢查操作(Membership test)很慢,掃描整個(gè)結(jié)構(gòu)花費(fèi)O(F)。插入操作很快,添加到末尾花費(fèi)O(1)。查找最佳元素(Finding the best element)很慢,掃描整個(gè)結(jié)構(gòu)花費(fèi)O(F)。對(duì)于數(shù)組,刪除最佳元素(Removing the best element)花費(fèi)O(F),而鏈表則是O(1)。調(diào)整操作中,查找結(jié)點(diǎn)花費(fèi)O(F),改變值花費(fèi)O(1)。
3.3.2?排序數(shù)組
為了加快刪除最掛操作,可以對(duì)數(shù)組進(jìn)行排序。集合關(guān)系檢查操作將變成O(log F),因?yàn)槲覀兛梢允褂谜郯氩檎摇2迦氩僮鲿?huì)很慢,為了給新元素騰出空間,需要花費(fèi)?O(F)以移動(dòng)所有的元素。查找最佳元素操作會(huì)很快,因?yàn)樗呀?jīng)在末尾了所以花費(fèi)是O(1)。如果我們保證最佳排序至數(shù)組的尾部(best sorts to the?end?of the array),刪除最佳元素操作花費(fèi)將是O(1)。調(diào)整操作中,查找結(jié)點(diǎn)花費(fèi)O(logF),改變值/位置花費(fèi)O(F)。
3.3.3?排序鏈表
在排序數(shù)組中,插入操作很慢。如果使用鏈表則可以加速該操作。集合關(guān)系檢查操作很慢,需要花費(fèi)O(F)用于掃描鏈表。插入操作是很快的,插入新元素只花費(fèi)O(1)時(shí)間,但是查找正確位置需要花費(fèi)O(F)。查找最佳元素很快,花費(fèi)O(1)時(shí)間,因?yàn)樽罴言匾呀?jīng)在表的尾部。刪除最佳元素也是O(1)。調(diào)整操作中,查找結(jié)點(diǎn)花費(fèi)O(F),改變值/位置花費(fèi)O(1)。
3.3.4?排序跳表
在未排序鏈表中查找元素是很慢的。如果用跳表(http://en.wikipedia.org/wiki/Skip_list)代替鏈表的話,可以加速這個(gè)操作。在跳表中,如果有排序鍵(sort key)的話,集合關(guān)系檢查操作會(huì)很快:O(log F)。如果你知道在何處插入的話,和鏈表一樣,插入操作也是O(1)。如果排序鍵是f,查找最佳元素很快,達(dá)到O(1),刪除一個(gè)元素也是O(1)。調(diào)整操作涉及到查找結(jié)點(diǎn),刪除結(jié)點(diǎn)和重插入。
如果我們用地圖位置作為跳表的排序鍵,集合關(guān)系檢查操作將是O(log F)。在完成集合關(guān)系檢查后,插入操作是O(1)。查找最佳元素是O(F),刪除一個(gè)結(jié)點(diǎn)是O(1)。因?yàn)榧详P(guān)系檢查更快,所以它比未排序鏈表要好一些。
如果我們用f值作為跳表的排序鍵,集合關(guān)系檢查操作將是O(F)。插入操作是O(1)。查找最佳元素是O(1),刪除一個(gè)結(jié)點(diǎn)是O(1)。這并不比排序鏈表好。
3.3.5?索引數(shù)組
如果結(jié)點(diǎn)的集合有限并且數(shù)目是適當(dāng)?shù)?#xff0c;我們可以使用直接索引結(jié)構(gòu),索引函數(shù)i(n)把結(jié)點(diǎn)n映射到一個(gè)數(shù)組的索引。未排序與排序數(shù)組的長(zhǎng)度等于OPEN集的最大值,和它們不同,對(duì)所有的n,索引數(shù)組的長(zhǎng)度總是等于max(i(n))。如果你的函數(shù)是密集的(沒(méi)有不被使用的索引),max(i(n))將是你地圖中結(jié)點(diǎn)的數(shù)目。只要你的地圖是網(wǎng)格的,讓索引函數(shù)密集就是容易的。
假設(shè)i(n)是O(1)的,集合關(guān)系檢查將花費(fèi)O(1),因?yàn)槲覀儙缀醪恍枰獧z查Array[i(n)]是否包含任何數(shù)據(jù)。Insertion is O(1), as we just ste?Array[i(n)].查找和刪除最佳操作是O(numnodes),因?yàn)槲覀儽仨毸阉髡麄€(gè)結(jié)構(gòu)。調(diào)整操作是O(1)。
3.3.6?哈希表
索引數(shù)組使用了很多內(nèi)存用于保存不在OPEN集中的所有結(jié)點(diǎn)。一個(gè)選擇是使用哈希表。哈希表使用了一個(gè)哈希函數(shù)h(n)把地圖上每個(gè)結(jié)點(diǎn)映射到一個(gè)哈希碼。讓哈希表的大小等于N的兩倍,以使發(fā)生沖突的可能性降低。假設(shè)h(n)?是O(1)的,集體關(guān)系檢查操作花費(fèi)O(1);插入操作花費(fèi)O(1);刪除最佳元素操作花費(fèi)O(numnodes),因?yàn)槲覀冃枰阉髡麄€(gè)結(jié)構(gòu)。調(diào)整操作花費(fèi)O(1)。
3.3.7?二元堆
一個(gè)二元堆(不要和內(nèi)存堆混淆)是一種保存在數(shù)組中的樹結(jié)構(gòu)。和許多普通的樹通過(guò)指針指向子結(jié)點(diǎn)所不同,二元堆使用索引來(lái)查找子結(jié)點(diǎn)。C++ STL包含了一個(gè)二元堆的高效實(shí)現(xiàn),我在我自己的A*代碼中使用了它。
在二元堆中,集體關(guān)系檢查花費(fèi)O(F),因?yàn)槟惚仨殥呙枵麄€(gè)結(jié)構(gòu)。插入操作花費(fèi)O(log F)而刪除最佳操作花費(fèi)也是O(log F)。調(diào)整操作很微妙(tricky),花費(fèi)O(F)時(shí)間找到節(jié)點(diǎn),并且很神奇,只用O(log F)來(lái)調(diào)整。
我的一個(gè)朋友(他研究用于最短路徑算法的數(shù)據(jù)結(jié)構(gòu))說(shuō),除非在你的fringe集里有多于10000個(gè)元素,否則二元堆是很不錯(cuò)的。除非你的游戲地圖特別大,否則你不需要更復(fù)雜的數(shù)據(jù)結(jié)構(gòu)(如multi-level buckets(http://www-cs-students.stanford.edu/~csilvers/))。你應(yīng)該盡可能不用Fibonacci?堆(http://www.star-lab.com/goldberg/pub/neci-tr-96-062.ps),因?yàn)殡m然它的漸近復(fù)雜度很好,但是執(zhí)行起來(lái)很慢,除非F足夠大。
3.3.8?伸展樹
堆是一種基于樹的結(jié)構(gòu),它有一個(gè)期望的O(log F)代價(jià)的時(shí)間操作。然而,問(wèn)題是在A*算法中,通常的情況是,一個(gè)代價(jià)小的節(jié)點(diǎn)被移除(花費(fèi)O(log F)的代價(jià),因?yàn)槠渌Y(jié)點(diǎn)必須從樹的底部向上移動(dòng)),而緊接著一些代價(jià)小的節(jié)點(diǎn)被添加(花費(fèi)O(log F)的代價(jià),因?yàn)檫@些結(jié)點(diǎn)被添加到底部并且被移動(dòng)到最頂部)。在這里,堆的操作在預(yù)期的情況下和最壞情況下是一樣的。如果我們找到這樣一種數(shù)據(jù)結(jié)構(gòu),最壞情況還是一樣,而預(yù)期的情況好一些,那么就可以得到改進(jìn)。
伸展樹(Splay tree)是一種自調(diào)整的樹結(jié)構(gòu)。任何對(duì)樹結(jié)點(diǎn)的訪問(wèn)都嘗試把該結(jié)點(diǎn)推到樹的頂部(top)。這就產(chǎn)生了一個(gè)緩存效果("caching" effect):很少被使用的結(jié)點(diǎn)跑到底部(bottom)去了并且不減慢操作(don't slow down operations)。你的splay樹有多大并不重要,因?yàn)槟愕牟僮鲀H僅和你的“cache size”一樣慢。在A*中,低代價(jià)的結(jié)點(diǎn)使用得很多,而高代價(jià)結(jié)點(diǎn)經(jīng)常不被使用,所以高代價(jià)結(jié)點(diǎn)將會(huì)移動(dòng)到樹的底部。
使用伸展樹后,集體關(guān)系檢查,插入,刪除最佳和調(diào)整操作都是期望的O(log F)(注:原文為expected O(log F)?),最壞情況是O(F)。然而有代表性的是,緩存過(guò)程(caching)避免了最壞情況的發(fā)生。Dijkstra算法和帶有低估的啟發(fā)函數(shù)(underestimating heuristic)的A*算法卻有一些特性讓伸展樹達(dá)不到最優(yōu)。特別是對(duì)結(jié)點(diǎn)n和鄰居結(jié)點(diǎn)n’來(lái)說(shuō),f(n') >= f(n)。當(dāng)這發(fā)生時(shí),也許插入操作總是發(fā)生在樹的同一邊結(jié)果是使它失去了平衡。我沒(méi)有試驗(yàn)過(guò)這個(gè)。
3.3.9 HOT隊(duì)列
還有一種比堆好的數(shù)據(jù)結(jié)構(gòu)。通常你可以限制優(yōu)先隊(duì)列中值的范圍。給定一個(gè)限定的范圍,經(jīng)常會(huì)存在更好的算法。例如,對(duì)任意值的排序可以在O(N log N)時(shí)間內(nèi)完成,但當(dāng)固定范圍時(shí),桶排序和基數(shù)排序可以在O(N)時(shí)間內(nèi)完成。
我們可以使用HOT(Heap On Top)隊(duì)列(http://www.star-lab.com/goldberg/pub /neci-tr-97-104.ps)來(lái)利用f(n') >= f(n),其中n’是n的一個(gè)鄰居結(jié)點(diǎn)。我們刪除f(n)值最小的結(jié)點(diǎn)n,插入滿足f(n) <= f(n') <= f(n) + delta的鄰居n',其中delta <= C。常數(shù)C是從一結(jié)點(diǎn)到鄰近結(jié)點(diǎn)代價(jià)改變量的最大值。因?yàn)閒(n)是OPEN集中的最小f值,并且正要被插入的所有結(jié)點(diǎn)都小于或等于f(n) + delta,我們知道OPEN集中的所有f值都不超過(guò)一個(gè)0..delta的范圍。在桶/基數(shù)排序中,我們可以用“桶”(buckets)對(duì)OPEN集中的結(jié)點(diǎn)進(jìn)行排序。
使用K個(gè)桶,我們把O(N)的代價(jià)降低到平均O(N/K)。通過(guò)HOT隊(duì)列,頂端的桶使用二元堆而所有其他的桶都是未排序數(shù)組。因而,對(duì)頂部的桶,集合關(guān)系檢查代價(jià)是預(yù)期的O(F/K),插入和刪除最佳是O(log (F/K))。對(duì)其他桶,集合關(guān)系檢查是O(F/K),插入是O(1),而刪除最佳根本不發(fā)生!如果頂端的桶是空的,那么我們必須把下一個(gè)桶即未排序數(shù)組轉(zhuǎn)換為二元堆。這個(gè)操作(“heapify”)可以在O(F/K)時(shí)間內(nèi)完成。在調(diào)整操作中,刪除是O(F/K),然后插入是O(log (F/K))或O(1)。
在A*中,我們加入OPEN集中的許多結(jié)點(diǎn)實(shí)際上根本是不需要的。在這方面HOT隊(duì)列很有優(yōu)勢(shì),因?yàn)椴恍枰脑氐牟迦氩僮髦换ㄙM(fèi)O(1)時(shí)間。只有需要的元素被heapified(代價(jià)較低的那些)。唯一一個(gè)超過(guò)O(1)的操作是從堆中刪除結(jié)點(diǎn),只花費(fèi)O(log (F/K))。
另外,如果C比較小,我們可以只讓K = C,則對(duì)于最小的桶,我們甚至不需要一個(gè)堆,國(guó)為在一個(gè)桶中的所有結(jié)點(diǎn)都有相同的f值。插入和刪除最佳都是O(1)時(shí)間!有人研究過(guò),HOT隊(duì)列在至多在OPEN集中有800個(gè)結(jié)點(diǎn)時(shí)和堆一樣快,并且如果OPEN集中至多有1500個(gè)結(jié)點(diǎn),則比堆快20%。我期望隨著結(jié)點(diǎn)的增加,HOT隊(duì)列也更快。
HOT隊(duì)列的一個(gè)簡(jiǎn)單的變化是一個(gè)二層隊(duì)列(two-level queue):把好的結(jié)點(diǎn)放進(jìn)一個(gè)數(shù)據(jù)結(jié)構(gòu)(堆或數(shù)組)而把壞的結(jié)點(diǎn)放進(jìn)另一個(gè)數(shù)據(jù)結(jié)構(gòu)(數(shù)組或鏈表)。因?yàn)榇蠖鄶?shù)進(jìn)入OPEN集中的結(jié)點(diǎn)都“壞的”,它們從不被檢查,因而把它們放進(jìn)出一個(gè)大數(shù)組是沒(méi)有害處的。
3.3.10?比較
注意有一點(diǎn)很重要,我們并不是僅僅關(guān)心漸近的行為(大O符號(hào))。我們也需要關(guān)心小常數(shù)(low constant)下的行為。為了說(shuō)明原因,考慮一個(gè)O(log F)的算法,和另一個(gè)O(F)的算法,其中F是堆中元素的個(gè)數(shù)。也許在你的機(jī)器上,第一個(gè)算法的實(shí)現(xiàn)花費(fèi)10000*log(F)秒,而另一個(gè)的實(shí)現(xiàn)花費(fèi)2*F秒。當(dāng)F=256時(shí),第一個(gè)算法將花費(fèi)80000秒而第二個(gè)算法花費(fèi)512秒。在這種情況下,“更快”的算法花費(fèi)更多的時(shí)間,而且只有在當(dāng)F>200000時(shí)才能運(yùn)行得更快。
你不能僅僅比較兩個(gè)算法。你還要比較算法的實(shí)現(xiàn)。同時(shí)你還需要知道你的數(shù)據(jù)的大小(size)。在上面的例子中,第一種實(shí)現(xiàn)在F>200000時(shí)更快,但如果在你的游戲中,F小于30000,那么第二種實(shí)現(xiàn)好一些。
基本數(shù)據(jù)結(jié)構(gòu)沒(méi)有一種是完全合適的。未排序數(shù)組或者鏈表使插入操作很快而集體關(guān)系檢查和刪除操作非常慢。排序數(shù)組或者鏈表使集體關(guān)系檢查稍微快一些,刪除(最佳元素)操作非常快而插入操作非常慢。二元堆讓插入和刪除操作稍微快一些,而集體關(guān)系檢查則很慢。伸展樹讓所有操作都快一些。HOT隊(duì)列讓插入操作很快,刪除操作相當(dāng)快,而集體關(guān)系檢查操作稍微快一些。索引數(shù)組讓集體關(guān)系檢查和插入操作非常快,但是刪除操作不可置信地慢,同時(shí)還需要花費(fèi)很多內(nèi)存空間。哈希表和索引數(shù)組類似,但在普通情況下,它花費(fèi)的內(nèi)存空間少得多,而刪除操作雖然還是很慢,但比索引數(shù)組要快。
關(guān)于更高級(jí)的優(yōu)先隊(duì)列的資料和實(shí)現(xiàn),請(qǐng)參考Lee Killough的優(yōu)先隊(duì)列頁(yè)面(http://members.xoom.com/killough/heaps.html)。
3.3.11?混合實(shí)現(xiàn)
為了得到最佳性能,你將希望使用混合數(shù)據(jù)結(jié)構(gòu)。在我的A*代碼中,我使用一個(gè)索引數(shù)組從而集合關(guān)系檢查是O(1)的,一個(gè)二元堆從而插入操作和刪除最佳都是O(log F)的。對(duì)于調(diào)整操作,我使用索引數(shù)組從而花費(fèi)O(1)時(shí)間檢查我是否真的需要進(jìn)行調(diào)整(通過(guò)在索引數(shù)組中保存g值),然后在少數(shù)確實(shí)需要進(jìn)行調(diào)整的情況中,我使用二元堆從而調(diào)整操作花費(fèi)O(F)時(shí)間。你也可以使用索引數(shù)組保存堆中每個(gè)結(jié)點(diǎn)的位置,這讓你的調(diào)整操作變成O(log F)。
3.4?與游戲循環(huán)的交互
交互式的(尤其是實(shí)時(shí)的)游戲?qū)ψ罴崖窂降挠?jì)算要求很高。能夠得到一個(gè)解決方案比得到最佳方案可能更重要。然而在所有其他因素都相同的情況下,短路徑比長(zhǎng)路徑好。
一般來(lái)說(shuō),計(jì)算靠近初始結(jié)點(diǎn)的路徑比靠近目標(biāo)結(jié)點(diǎn)的路徑更重要一些。立即開始原理(The principle of?immediate start):讓游戲中的物體盡可能快地開始行動(dòng),哪怕是沿著一條不理想的路徑,然后再計(jì)算一條更好的路徑。在實(shí)時(shí)游戲中,應(yīng)該更多地關(guān)注A*的延遲情況(latency)而不是吞吐量(throughput)。
可以對(duì)物體編程讓它們根據(jù)自己的本能(簡(jiǎn)單行為)或者智力(一條預(yù)先計(jì)算好的路徑)來(lái)行動(dòng)。除非它們的智力告訴它們?cè)趺葱袆?dòng),否則它們就根據(jù)自己的本能來(lái)行動(dòng)(這是實(shí)際上使用的方法,并且Rodney Brook在他的機(jī)器人體系結(jié)構(gòu)中也用到)。和立即計(jì)算所有路徑所不同,讓游戲在每一個(gè),兩個(gè),或者三個(gè)循環(huán)中搜索一條路徑。讓物體在開始時(shí)依照本能行動(dòng)(可能僅僅是簡(jiǎn)單地朝著目標(biāo)直線前進(jìn)),然后才為它們尋找路徑。這種方法讓讓路徑搜索的代價(jià)趨于平緩,因此它不會(huì)集中發(fā)生在同一時(shí)刻。
3.4.1?提前退出
可以從A*算法的主循環(huán)中提前退出來(lái)同時(shí)得到一條局部路徑。通常,當(dāng)找到目標(biāo)結(jié)點(diǎn)時(shí),主循環(huán)就退出了。然而,在此之前的任意結(jié)點(diǎn),可以得到一條到達(dá)OPEN中當(dāng)前最佳結(jié)點(diǎn)的路徑。這個(gè)結(jié)點(diǎn)是到達(dá)目標(biāo)點(diǎn)的最佳選擇,所以它是一個(gè)理想的中間結(jié)點(diǎn)(原文為so it's a reasonable place to go)。
可以提前退出的情況包括檢查了一定數(shù)量的結(jié)點(diǎn),A*算法已經(jīng)運(yùn)行了幾毫秒時(shí)間,或者掃描了一個(gè)離初始點(diǎn)有些距離的結(jié)點(diǎn)。當(dāng)使用路徑拼接時(shí),應(yīng)該給被拼接的路徑一個(gè)比全路徑(full path)小的最大長(zhǎng)度。
3.4.2?中斷算法
如果需要進(jìn)行路徑搜索的物體較少,或者如果用于保存OPEN和CLOSED集的數(shù)據(jù)結(jié)構(gòu)較小,那么保存算法的狀態(tài)是可行的,然后退出到游戲循環(huán)繼續(xù)運(yùn)行游戲。
3.4.3?組運(yùn)動(dòng)
路徑請(qǐng)求并不是均勻分布的。即時(shí)策略游戲中有一個(gè)常見的情況,玩家會(huì)選擇多個(gè)物體并命令它們朝著同樣的目標(biāo)移動(dòng)。這給路徑搜索系統(tǒng)以沉重的負(fù)載。
在這種情況下,為某個(gè)物體尋找到的路徑對(duì)其它物體也是同樣有用的。一種方法是,尋找一條從物體的中心到目的地中心的路徑P。對(duì)所有物體使用該路徑的絕大部分,對(duì)每一個(gè)物體,前十步和后十步使用為它自己尋找的路徑。物體i得到一條從它的開始點(diǎn)到P[10]的路徑,緊接著是共享的路徑P[10..len(P)-10],最后是從P[len(P)-10]到目的地的路徑。
為每個(gè)物體尋找的路徑是較短的(平均步數(shù)大約是10),而較長(zhǎng)的路徑被共享。大多數(shù)路徑只尋找一次并且為所有物體所共享。然而,當(dāng)玩家們看到所有的物體都沿著相同的路徑移動(dòng)時(shí),將對(duì)游戲失去興趣。為了對(duì)系統(tǒng)做些改進(jìn),可以讓物體稍微沿著不同的路徑運(yùn)動(dòng)。一種方法是選擇鄰近結(jié)點(diǎn)以改變路徑。
另一種方法是讓每個(gè)物體都意識(shí)到其它物體的存在(或許是通過(guò)隨機(jī)選擇一個(gè)“領(lǐng)導(dǎo)”物體,或者是通過(guò)選擇一個(gè)能夠最好地意識(shí)到當(dāng)前情況的物體),同時(shí)僅僅為領(lǐng)導(dǎo)尋路。然后用flocking算法讓它們以組的形式運(yùn)動(dòng)。
然而還有一種方法是利用A*算法的中間狀態(tài)。這個(gè)狀態(tài)可以被朝著相同目標(biāo)移動(dòng)的多個(gè)物體共享,只要物體共享相同的啟發(fā)式函數(shù)和代價(jià)函數(shù)。當(dāng)主循環(huán)退出時(shí),不要消除OPEN和CLOSED集;用A*上一次的OPEN和CLOSED集開始下一次的循環(huán)(下一個(gè)物體的開始位置)。(這可以被看成是中斷算法和提前退出部分的一般化)
3.4.4?細(xì)化
如果地圖中沒(méi)有障礙物,而有不同代價(jià)的地形,那么可以通過(guò)低估地形的代價(jià)來(lái)計(jì)算一條初始路徑。例如,如果草地的代價(jià)是1,山地代價(jià)是2,山脈的代價(jià)是3,那么A*會(huì)考慮通過(guò)3個(gè)草地以避免1個(gè)山脈。通過(guò)把草地看成1,山地看成1.1,而山脈看成1.2來(lái)計(jì)算初始路徑,A*將會(huì)用更少的時(shí)間去設(shè)法避免山脈,而且可以更快地找到一條路徑(這接近于精確啟發(fā)函數(shù)的效果)。一旦找到一條路徑,物體就可以開始移動(dòng),游戲循環(huán)就可以繼續(xù)了。當(dāng)多余的CPU時(shí)間是可用的時(shí)候,可以用真實(shí)的移動(dòng)代價(jià)去計(jì)算更好的路徑。
4 A*算法的變種
4.1 beam search?beam search
在A*的主循環(huán)中,OPEN集保存所有需要檢查的結(jié)點(diǎn)。Beam Search是A*算法的一個(gè)變種,這種算法限定了OPEN集的尺寸。如果OPEN集變得過(guò)大,那些沒(méi)有機(jī)會(huì)通向一條好的路徑的結(jié)點(diǎn)將被拋棄。缺點(diǎn)是你必須讓排序你的集合以實(shí)現(xiàn)這個(gè),這限制了可供選擇的數(shù)據(jù)結(jié)構(gòu)。
4.2?迭代深化
迭代深化是一種在許多AI算法中使用的方法,這種方法從一個(gè)近似解開始,逐漸得到更精確的解。該名稱來(lái)源于游戲樹搜索,需要查看前面幾步(比如在象棋里),通過(guò)查看前面更多步來(lái)提高樹的深度。一旦你的解不再有更多的改變或者改善,就可以認(rèn)為你已經(jīng)得到足夠好的解,當(dāng)你想要進(jìn)一步精確化時(shí),它不會(huì)再有改善。在ID-A*中,深度是f值的一個(gè)cutoff。當(dāng)f的值太大時(shí),結(jié)點(diǎn)甚至將不被考慮(例如,它不會(huì)被加入OPEN集中)。第一次迭代只處理很少的結(jié)點(diǎn)。此后每一次迭代,訪問(wèn)的結(jié)點(diǎn)都將增加。如果你發(fā)現(xiàn)路徑有所改善,那么就繼續(xù)增加cutoff,否則就可以停止了。更多的細(xì)節(jié)請(qǐng)參考這些關(guān)于ID-A*的資料:http://www.apl.jhu.edu/~hall/AI-Programming/IDA-Star.html。
我本人認(rèn)為在游戲地圖中沒(méi)有太大的必要使用ID-A*尋路。ID算法趨向于增加計(jì)算時(shí)間而減少內(nèi)存需求。然而在地圖路徑搜索中,“結(jié)點(diǎn)”是很小的——它們僅僅是坐標(biāo)而已。我認(rèn)為不保存這些結(jié)點(diǎn)以節(jié)省空間并不會(huì)帶來(lái)多大改進(jìn)。
4.3?動(dòng)態(tài)衡量
在動(dòng)態(tài)衡量中,你假設(shè)在開始搜索時(shí),最重要的是訊速移動(dòng)到任意位置;而在搜索接近結(jié)束時(shí),最重要的是移動(dòng)到目標(biāo)點(diǎn)。
f(p) = g(p) + w(p) * h(p)
啟發(fā)函數(shù)中帶有一個(gè)權(quán)值(weight)(w>=1)。當(dāng)你接近目標(biāo)時(shí),你降低這個(gè)權(quán)值;這降低了啟發(fā)函數(shù)的重要性,同時(shí)增加了路徑真實(shí)代價(jià)的相對(duì)重要性。
4.4?帶寬搜索
帶寬搜索(Bandwidth Search)有兩個(gè)對(duì)有些人也許有用的特性。這個(gè)變種假設(shè)h是過(guò)高估計(jì)的值,但不高于某個(gè)數(shù)e。如果這就是你遇到的情況,那么你得到的路徑的代價(jià)將不會(huì)比最佳路徑的代價(jià)超過(guò)e。重申一次,你的啟發(fā)函數(shù)設(shè)計(jì)的越好,最終效果就越好。
另一個(gè)特性是,你可以丟棄OPEN集中的某些結(jié)點(diǎn)。當(dāng)h+d比路徑的真實(shí)代價(jià)高的時(shí)候(對(duì)于某些d),你可以丟棄那些f值比OPEN集中的最好結(jié)點(diǎn)的f值高至少e+d的結(jié)點(diǎn)。這是一個(gè)奇怪的特性。對(duì)于好的f值你有一個(gè)“范圍”("band"),任何在這個(gè)范圍之外的結(jié)點(diǎn)都可以被丟棄掉,因?yàn)檫@個(gè)結(jié)點(diǎn)肯定不會(huì)在最佳路徑上。
好奇地(Curiously),你可以對(duì)這兩種特性使用不同的啟發(fā)函數(shù),而問(wèn)題仍然可以得到解決。使用一個(gè)啟發(fā)函數(shù)以保證你得到的路徑不會(huì)太差,另一個(gè)用于檢查從OPEN集中去掉哪些結(jié)點(diǎn)。
4.5?雙向搜索
與從開始點(diǎn)向目標(biāo)點(diǎn)搜索不同的是,你也可以并行地進(jìn)行兩個(gè)搜索——一個(gè)從開始點(diǎn)向目標(biāo)點(diǎn),另一個(gè)從目標(biāo)點(diǎn)向開始點(diǎn)。當(dāng)它們相遇時(shí),你將得到一條好的路徑。
這聽起來(lái)是個(gè)好主意,但我不會(huì)給你講很多內(nèi)容。雙向搜索的思想是,搜索過(guò)程生成了一棵在地圖上散開的樹。一棵大樹比兩棵小樹差得多,所以最好是使用兩棵較小的搜索樹。然而我的試驗(yàn)表明,在A*中你得不到一棵樹,而只是在搜索地圖中當(dāng)前位置附近的區(qū)域,但是又不像Dijkstra算法那樣散開。事實(shí)上,這就是讓A*算法運(yùn)行得如此快的原因——無(wú)論你的路徑有多長(zhǎng),它并不進(jìn)行瘋狂的搜索,除非路徑是瘋狂的。它只嘗試搜索地圖上小范圍的區(qū)域。如果你的地圖很復(fù)雜,雙向搜索會(huì)更有用。
面對(duì)面的方法(The?front-to-front?variation)把這兩種搜索結(jié)合在一起。這種算法選擇一對(duì)具有最好的g(start,x) + h(x,y) + g(y,goal)的結(jié)點(diǎn),而不是選擇最好的前向搜索結(jié)點(diǎn)——g(start,x) + h(x,goal),或者最好的后向搜索結(jié)點(diǎn)——g(y,goal) + h(start,y)。
Retargeting方法不允許前向和后向搜索同時(shí)發(fā)生。它朝著某個(gè)最佳的中間結(jié)點(diǎn)運(yùn)行前向搜索一段時(shí)間,然后再朝這個(gè)結(jié)點(diǎn)運(yùn)行后向搜索。然后選擇一個(gè)后向最佳中間結(jié)點(diǎn),從前向最佳中間結(jié)點(diǎn)向后向最佳中間結(jié)點(diǎn)搜索。一直進(jìn)行這個(gè)過(guò)程,直到兩個(gè)中間結(jié)點(diǎn)碰到一塊。
4.6?動(dòng)態(tài)A*與終身計(jì)劃A*
有一些A*的變種允許當(dāng)初始路徑計(jì)算出來(lái)之后,世界發(fā)生改變。D*用于當(dāng)你沒(méi)有全局所有信息的時(shí)候。如果你沒(méi)有所有的信息,A*可能會(huì)出錯(cuò);D*的貢獻(xiàn)在于,它能糾正那些錯(cuò)誤而不用過(guò)多的時(shí)間。LPA*用于代價(jià)會(huì)改變的情況。在A*中,當(dāng)?shù)貓D發(fā)生改變時(shí),路徑將變得無(wú)效;LPA*可以重新使用之前A*的計(jì)算結(jié)果并產(chǎn)生新的路徑。然而,D*和LPA*都需要很多內(nèi)存——用于運(yùn)行A*并保存它的內(nèi)部信息(OPEN和CLOSED集,路徑樹,g值),當(dāng)?shù)貓D發(fā)生改變時(shí),D*或者LPA*會(huì)告訴你,是否需要就地圖的改變對(duì)路徑作調(diào)整。在一個(gè)有許多運(yùn)動(dòng)著的物體的游戲中,你經(jīng)常不希望保存所有這些信息,所以D*和LPA*在這里并不適用。它們是為機(jī)器人技術(shù)而設(shè)計(jì)的,這種情況下只有一個(gè)機(jī)器人——你不需要為別的機(jī)器人尋路而重用內(nèi)存。如果你的游戲只有一個(gè)或者少數(shù)幾個(gè)物體,你可以研究一下D*或者LPA*。
- Overview of D*(http://www.frc.ri.cmu.edu/~axs/dynamic_plan.html)
- D* Paper 1(http:// http://www.frc.ri.cmu.edu/~axs/doc/icra94.ps)
- D* Paper 2(http:// http://www.frc.ri.cmu.edu/~axs/doc/ijcai95.ps)
- Lifelong planning overview(http://idm-lab.org/project-a.html)
- Lifelong planning paper (PDF)(http://csci.mrs.umn.edu/UMMCsciwiki/pub/?Csci3903s03/KellysPaper/seminar.pdf)
- Lifelong planning A* applet(http://idm-lab.org/applet.html)
5?處理運(yùn)動(dòng)障礙物
一個(gè)路徑搜索算法沿著固定障礙物計(jì)算路徑,但是當(dāng)障礙物會(huì)運(yùn)動(dòng)時(shí)情況又怎樣?當(dāng)一個(gè)物體到達(dá)一個(gè)特寫的位置,原來(lái)的障礙物也許不再在那兒了,或者一個(gè)新的障礙物也許到達(dá)那兒。處理該問(wèn)題的一個(gè)方法是放棄路徑搜索而使用運(yùn)動(dòng)算法(movement algorithms)替代,這就不能look far ahead;這種方法會(huì)在后面的部分中討論。這一部分將對(duì)路徑搜索方法進(jìn)行修改從而解決運(yùn)動(dòng)障礙物的問(wèn)題。
5.1?重新計(jì)算路徑
當(dāng)時(shí)間漸漸過(guò)去,我們希望游戲世界有所改變。以前搜索到的一條路徑到現(xiàn)在也許不再是最佳的了。對(duì)舊的路徑用新的信息進(jìn)行更新是有價(jià)值的。以下規(guī)則可以用于決定什么時(shí)候需要重新計(jì)算路徑:
- 每N步:這保證用于計(jì)算路徑的信息不會(huì)舊于N步。
- 任何可以使用額外的CPU時(shí)間的時(shí)候:這允許動(dòng)態(tài)調(diào)整路徑的性質(zhì);在物體數(shù)量多時(shí),或者運(yùn)行游戲的機(jī)器比較慢時(shí),每個(gè)物體對(duì)CPU的使用可得到減少。
- 當(dāng)物體拐彎或者跨越一個(gè)導(dǎo)航點(diǎn)(waypoint)的時(shí)候。
- 當(dāng)物體附近的世界改變了的時(shí)候。
重計(jì)算路徑的主要缺點(diǎn)是許多路徑信息被丟棄了。例如,如果路徑是100步長(zhǎng),每10步重新計(jì)算一次,路徑的總步數(shù)將是100+90+80+70+60+50+40+30+20+10 = 550。對(duì)M步長(zhǎng)的路徑,大約需要計(jì)算M^2步。因此如果你希望有許多很長(zhǎng)的路徑,重計(jì)算不是個(gè)好主意。重新使用路徑信息比丟棄它更好。
5.2?路徑拼接
當(dāng)一條路徑需要被重新計(jì)算時(shí),意味著世界正在改變。對(duì)于一個(gè)正在改變的世界,對(duì)地圖中當(dāng)前鄰近的區(qū)域總是比對(duì)遠(yuǎn)處的區(qū)域了解得更多。因此,我們應(yīng)該集中于在附近尋找好的路徑,同時(shí)假設(shè)遠(yuǎn)處的路徑不需要重新計(jì)算,除非我們接近它。與重新計(jì)算整個(gè)路徑不同,我們可以重新計(jì)算路徑的前M步:
?
?
因?yàn)閜[1]和p[M]比分開的M步小(原文:Since p[1] and p[M] are fewer than M steps apart),看起來(lái)新路徑不會(huì)很長(zhǎng)。不幸的是,新的路徑也許很長(zhǎng)而且不夠好。上面的圖顯示了這種情況。最初的紅色路徑是1-2-3-4,褐色的是障礙物。如果我們到達(dá)2并且發(fā)現(xiàn)從2到達(dá)3的路徑被封鎖了,路徑拼接技術(shù)會(huì)把2-3用2-5-3取代,結(jié)果是物體沿著路徑1-2-5-3-4運(yùn)動(dòng)。我們可以看到這不是一條好的路徑,藍(lán)色的路徑1-2-5-4是一條更好的路徑。
通常可以通過(guò)查看新路徑的長(zhǎng)度檢測(cè)到壞的路徑。如果這嚴(yán)格大于M,就可能是不好的。一個(gè)簡(jiǎn)單的解決方法是,為搜索算法設(shè)置一個(gè)最大路徑長(zhǎng)度。如果找不到一條短的路徑,算法返回錯(cuò)誤代碼;這種情況下,用重計(jì)算路徑取代路徑拼接,從而得到路徑1-2-5-4.。
對(duì)于其它情況,對(duì)于N步的路徑,路徑拼接會(huì)計(jì)算2N或者3N步,這取決于拼接新路徑的頻率。對(duì)于對(duì)世界的改變作反應(yīng)的能力而言,這個(gè)代價(jià)是相當(dāng)?shù)偷摹A钊顺泽@的是這個(gè)代價(jià)和拼接的步數(shù)M無(wú)關(guān)。M不影響CPU時(shí)間,而控制了響應(yīng)和路徑質(zhì)量的折衷。如果M太大,物體的移動(dòng)將不能快速對(duì)地圖的改變作出反應(yīng)。如果M太小,拼接的路徑可能太短以致不能正確地繞過(guò)障礙物;許多不理想的路徑(如1-2-5-3-4)將被找到。嘗試不同的M值和不同的拼接標(biāo)準(zhǔn)(如每3/4?M步),看看哪一種情況對(duì)你的地圖最合適。
路徑拼接確實(shí)比重計(jì)算路徑要快,但它不能對(duì)路徑的改變作出很好的反應(yīng)。經(jīng)常可以發(fā)現(xiàn)這種情況并用路徑重計(jì)算來(lái)取代。也可以調(diào)整一些變量,如M和尋找新路徑的時(shí)機(jī),所以可以對(duì)該方法進(jìn)行調(diào)整(甚至在運(yùn)行時(shí))以用于不同的情況。
Note:Bryan Stout?有兩個(gè)算法,Patch-One和Patch-All,他從路徑拼接中得到靈感,并在實(shí)踐中運(yùn)行得很好。他出席了GDC 2007(https://www.cmpevents.com/GD07/a.asp?option =C &V=11& SessID=4608);一旦他把資料放在網(wǎng)上,我將鏈接過(guò)去。
Implementation Note:
反向保存路徑,從而刪除路徑的開始部分并用不同長(zhǎng)度的新路徑拼接將更容易,因?yàn)檫@兩個(gè)操作都將在數(shù)組的末尾進(jìn)行。本質(zhì)上你可以把這個(gè)數(shù)組看成是堆棧因?yàn)轫敳康脑乜偸窍乱粋€(gè)要使用的。
5.3?監(jiān)視地圖變化
與間隔一段時(shí)間重計(jì)算全部或部分路徑不同的是,可以讓地圖的改變觸發(fā)一次重計(jì)算。地圖可以分成區(qū)域,每個(gè)物體都可以對(duì)某些區(qū)域感興趣(可以是包含部分路徑的所有區(qū)域,也可以只是包含部分路徑的鄰近區(qū)域)。當(dāng)一個(gè)障礙物進(jìn)入或者離開一個(gè)區(qū)域,該區(qū)域?qū)⒈粯?biāo)識(shí)為已改變,所有對(duì)該區(qū)域感興趣的物體都被通知到,所以路徑將被重新計(jì)算以適應(yīng)障礙物的改變。
這種技術(shù)有許多變種。例如,可以每隔一定時(shí)間通知物體,而不是立即通知物體。多個(gè)改變可以成組地觸發(fā)一個(gè)通知,因此避免了額外的重計(jì)算。另一個(gè)例子是,讓物體檢查區(qū)域,而不是讓區(qū)域通知物體。
監(jiān)視地圖變化允許當(dāng)障礙物不改變時(shí)物體避免重計(jì)算路徑,所以當(dāng)你有許多區(qū)域并不經(jīng)常改變時(shí),考慮這種方法。
5.4?預(yù)測(cè)障礙物的運(yùn)動(dòng)
如果障礙物的運(yùn)動(dòng)可以預(yù)測(cè),就能為路徑搜索考慮障礙物的未來(lái)位置。一個(gè)諸如A*的算法有一個(gè)代價(jià)函數(shù)用以檢查穿過(guò)地圖上一點(diǎn)的代價(jià)有多難。A*可以被改進(jìn)從而知道到達(dá)一點(diǎn)的時(shí)間需求(通過(guò)當(dāng)前路徑長(zhǎng)度來(lái)檢查),而現(xiàn)在則輪到代價(jià)函數(shù)了。代價(jià)函數(shù)可以考慮時(shí)間,并用預(yù)測(cè)的障礙物位置檢查在某個(gè)時(shí)刻地圖某個(gè)位置是否可以通過(guò)。這個(gè)改進(jìn)不是完美的,然而,因?yàn)樗⒉豢紤]在某個(gè)點(diǎn)等待障礙物自動(dòng)離開的可能性,同時(shí)A*并不區(qū)分到達(dá)相同目的地的不同的路徑,而是針對(duì)不同的目的地,所以還是可以接受的。
6?預(yù)計(jì)算路徑的空間代價(jià)
有時(shí),路徑計(jì)算的限制因素不是時(shí)間,而是用于數(shù)以百計(jì)的物體的存儲(chǔ)空間。路徑搜索器需要空間以運(yùn)行算法和保存路徑。算法運(yùn)行所需的臨時(shí)空間(在A*中是OPEN和CLOSED集)通常比保存結(jié)果路徑的空間大許多。通過(guò)限制在一定的時(shí)間計(jì)算一條路徑,可以把臨時(shí)空間數(shù)量最小化。另外,為OPEN和CLOSED集所選擇的數(shù)據(jù)結(jié)構(gòu)的不同,最小化臨時(shí)空間的程度也有很大的不同。這一部分聚集于優(yōu)化用于計(jì)算路徑的空間代價(jià)。
6.1?位置VS方向
一條路徑可以用位置或者方向來(lái)表示。位置需要更多的空間,但是有一個(gè)優(yōu)點(diǎn),易于查詢路徑中的任意位置或者方向而不用沿著路徑移動(dòng)。當(dāng)保存方向時(shí),只有方向容易被查詢;只有沿著整個(gè)路徑移動(dòng)才能查詢位置。在一個(gè)典形的網(wǎng)格地圖中,位置可以被保存為兩個(gè)16位整數(shù),每走一步是32位。而方向是很少的,因此用極少的空間就夠了。如果物體只能沿著四個(gè)方向移動(dòng),每一步用兩位就夠了;如果物體能沿著6個(gè)或者8個(gè)方向移動(dòng),每一步也只需要三位。這些對(duì)于保存路徑中的位置都有明顯的空間節(jié)省。Hannu Kankaanpaa指出可以進(jìn)一步減少空間需求,那就是保存相對(duì)方向(右旋60度)而不是絕對(duì)方向(朝北走)。有些相對(duì)方向?qū)δ承┪矬w來(lái)說(shuō)意義不大。比如,如果你的物體朝北移動(dòng),那么下一步朝南移動(dòng)的可能性很小。在只有六種方向的游戲中,你只有五個(gè)有意義的方向。在某些地圖中,也許只有三個(gè)方向(直走,左旋60度,右旋60度)有意義,而其它地圖中,右旋120度是有效的(比如,沿著陡峭的山坡走之字形的路徑時(shí))。
6.2?路徑壓縮
一旦找到一條路徑,可以對(duì)它進(jìn)行壓縮。可以用一個(gè)普通的壓縮算法,但這里不進(jìn)行討論。使用特定的壓縮算法可以縮小路徑的存儲(chǔ),無(wú)論它是基于位置的還是基于方向的。在做決定之前,考察你的游戲中的路徑以確定哪種壓縮效果最好。另外還要考慮實(shí)現(xiàn)和調(diào)試,代碼量,and whether it really matters.如果你有300個(gè)物體并且在同一時(shí)刻只有50個(gè)在移動(dòng),同時(shí)路徑比較短(100步),內(nèi)存總需求大概只有不到50k,總之,沒(méi)有必要擔(dān)心壓縮的效果。
6.2.1?位置存儲(chǔ)
在障礙物比地形對(duì)路徑搜索影響更大的地圖中,路徑中有大部分是直線的。如果是這種情況,那么路徑只需要包含直線部分的終止點(diǎn)(有時(shí)叫waypoints)。此時(shí)移動(dòng)過(guò)程將包含檢查下一結(jié)點(diǎn)和沿著直線向前移動(dòng)。
6.2.2?方向存儲(chǔ)
保存方向時(shí),有一種情況是同一個(gè)方向保存了很多次。可以用簡(jiǎn)單的方法節(jié)省空間。
一種方法是保存方向以及朝著該方向移動(dòng)的次數(shù)。和位置存儲(chǔ)的優(yōu)化不同,當(dāng)一個(gè)方向并不是移動(dòng)很多次時(shí),這種優(yōu)化的效果反而不好。同樣的,對(duì)于那些可以進(jìn)行位置壓縮的直線來(lái)說(shuō),方向壓縮是行不通的,因?yàn)檫@條直線可能沒(méi)有和正在移動(dòng)的方向關(guān)聯(lián)。通過(guò)相對(duì)方向,你可以把“繼續(xù)前進(jìn)”當(dāng)作可能的方向排除掉。Hannu Kankaanpaa指出,在一個(gè)八方向地圖中,你可以去掉前,后,以及向左和向右135度(假設(shè)你的地圖允許這個(gè)),然后你可以僅用兩個(gè)比特保存每個(gè)方向。
另一種保存路徑的方法是變長(zhǎng)編碼。這種想法是使用一個(gè)簡(jiǎn)單的比特(0)保存最一般的步驟:向前走。使用一個(gè)“1”表示拐彎,后邊再跟幾個(gè)比特表示拐彎的方向。在一個(gè)四方向地圖中,你只能左轉(zhuǎn)和右轉(zhuǎn),因此可以用“10”表示左轉(zhuǎn),“11”表示右轉(zhuǎn)。
變長(zhǎng)編碼比run length encoding更一般,并且可以壓縮得更好,但對(duì)于較長(zhǎng)的直線路徑則不然。序列(向北直走6步,左轉(zhuǎn),直走3步,右轉(zhuǎn),直走5步,左轉(zhuǎn),直走2步)用run length encoding表示是[(NORTH, 6), (WEST, 3), (NORTH, 5), (WEST, 2)]。如果每個(gè)方向用2比特,每個(gè)距離用8比特,保存這條路徑需要40比特。而對(duì)于變長(zhǎng)編碼,你用1比特表示每一步,2比特表示拐彎——[NORTH 0 0 0 0 0 0 10 0 0 0 11 0 0 0 0 0 10 0 0]——一共24比特。如果初始方向和每次拐彎對(duì)應(yīng)1步,則每次拐彎都節(jié)省了一個(gè)比特,結(jié)果只需要20比特保存這條路徑。然而,用變長(zhǎng)編碼保存更長(zhǎng)的路徑時(shí)需要更多的空間。序列(向北直走200步)用run length encoding表示是[(NORTH, 200)],總共需要10比特。用變長(zhǎng)編碼表示同樣的序列則是[NORTH 0 0 ...],一共需要202比特。
6.3?計(jì)算導(dǎo)航點(diǎn)
一個(gè)導(dǎo)航點(diǎn)(waypoint)是路徑上的一個(gè)結(jié)點(diǎn)。與保存路徑上的每一步不同,在進(jìn)行路徑搜索之后,一個(gè)后處理(post-processing)的步驟可能會(huì)把若干步collapse(譯者:不好翻譯,保留原單詞)為一個(gè)簡(jiǎn)單的導(dǎo)航點(diǎn),這經(jīng)常發(fā)生在路徑上那些方向發(fā)生改變的地方,或者在一個(gè)重要的(major)位置如城市。然后運(yùn)動(dòng)算法將在兩個(gè)導(dǎo)航點(diǎn)之間運(yùn)行。
6.4?極限路徑長(zhǎng)度
當(dāng)?shù)貓D中的條件或者秩序會(huì)發(fā)生改變時(shí),保存一條長(zhǎng)路徑是沒(méi)有意義的,因?yàn)樵趶哪承c(diǎn)開始,后邊的路徑已經(jīng)沒(méi)有用了。每個(gè)物體都可以保存路徑開始時(shí)的特定幾步,然后當(dāng)路徑已經(jīng)沒(méi)用時(shí)重新計(jì)算路徑。這種方法慮及了(allows for)對(duì)每個(gè)物體使用數(shù)據(jù)的總量的管理。
6.5?總結(jié)
在游戲中,路徑潛在地花費(fèi)了許多存儲(chǔ)空間,特別是當(dāng)路徑很長(zhǎng)并且有很多物體需要尋路時(shí)。路徑壓縮,導(dǎo)航點(diǎn)和beacons通過(guò)把多個(gè)步驟保存為一個(gè)較小數(shù)據(jù)從而減少了空間需求。Waypoints rely on straight-line segments being common so that we have to store only the endpoints, while beacons rely on there being well-known paths calculated beforehand between specially marked places on the map.(譯者:此處不好翻譯,暫時(shí)保留原文)如果路徑仍然用了許多存儲(chǔ)空間,可以限制路徑長(zhǎng)度,這就回到了經(jīng)典的時(shí)間-空間折衷法:為了節(jié)省空間,信息可以被丟棄,稍后才重新計(jì)算它。
轉(zhuǎn)載于:https://www.cnblogs.com/alexanderkun/p/4599287.html
總結(jié)
- 上一篇: iOS应用跳转qq指定联系人聊天
- 下一篇: [转]oracle中查询指定行数的记录