雖然A*(讀作A星)算法對(duì)初學(xué)者來(lái)說(shuō)是比較深?yuàn)W難懂,但是一旦你找到門(mén)路了,它又會(huì)變得非常簡(jiǎn)單。網(wǎng)上有很多解釋A*算法的文章,但是大多數(shù)是寫(xiě)給那些有一定基礎(chǔ)的人看的,而您看到的這一篇呢,是真正寫(xiě)給菜鳥(niǎo)的。
本篇文章并不想給這個(gè)算法題目作一些權(quán)威性論斷,而是闡述它的基本原理,并為你理解更多相關(guān)資料與討論打下基礎(chǔ)。文章末尾給出了一些比較好的鏈接,放在“進(jìn)階閱讀”一節(jié)之后。
最后,本文不是編程規(guī)范,你將可能使這里講述的東西編寫(xiě)成任何計(jì)算機(jī)語(yǔ)言。在本文的末尾我還給出了一個(gè)例子
程序
包的
下載
鏈接,也許正合你意。在這個(gè)包中有C++和Blitz Basic兩個(gè)版本的程序
代碼
,如果你只是想看看A*算法是如何運(yùn)作的,該包中也有可直接執(zhí)行的
文件
供你研究。
我們還是要超越自己的(把算法弄懂),所以,讓我們從頭開(kāi)始吧!
初步:搜索區(qū)域
我們假設(shè)某個(gè)人要從A點(diǎn)到達(dá)B點(diǎn),而一堵墻把這兩個(gè)點(diǎn)隔開(kāi)了,如下圖所示,綠色部分代表起點(diǎn)A,紅色部分代表終點(diǎn)B,藍(lán)色方塊部分代表之間的墻。
[圖一]
你首先會(huì)注意到我們把這一塊搜索區(qū)域分成了一個(gè)一個(gè)的方格,如此這般,使搜索區(qū)域簡(jiǎn)單化,正是尋找路徑的第一步。這種方法將我們的搜索區(qū)域簡(jiǎn)化成了一個(gè)普通的二維數(shù)組。數(shù)組中的每一個(gè)元素表示對(duì)應(yīng)的一個(gè)方格,該方格的狀態(tài)被標(biāo)記為可通過(guò)的和不可通過(guò)的。通過(guò)找出從A點(diǎn)到B點(diǎn)所經(jīng)過(guò)的方格,就能得到AB之間的路徑。當(dāng)路徑找出來(lái)以后,這個(gè)人就可以從一個(gè)格子中央移動(dòng)到另一個(gè)格子中央,直到抵達(dá)目的地。
這些格子的中點(diǎn)叫做
節(jié)點(diǎn)
。當(dāng)你在其他地方看到有關(guān)尋找路徑的東西時(shí),你會(huì)經(jīng)常發(fā)現(xiàn)人們?cè)谟懻摴?jié)點(diǎn)。為什么不直接把它們稱作方格呢?因?yàn)槟悴灰欢ㄒ涯愕乃阉鲄^(qū)域分隔成方塊,矩形、六邊形或者其他任何形狀都可以。況且節(jié)點(diǎn)還有可能位于這些形狀內(nèi)的任何一處呢?在中間、靠著邊,或者什么的。我們就用這種設(shè)定,因?yàn)楫吘惯@是最簡(jiǎn)單的情況。
開(kāi)始搜索
當(dāng)我們把搜索區(qū)域簡(jiǎn)化成一些很容易操作的節(jié)點(diǎn)后,下一步就要構(gòu)造一個(gè)搜索來(lái)尋找最短路徑。在A*算法中,我們從A點(diǎn)開(kāi)始,依次檢查它的相鄰節(jié)點(diǎn),然后照此繼續(xù)并向外擴(kuò)展直到找到目的地。
我們通過(guò)以下方法來(lái)開(kāi)始搜索:
1.
從A點(diǎn)開(kāi)始,將A點(diǎn)加入一個(gè)專門(mén)存放待檢驗(yàn)的方格的“開(kāi)放列表”中。這個(gè)開(kāi)放列表有點(diǎn)像一張購(gòu)物清單。當(dāng)前這個(gè)列表中只有一個(gè)元素,但一會(huì)兒將會(huì)有更多。列表中包含的方格可能會(huì)是你要途經(jīng)的方格,也可能不是。總之,這是一個(gè)包含待檢驗(yàn)方格的列表。
2.
檢查起點(diǎn)A相鄰的所有可達(dá)的或者可通過(guò)的方格,不用管墻啊,水啊,或者其他什么無(wú)效地形,把它們也都加到開(kāi)放列表中。對(duì)于每一個(gè)相鄰方格,將點(diǎn)A保存為它們的“父方格”。當(dāng)我們要回溯路徑的時(shí)候,父方格是一個(gè)很重要的元素。稍后我們將詳細(xì)解釋它。
3.
從開(kāi)放列表中去掉方格A,并把A加入到一個(gè)“封閉列表”中。封閉列表存放的是你現(xiàn)在不用再去考慮的方格。
此時(shí)你將得到如下圖所示的樣子。在這張圖中,中間深綠色的方格是你的起始方格,所有相鄰方格目前都在開(kāi)放列表中,并且以亮綠色描邊。每個(gè)相鄰方格有一個(gè)灰色的指針指向它們的父方格,即起始方格。
[圖二]
接下來(lái),我們?cè)陂_(kāi)放列表中選一個(gè)相鄰方格并再重復(fù)幾次如前所述的過(guò)程。但是我們?cè)撨x哪一個(gè)方格呢?具有最小F值的那個(gè)。
路徑排序
決定哪些方格會(huì)形成路徑的關(guān)鍵是下面這個(gè)等式:
F = G + H
這里
-
G=從起點(diǎn)A沿著已生成的路徑到一個(gè)給定方格的移動(dòng)開(kāi)銷(xiāo)。 -
H=從給定方格到目的方格的估計(jì)移動(dòng)開(kāi)銷(xiāo)。這種方式常叫做試探,有點(diǎn)困惑人吧。其實(shí)之所以叫做試探法是因?yàn)檫@只是一個(gè)猜測(cè)。在找到路徑之前我們實(shí)際上并不知道實(shí)際的距離,因?yàn)槿魏螙|西都有可能出現(xiàn)在半路上(墻啊,水啊什么的)。本文中給出了一種計(jì)算H值的方法,網(wǎng)上還有很多其他文章介紹的不同方法。
?
我們要的路徑是通過(guò)反復(fù)遍歷開(kāi)放列表并選擇具有最小F值的方格來(lái)生成的。本文稍后將詳細(xì)討論這個(gè)過(guò)程。我們先進(jìn)一步看看如何計(jì)算那個(gè)等式。
如前所述,G是從起點(diǎn)A沿著已生成的路徑到一個(gè)給定方格的移動(dòng)開(kāi)銷(xiāo),在本例中,我們指定每一個(gè)水平或者垂直移動(dòng)的開(kāi)銷(xiāo)為10,對(duì)角線移動(dòng)的開(kāi)銷(xiāo)為14。因?yàn)閷?duì)角線的實(shí)際距離是2的平方根(別嚇到啦),或者說(shuō)水平及垂直移動(dòng)開(kāi)銷(xiāo)的1.414倍。為了簡(jiǎn)單起見(jiàn)我們用了10和14這兩個(gè)值。比例大概對(duì)就好,我們還因此避免了平方根和小數(shù)的計(jì)算。這倒不是因?yàn)槲覀儽炕蛘哒f(shuō)不喜歡數(shù)學(xué),而是因?yàn)閷?duì)電腦來(lái)說(shuō),計(jì)算這樣的數(shù)字也要快很多。不然的話你會(huì)發(fā)現(xiàn)尋找路徑會(huì)非常慢。
我們要沿特定路徑計(jì)算給定方格的G值,辦法就是找出該方格的父方格的G值,并根據(jù)與父方格的相對(duì)位置(斜角或非斜角方向)來(lái)給這個(gè)G值加上14或者10。在本例中這個(gè)方法將隨著離起點(diǎn)方格越來(lái)越遠(yuǎn)計(jì)算的方格越來(lái)越多而用得越來(lái)越多。
有很多方法可以用來(lái)估計(jì)H值。我們用的這個(gè)叫做曼哈頓(Manhattan)方法,即計(jì)算通過(guò)水平和垂直方向的平移到達(dá)目的地所經(jīng)過(guò)的方格數(shù)乘以10來(lái)得到H值。之所以叫Manhattan方法是因?yàn)檫@就像計(jì)算從一個(gè)地方移動(dòng)到另一個(gè)地方所經(jīng)過(guò)的城市街區(qū)數(shù)一樣,而通常你是不能斜著穿過(guò)街區(qū)的。重要的是,在計(jì)算H值時(shí)并不考慮任何障礙物。因?yàn)檫@是對(duì)剩余距離的估計(jì)值而不是實(shí)際值(通常是要保證估計(jì)值不大于實(shí)際值――譯者注)。這就是為什么這個(gè)方式被叫做試探法的原因了。想要了解更多些嗎?
這里還有更多式子和關(guān)于試探法的額外說(shuō)明。
G和H相加就得到了F。第一步搜索所得到的結(jié)果如下圖所示。每個(gè)方格里都標(biāo)出了F、G和H值。如起點(diǎn)方格右側(cè)的方格標(biāo)出的,左上角顯示的是F值,左下角是G值,右下角是H值。
[圖三]
我們來(lái)看看這些方格吧。在有字母的方格中,G=10,這是因?yàn)樗谒椒较蛏想x起點(diǎn)只有一個(gè)方格遠(yuǎn)。起點(diǎn)緊挨著的上下左右都具有相同的G值10。對(duì)角線方向的方塊G值都是14。
H值通過(guò)估算到紅色目標(biāo)方格的曼哈頓距離而得出。用這種方法得出的起點(diǎn)右側(cè)方格到紅色方格有3個(gè)方格遠(yuǎn),則該方格H值就是30。上面那個(gè)方格有4個(gè)方格遠(yuǎn)(注意只能水平和垂直移動(dòng)),H就是40。你可以大概看看其他方格的H值是怎么計(jì)算出來(lái)的。
每一個(gè)方格的F值,當(dāng)然就不過(guò)是G和H值之和了。
繼續(xù)搜索
為了繼續(xù)搜索,我們簡(jiǎn)單的從開(kāi)放列表中選擇具有最小F值的方格,然后對(duì)選中的方格進(jìn)行如下操作:
4.
將其從開(kāi)放列表中移除,并加到封閉列表中。
5.
檢驗(yàn)所有的相鄰方格,忽略那些不可通過(guò)的或者已經(jīng)在封閉列表里的方格。如果這個(gè)相鄰方格不在開(kāi)放列表中,就把它添加進(jìn)去。并將當(dāng)前選定方格設(shè)為新添方格的父方格。
6.
如果某個(gè)相鄰方格已經(jīng)在開(kāi)放列表中了(意味著已經(jīng)探測(cè)過(guò),而且已經(jīng)設(shè)置過(guò)父方格――譯者),就看看有沒(méi)有到達(dá)那個(gè)方格的更好的路徑。也就是說(shuō),如果從當(dāng)前選中方格到那個(gè)方格,會(huì)不會(huì)使那個(gè)方格的G值更小。如果不能,就不進(jìn)行任何操作。
相反的,如果新路徑的G值更小,就將該相鄰方格的父方格重設(shè)為當(dāng)前選中方格。(在上圖中是改變其指針的方向?yàn)橹赶蜻x中方格。最后,重新計(jì)算那個(gè)相鄰方格的F和G值。如果你看糊涂了,下面會(huì)有圖解說(shuō)明。
好啦,咱們來(lái)看看具體點(diǎn)的例子。在初始時(shí)的9個(gè)方塊中,當(dāng)開(kāi)始方格被加到封閉列表后,開(kāi)放列表里還剩8個(gè)方格。在這八個(gè)方格當(dāng)中,位于起點(diǎn)方格右邊的那個(gè)方格具有最小的F值40。所以我們選擇這個(gè)方格作為下一個(gè)中心方格。下圖中它以高亮的藍(lán)色表示。
[圖四]
首先,我們將選中的方格從開(kāi)放列表中移除,并加入到封閉列表中(所以用亮藍(lán)色標(biāo)記)。然后再檢驗(yàn)它的相鄰節(jié)點(diǎn)。那么在它緊鄰的右邊的方格都是墻,所以不管它們。左邊挨著的是起始方格,而起始方格已經(jīng)在封閉列表中了,所以我們也不管它。
其他四個(gè)方格已經(jīng)在開(kāi)放列表中,那么我們就要檢驗(yàn)一下如果路徑經(jīng)由當(dāng)前選中方格到那些方格的話會(huì)不會(huì)更好,當(dāng)然,是用G值作為參考。來(lái)看看選中方格右上角的那一個(gè)方格,它當(dāng)前的G值是14,如果我們經(jīng)由當(dāng)前節(jié)點(diǎn)再到達(dá)那個(gè)方格的話,G值會(huì)是20(到當(dāng)前方格的G值是10,然后向上移動(dòng)一格就再加上10)。為20的G值比14大,因此這樣的路徑不會(huì)更好。你看看圖就會(huì)容易理解些。顯然從起始點(diǎn)沿斜角方向移動(dòng)到那個(gè)方格比先水平移動(dòng)一格再垂直移動(dòng)一格更直接。
當(dāng)我們按如上過(guò)程依次檢驗(yàn)開(kāi)放列表中的所有四個(gè)方格后,會(huì)發(fā)現(xiàn)經(jīng)由當(dāng)前方格的話不會(huì)形成更好的路徑,那我們就保持目前的狀況不變。現(xiàn)在我們已經(jīng)處理了所有相鄰方格,準(zhǔn)備到下一個(gè)方格吧。
我們?cè)俦闅v一下開(kāi)放列表,目前只有7個(gè)方格了。我們挑個(gè)F值最小的吧。有趣的是,目前這種情況下,有兩個(gè)F值為54的方格。那我們?cè)趺催x擇呢?其實(shí)選哪個(gè)都沒(méi)關(guān)系,要考慮到速度的話,選你最近加到開(kāi)放列表中的那一個(gè)會(huì)更快些。當(dāng)離目的地越來(lái)越近的時(shí)候越偏向于選最后發(fā)現(xiàn)的方格。實(shí)際上這個(gè)真的沒(méi)關(guān)系(對(duì)待這個(gè)的不同造成了兩個(gè)版本的A*算法得到等長(zhǎng)的不同路徑)。
那我們選下面的那個(gè)好了,就是起始方格右邊的,下圖所示的那個(gè)。
[圖五]
這一次,在我們檢驗(yàn)相鄰方格的時(shí)候發(fā)現(xiàn)右邊緊挨的那個(gè)是墻,就不管它了。上面挨著的那個(gè)也同樣忽略。還有右邊墻下面那個(gè)方格我們也不管。為什么呢?因?yàn)槟悴豢赡芮写侵苯拥竭_(dá)那個(gè)格子。實(shí)際上你得先向下走然后再通過(guò)那個(gè)方格。這個(gè)過(guò)程中是繞著墻角走。(注意:穿過(guò)墻角的這個(gè)規(guī)則是可選的,取決于你的節(jié)點(diǎn)是如何放置的。)
那么還剩下其他五個(gè)相鄰方格。當(dāng)前方格的下面那兩個(gè)還不在開(kāi)放列表中,那我們把它們加進(jìn)去并且把當(dāng)前方格作為它們的父方格。其他三個(gè)中有兩個(gè)已經(jīng)在封閉列表中了(兩個(gè)已經(jīng)在圖中用亮藍(lán)色標(biāo)記了,起始方格,上面的方格),所以就不用管了。最后那個(gè),當(dāng)前方格左邊挨著的,要檢查一下經(jīng)由當(dāng)前節(jié)點(diǎn)到那里會(huì)不會(huì)降低它的G值。結(jié)果不行,所以我們又處理完畢了,然后去檢驗(yàn)開(kāi)放列表中的下一個(gè)格子。
重復(fù)這個(gè)過(guò)程直到我們把目的方格加入到開(kāi)放列表中了,那時(shí)候看起來(lái)會(huì)像下圖這個(gè)樣子。
[圖六]
注意到?jīng)]?起始方格下兩格的位置,那里的格子已經(jīng)和前一張圖不一樣了。之前它的G值是28并且指向右上方的那個(gè)方格。現(xiàn)在它的G值變成了20并且指向了正上方的方格。這個(gè)改變是在搜索過(guò)程中,它的G值被核查時(shí)發(fā)現(xiàn)在某個(gè)新路徑下可以變得更小時(shí)發(fā)生的。然后它的父方格也被重設(shè)并且重新計(jì)算了G值和F值。在本例中這個(gè)改變看起來(lái)好像不是很重要,但是在很多種情況下這種改變會(huì)使到達(dá)目標(biāo)的最佳路徑變得非常不同。
那么我們?cè)鯓觼?lái)自動(dòng)得出實(shí)際路徑的呢?很簡(jiǎn)單,只要從紅色目標(biāo)方格開(kāi)始沿著每一個(gè)方格的指針?lè)较蛞苿?dòng),依次到達(dá)它們的父方格,最終肯定會(huì)到達(dá)起始方格。那就是你的路徑!如下圖所示。從A方格到B方格的移動(dòng)就差不多是沿著這個(gè)路徑從每個(gè)方格中心(節(jié)點(diǎn))移動(dòng)到另一個(gè)方格中心,直到抵達(dá)終點(diǎn)。簡(jiǎn)單吧!
[圖七]
A*
算法總結(jié)
1.
將開(kāi)始節(jié)點(diǎn)放入開(kāi)放列表(開(kāi)始節(jié)點(diǎn)的F和G值都視為0);
2.
重復(fù)一下步驟:
? ?? ?? ?? ?i.
在開(kāi)放列表中查找具有最小F值的節(jié)點(diǎn),并把查找到的節(jié)點(diǎn)作為
當(dāng)前節(jié)點(diǎn)
;
? ?? ?? ???ii.
把
當(dāng)前節(jié)點(diǎn)
從開(kāi)放列表刪除, 加入到封閉列表;
? ?? ?? ?iii.
對(duì)
當(dāng)前節(jié)點(diǎn)
相鄰的每一個(gè)節(jié)點(diǎn)依次執(zhí)行以下步驟:
1.
如果該
相鄰節(jié)點(diǎn)
不可通行或者該
相鄰節(jié)點(diǎn)
已經(jīng)在封閉列表中,則什么操作也不執(zhí)行,繼續(xù)檢驗(yàn)下一個(gè)節(jié)點(diǎn);
2.
如果該
相鄰節(jié)點(diǎn)
不在開(kāi)放列表中,則將該節(jié)點(diǎn)添加到開(kāi)放列表中, 并將該
相鄰節(jié)點(diǎn)
的父節(jié)點(diǎn)設(shè)為
當(dāng)前節(jié)點(diǎn)
,同時(shí)保存該
相鄰節(jié)點(diǎn)
的G和F值;
3.
如果該
相鄰節(jié)點(diǎn)
在開(kāi)放列表中, 則判斷若經(jīng)由
當(dāng)前節(jié)點(diǎn)
到達(dá)該
相鄰節(jié)點(diǎn)
的G值是否小于原來(lái)保存的G值,若小于,則將該
相鄰節(jié)點(diǎn)
的父節(jié)點(diǎn)設(shè)為
當(dāng)前節(jié)點(diǎn)
,并重新設(shè)置該
相鄰節(jié)點(diǎn)
的G和F值.
? ?? ???iv.
循環(huán)結(jié)束條件:
當(dāng)
終點(diǎn)節(jié)點(diǎn)
被加入到開(kāi)放列表作為待檢驗(yàn)節(jié)點(diǎn)時(shí), 表示路徑被找到,此時(shí)應(yīng)終止循環(huán);
或者當(dāng)開(kāi)放列表為空,表明已無(wú)可以添加的新節(jié)點(diǎn),而已檢驗(yàn)的節(jié)點(diǎn)中沒(méi)有終點(diǎn)節(jié)點(diǎn)則意味著路徑無(wú)法被找到,此時(shí)也結(jié)束循環(huán);
3.
從
終點(diǎn)節(jié)點(diǎn)
開(kāi)始沿父節(jié)點(diǎn)遍歷, 并保存整個(gè)遍歷到的節(jié)點(diǎn)坐標(biāo),遍歷所得的節(jié)點(diǎn)就是最后得到的路徑;
一點(diǎn)感慨
原諒我的離題。但畢竟值得指出的是,當(dāng)你在網(wǎng)上和論壇里看到很多討論A*路徑尋找算法的東西時(shí),偶爾會(huì)遇到一些人所指的A*算法并不是真正的A*的情況。實(shí)際上要應(yīng)用真正的A*需要包含我們上面討論的那些元素:專門(mén)的開(kāi)放列表和封閉列表,用F、G和H值來(lái)排序的路徑統(tǒng)計(jì)。有很多其他的尋找路徑算法,但是其他的都不是A*,而A*一般認(rèn)為是這些算法當(dāng)中最好的。本文末尾的參考文檔里有Bryan Stout對(duì)這些算法的討論。在特定情況下某些算法可能會(huì)更好,但是你至少要理解你要準(zhǔn)備做什么。好了,差不多了,還是回到主題吧。 實(shí)現(xiàn)時(shí)的注意事項(xiàng) 現(xiàn)在你已經(jīng)理解了基本的算法,下面將討論一些你在寫(xiě)自己的程序時(shí)要考慮的東西。下面一些材料和我用C++和Blitz Basic寫(xiě)的程序有關(guān),但是那些要點(diǎn)是適用于任何其他語(yǔ)言的。 1. 開(kāi)放列表的維護(hù):實(shí)際上是A*算法中最耗時(shí)的因素。每次你處理開(kāi)放列表時(shí)你都要從中找出具有最小F值的方格。有幾種方法可以做到。你可以保存所需的路徑元素,簡(jiǎn)單的遍歷列表來(lái)找出最小F值的方格。這種方法是很簡(jiǎn)單,但是在路徑很長(zhǎng)的時(shí)候會(huì)很慢。也可以改進(jìn)一下,變成維護(hù)一個(gè)有序列表,這樣在需要最小F值方格的時(shí)候僅須獲取該有序列表的第一個(gè)元素。我在寫(xiě)我那個(gè)程序的時(shí)候,開(kāi)始就是用的這個(gè)辦法。 這種方法很適合于地圖不大的情況。但這還不是最快的解決辦法。真正追求速度的認(rèn)真的程序員往往會(huì)使用二叉堆。這也是我在我的代碼中用的方法。據(jù)我的經(jīng)驗(yàn),這種方法會(huì)比大多數(shù)解決方案快至少2-3倍,在路徑很長(zhǎng)的時(shí)候更快(快10倍以上)。如果你有興趣了解更多二叉堆的內(nèi)容,去看看我的這篇文章, Using Binary Heaps in A* Pathfinding 。 2. 其他單位。如果你仔細(xì)看了我的例子代碼,你會(huì)注意到它完全忽略了地圖上的其他單位。我的尋路者實(shí)際上是直接穿過(guò)彼此的。這樣行得通行不通是取決于游戲的。如果你要考慮地圖上的其他單位并且它們還能夠移動(dòng),我建議你在路徑尋找算法中忽略它們而另外寫(xiě)一些代碼去檢測(cè)它們是否發(fā)生了碰撞。當(dāng)碰撞發(fā)生時(shí),你可以馬上生成一個(gè)新的路徑或者應(yīng)用一些標(biāo)準(zhǔn)移動(dòng)規(guī)則(比如總是靠右走等等)直到路徑上不再有障礙物或,然后再生成一個(gè)新的路徑。為什么在你最初計(jì)算路徑的時(shí)候就把這些單位考慮進(jìn)去呢?那其實(shí)是因?yàn)槠渌麊挝灰矔?huì)移動(dòng),它們的位置在不停的改變。這樣會(huì)導(dǎo)致產(chǎn)生一些不可思議的結(jié)果,比如某個(gè)單位會(huì)突然轉(zhuǎn)向來(lái)避免一個(gè)實(shí)際上已經(jīng)不在那兒的另一個(gè)單位,或者撞上一個(gè)后來(lái)移動(dòng)到它的預(yù)定路徑上的單位。 在尋路算法中忽略其他單位意味著你將不得不寫(xiě)一些單獨(dú)的代碼來(lái)避免沖突。這完全是由游戲特性決定的。所以我把解決方案留給你自己去琢磨。本文末尾參考文章一節(jié)Bryan Stout的一些地方提供了幾個(gè)解決方案(比如魯棒式跟蹤等等),不妨去看看。 3. 一些提速技巧:你在開(kāi)發(fā)你自己的A*程序或者改變我寫(xiě)的那個(gè)時(shí),總會(huì)發(fā)現(xiàn)這個(gè)尋路算法很耗CPU,特別是地圖上有大量的尋路者和地圖相當(dāng)大的時(shí)候。如果你在網(wǎng)上讀過(guò)一些資料你會(huì)知道這個(gè)對(duì)于像星際爭(zhēng)霸或者帝國(guó)時(shí)代這樣專業(yè)的游戲也不例外。當(dāng)你發(fā)現(xiàn)你的尋路算法使你寫(xiě)的東西變慢了的話,可以參考下面這些提速的辦法:
地形多樣化的開(kāi)銷(xiāo):在這個(gè)入門(mén)教程和我附帶的程序中,地形地貌只包括兩種情況:可通過(guò)的和不可通過(guò)的。但是假如還有一些地形那是可以通過(guò)的只是移動(dòng)的開(kāi)銷(xiāo)更大一點(diǎn)呢?比如沼澤、山丘、地牢里的梯子等等呢?這些都是可以通行的地形的例子,只不過(guò)比開(kāi)闊的平地具有更高的開(kāi)銷(xiāo)。類(lèi)似的,公路地形會(huì)比路郊的移動(dòng)開(kāi)銷(xiāo)小。 這個(gè)問(wèn)題很容易解決。只要在計(jì)算給定區(qū)塊的G值時(shí)把地形的開(kāi)銷(xiāo)加上去就行。把某個(gè)區(qū)塊加上額外的開(kāi)銷(xiāo),A*算法仍然有效。在我描述的那個(gè)簡(jiǎn)單例子里,地形只分可通過(guò)和不可通過(guò)兩種,A*算法會(huì)找到最直接最短的路徑。但當(dāng)在一個(gè)地形多樣化的環(huán)境里時(shí),最小開(kāi)銷(xiāo)路徑可能會(huì)是比較長(zhǎng)的一段路程。例如從公路上繞過(guò)去顯然比直接通過(guò)沼澤地開(kāi)銷(xiāo)要小。 還有一個(gè)有趣的辦法是專業(yè)人士們叫做“感應(yīng)映射”的東西。如同上面描述的多樣化地形開(kāi)銷(xiāo)一樣,你可以創(chuàng)建一個(gè)額外的點(diǎn)數(shù)制度并將之應(yīng)用于AI方面的路徑中。假設(shè)在一張地圖上有一堆家伙在守衛(wèi)一個(gè)山區(qū)要道,每次電腦派遣某人通過(guò)那條要道時(shí)都會(huì)被困住。那你就可以創(chuàng)建一個(gè)感應(yīng)地圖使得發(fā)生很多戰(zhàn)斗沖突的那些區(qū)塊開(kāi)銷(xiāo)增大,以此來(lái)告知電腦去選一個(gè)更安全的路徑,并避免僅憑著是最短路徑(但是更危險(xiǎn))就持續(xù)派遣人員通過(guò)某條路徑的愚蠢情形。 5. 處理未探索區(qū)域:你有玩過(guò)電腦總是知道怎么走,甚至地圖都還沒(méi)探索過(guò)的PC游戲嗎?由此看來(lái)這個(gè)路徑尋找算法真是好得不現(xiàn)實(shí)。還好這個(gè)問(wèn)題可以很容易解決。 方法就是為不同的玩家和電腦(不是每個(gè)單位,不然會(huì)耗掉好多內(nèi)存)創(chuàng)建一個(gè)獨(dú)立的“已知可通行”數(shù)組。每個(gè)數(shù)組包含了玩家已探索區(qū)域和未知區(qū)域的信息,并把未知區(qū)域全部視為可通行區(qū)域來(lái)對(duì)待,除非后來(lái)發(fā)現(xiàn)是其他的地形。使用這種方法的話,移動(dòng)單位就會(huì)繞到死角或犯類(lèi)似錯(cuò)誤,直到它們發(fā)現(xiàn)了周?chē)穆贰R坏┑貓D都被探索過(guò)了,路徑尋找算法就會(huì)正常運(yùn)行了。 |
平滑路徑:A*算法會(huì)得出開(kāi)銷(xiāo)最小路程最短的路徑,但沒(méi)法得到看起來(lái)最平滑的路徑。看看那個(gè)例子的最終路徑(圖七),該路徑的第一步是起始點(diǎn)的右下方的方格。如果第一步是正下方那個(gè)方格,那得到的路徑不是就要平滑很多嗎?
有幾種方法可以處理這個(gè)問(wèn)題。如在計(jì)算路徑的時(shí)候你可以給那些改變了路徑方向的方格一個(gè)額外開(kāi)銷(xiāo),使其G值增大。這樣,你可以沿著所得路徑看看哪些取舍能使你的路徑看起來(lái)更平滑。對(duì)于這個(gè)話題,可以看看 Toward More Realistic Pathfinding (是免費(fèi)的,但是查看需要注冊(cè))來(lái)了解更多。
7.
非方格的搜索區(qū)域:在上面的例子中,我們用的是簡(jiǎn)單的二維方格布局。你可以不這樣弄,不規(guī)則區(qū)域也行。想想棋盤(pán)游戲Risk和Risk里的那些國(guó)家。你可以設(shè)計(jì)一個(gè)那樣的尋路的關(guān)卡。要做這樣的關(guān)卡,你得創(chuàng)建一個(gè)查找表來(lái)存儲(chǔ)每個(gè)國(guó)家對(duì)應(yīng)的鄰國(guó),和從一個(gè)國(guó)家移動(dòng)到另一個(gè)國(guó)家的開(kāi)銷(xiāo)G。另外也得選一種計(jì)算估計(jì)值H的方法。其他的處理就和前面的例子差不多了。只是當(dāng)對(duì)新區(qū)域進(jìn)行檢驗(yàn)時(shí),即添加新項(xiàng)目到開(kāi)放列表中的時(shí)候,是從表中查找鄰近國(guó)家而不是選擇鄰近方格。
類(lèi)似的,你可以針對(duì)固定地形的地圖來(lái)創(chuàng)建一個(gè)路標(biāo)系統(tǒng)。路標(biāo)通常是橫穿路徑的點(diǎn),可能是在公路上也可能是在地牢的主隧道里。對(duì)于游戲設(shè)計(jì)者來(lái)說(shuō),需要提前設(shè)置好這些路標(biāo)。如果兩個(gè)路標(biāo)所成的直線路徑之間沒(méi)有障礙物的話就稱之為“相鄰的”。在Risk游戲的例子中,你會(huì)將相鄰信息保存在一個(gè)查找表中,并會(huì)在新增開(kāi)放列表元素時(shí)用到這個(gè)查找表。然后你會(huì)用到相關(guān)的G值(可能通過(guò)兩個(gè)節(jié)點(diǎn)間的直線距離得到)和H值(可能通過(guò)該節(jié)點(diǎn)到終點(diǎn)的直線距離得到)其他的運(yùn)算就和普通的差不多。
另外一個(gè)使用非方格搜索區(qū)域的斜視角RPG游戲的例子參見(jiàn)我的文章 Two-Tiered A* Pathfinding 。
進(jìn)階閱讀
好了!現(xiàn)在你基本上掌握了基礎(chǔ)知識(shí),并對(duì)高級(jí)的概念也有了些印象。在此我建議把我的代碼拿來(lái)研究研究。壓縮包里有兩個(gè)版本,分別是C++的和Blitz Basic的。它們的注釋都很詳細(xì),我想應(yīng)該很容易理解。下面是鏈接:
如果你沒(méi)法用C++或者Blitz Basic,在C++版里有兩個(gè)程序文件可以直接運(yùn)行。而B(niǎo)litz Basic版本要在 Blitz Basic的網(wǎng)站上下載免費(fèi)演示版的Blitz Basic 3D才能運(yùn)行。 這里還有Ben O’Neill寫(xiě)的在線示例。
你也應(yīng)通讀一下下面的網(wǎng)頁(yè)。這些在你讀了本文之后應(yīng)該很好理解了。
- Amit's A* Pages : 這是 Amit Patel 的一篇廣為引用的文章。不過(guò)如果你沒(méi)讀先讀本文的話看他這個(gè)可能會(huì)有些困惑。非常值得一讀。特別是 Amit 對(duì)此的獨(dú)特觀點(diǎn)。
- Smart Moves: Intelligent Path Finding : Gamasutra.com網(wǎng)站上Bryan的這篇文章是需要注冊(cè)才能閱讀的。不過(guò)注冊(cè)是免費(fèi)的而且為了這篇文章麻煩一點(diǎn)也值得。Bryan用Delphi寫(xiě)的那個(gè)程序幫助我了解了A*,那也是我的A*程序的靈感來(lái)源。這篇文章也寫(xiě)了一些A*的變種
- Terrain Analysis :這是一篇高階的文章,不過(guò)很有趣,它是由Ensemble Studios的專家Dave Pottinger所寫(xiě)。這家伙負(fù)責(zé)協(xié)調(diào)帝國(guó)時(shí)代II:Age of Kings的開(kāi)發(fā)。要想把這里所有東西都看明白是不可能的,不過(guò)它或許會(huì)給你自己的項(xiàng)目帶來(lái)一些有趣的想法。文章還包括一些關(guān)于材質(zhì)貼圖,感應(yīng)映射和其他一些高級(jí)AI/路徑尋找概念。其中關(guān)于“洪泛(flood filling)”的討論是我的死角和島嶼等地圖預(yù)處理代碼的靈感來(lái)源。在我的Blitz Basic版的程序中包含了這個(gè)東西。。
其他值得一讀的文章:
更多文章、技術(shù)交流、商務(wù)合作、聯(lián)系博主
微信掃碼或搜索:z360901061

微信掃一掃加我為好友
QQ號(hào)聯(lián)系: 360901061
您的支持是博主寫(xiě)作最大的動(dòng)力,如果您喜歡我的文章,感覺(jué)我的文章對(duì)您有幫助,請(qǐng)用微信掃描下面二維碼支持博主2元、5元、10元、20元等您想捐的金額吧,狠狠點(diǎn)擊下面給點(diǎn)支持吧,站長(zhǎng)非常感激您!手機(jī)微信長(zhǎng)按不能支付解決辦法:請(qǐng)將微信支付二維碼保存到相冊(cè),切換到微信,然后點(diǎn)擊微信右上角掃一掃功能,選擇支付二維碼完成支付。
【本文對(duì)您有幫助就好】元
