A*尋路,二叉堆優化及AS3實現

 遊戲時代羣雄並起,尋路乃中原逐鹿第一步,重要性不言而喻。今習得尋路戰術之首A*算法,爲大家操演一番,不足之處還望不吝賜教。可以選擇閱讀下面的內容,或者先看看 尋路示例AS3類代碼 及其 API文檔

牛刀小試 – A*尋路算法簡介

  eidiot掛帥出征,攜令牌一枚,率人馬若干,編制如下:

  • 尋路元帥
      尋路總指揮,執“行動令牌”一枚和“開啓士兵名錄”、“關閉將軍名錄”各一冊。憑“行動令牌”調兵遣將。
  • 預備士兵
      由元帥或預備將軍派往未探索區域,完成探索任務後授“開啓”軍銜,晉爲“開啓士兵”。發令派其出者爲其“父將”。
  • 開啓士兵
      前線待命。接到“行動令牌”後晉爲“預備將軍”執行探索任務。
  • 預備將軍
      憑“行動令牌”派出預備士兵至周圍未探索區域,並考察周圍“開啓士兵”狀態,以“父將”之名節制所派士兵。歸還“行動令牌”後授“關閉”軍銜,晉爲“關閉將軍”。
  • 關閉將軍
      後方待命。到達終點後依次報告“父將”直至元帥,尋路任務完成。

  爲協調行動,特頒軍令如下:

  • “預備士兵”只能由起點或“父將”所在格橫、豎或斜向移動一格,直向(橫、豎)移動一格走10步,斜向一格14步(斜向是直向1.414倍,取整數),抵達後不得再移動。
  • 所有人員需記下派出自己的“父將”、從起點到所在位置所走步數(G)、預計到達終點共需步數(F)。
    其中 F = G + H ,F 是從起點經過該點到終點的總路程,G 爲起點到該點的“已走路程”,H 爲該點到終點的“預計路程”。G 的計算是“父將”的 G 值加上“父將”位置到該位置所走步數,H 的計算是該點到終點“只走直路”所需路程。

  看看戰圖更容易理解,從紅色方格出發越過黃色障礙到達藍色方格:
0.gif
圖例:
00.gif
  由圖可形象看出何謂“開啓士兵”、“關閉將軍”:外圍的綠色方格爲“開啓士兵”,“前線待命”,隨時可向外繼續探索。內圍的紫色方格是“關閉將軍”,從終點開始沿箭頭尋其“父將”直至起點即得最終路徑。
  戰前會議結束,拔營出征。

  • 首先派出編號爲0的“預備士兵”偵查起點,然後升其爲“開啓士兵”,列入“開啓士兵名錄”。
  • 檢查“開啓士兵名錄”,找出F值最低的“開啓士兵”(只有一名人員,當然是0號),發出“行動令牌”派其執行探索任務。
  • 0號“開啓士兵”接到“行動令牌”,晉爲“預備將軍”,探索周圍格子。
  • 向周圍8個格子分別派出編號爲1到8的“預備士兵”,成爲這八名“預備士兵”的“父將”。
  • 八名“預備士兵”到達方格後計算G值和F值,報告0號“父將”,晉爲“開啓士兵”。
  • 0號“預備將軍”收到八名“開啓士兵”的報告,歸還“行動令牌”,晉爲“關閉將軍”。
  • 元帥收回“行動令牌”,將0號加入“關閉將軍名錄”,1到8號加入“開啓士兵名錄”。

  此過程結果如下(方格右上角數字是人員編號,左下角是G,右下角是H,左上角是F):
1.gif
  第一輪探索任務完成,元帥開始檢查“開啓士兵名錄”。此時名錄中有8名人員,其中1號F值最低爲40(起點右移一格,G值爲10,到終點平移3格,H值爲30,F = G + H = 40),向其發出“行動令牌”。

  • 1號“開啓士兵”接到“行動令牌”,晉爲“預備將軍”,探索周圍格子。
  • 周圍8個格子中有3格障礙,跳過。一格是“關閉將軍”,跳過。其餘四格是“開啓士兵”,檢查如果從該位置過去G值是否更低。以2號爲例,如果從1號過去G值爲 10 + 14 = 24 (1號的G值加上1號到2號的步數),而2號原來的G值是10,不做處理(如果此時發現新的G值更低,則更新2號的G值,並改2號的“父將”爲1號)。其他類推。
  • 1號檢測完周圍的方格,不需做任何處理,歸還“行動令牌”,晉爲“關閉將軍”。
  • 元帥收回“行動令牌”,將1號加入“關閉將軍名錄”。

  此過程結果如下:
2.gif
  第二輪結束,元帥再次檢查“開啓士兵名錄”。此時還有7名“開啓士兵”,5號和8號的F值都爲最低的54,選擇不同尋路的結果也將不同。元帥選擇了最後加入的8號“開啓士兵”發出“行動令牌”,過程同上,不贅述,結果如下:
3.gif
  重複這個過程直到某位“關閉將軍”站到了終點上(或者“開啓士兵”探測到了終點,這樣更快捷,但某些情況找到的路徑不夠短),亦即找到了路徑;或是“開啓士兵名錄”已空,無法到達終點。
  下面整理一下全過程並翻譯成“標準語言”,首先是各名詞:

  • “開啓士兵名錄” – 開啓列表 – open list
  • “關閉將軍名錄” – 關閉列表 – closed list
  • “父將” – 父節點 – parent square
  • F – 路徑評分
  • G – 起點到該點移動損耗
  • H – 該點到終點(啓發式)預計移動損耗

  尋路過程:

  • 1, 將起點放入開啓列表
  • 2, 尋找開放列表中F值最低的節點作爲當前節點
  • 3, 將當前節節點切換到關閉列表
  • 4, 如果當前節點是終點則路徑被找到,尋路結束
  • 5, 對於其周圍8個節點:
    • 如果不可通過或已在關閉列表,跳過,否則:
    • 如果已在開放列表中,檢查新路徑是否更好。如果新G值更低則更新其G值並改當前節點爲其父節點,否則跳過
    • 如果是可通過區域則放入開啓列表,計算這一點的F、G、H值,並記當前節點爲其父節點
  • 6, 如果開啓列表空了,則無法到達目標,路徑不存在。否則回到2

  再翻譯成“編程語言”?請看第三部分,鋒芒畢露 – AS3代碼和示例

如虎添翼 – 使用二叉堆優化

  如何讓A*尋路更快?元帥三顧茅廬,請來南陽二叉堆先生幫忙優化尋找“開啓士兵名錄”中最低F值的過程,將尋路速度提高了2到3倍,而且越大的地圖效果越明顯。下面隆重介紹二叉堆先生:
  下圖是一個二叉堆的例子,形式上看,它從頂點開始,每個節點有兩個子節點,每個子節點又各自有自己的兩個子節點;數值上看,每個節點的兩個子節點都比它大或和它相等。
binary1.gif
  在二叉堆裏我們要求:

  • 最小的元素在頂端
  • 每個元素都比它的父節點大,或者和父節點相等。

  只要滿足這兩個條件,其他的元素怎麼排都行。如上面的例子,最小的元素10在最頂端,第二小的元素20在10的下面,但是第三小的元素24在20的下面,也就是第三層,更大的30反而在第二層。
  這樣一“堆”東西我們在程序中怎麼用呢?幸運的是,二叉堆可以用一個簡單的一維數組來存儲,如下圖所示。
binary2.jpg
  假設一個元素的位置是n(第一個元素的位置爲1,而不是通常數組的第一個索引0),那麼它兩個子節點分別是 n × 2 和 n × 2 + 1 ,父節點是n除以2取整。比如第3個元素(例中是20)的兩個子節點位置是6和7,父節點位置是1。
  對於二叉堆我們通常有三種操作:添加、刪除和修改元素:

  • 添加元素
    首先把要添加的元素加到數組的末尾,然後和它的父節點(位置爲當前位置除以2取整,比如第4個元素的父節點位置是2,第7個元素的父節點位置是3)比較,如果新元素比父節點元素小則交換這兩個元素,然後再和新位置的父節點比較,直到它的父節點不再比它大,或者已經到達頂端,及第1的位置。
  • 刪除元素
    刪除元素的過程類似,只不過添加元素是“向上冒”,而刪除元素是“向下沉”:刪除位置1的元素,把最後一個元素移到最前面,然後和它的兩個子節點比較,如果較小的子節點比它小就將它們交換,直到兩個子節點都比它大。
  • 修改元素
    和添加元素完全一樣的“向上冒”過程,只是要注意被修改的元素在二叉堆中的位置。

  可以看出,使用二叉堆只需很少的幾步就可以完成排序,很大程度上提高了尋路速度。
  關於二叉堆先生需要了解的就是這麼多了,下面來看看他怎麼幫助元帥工作:

  • 每次派出的“預備士兵”都會獲得一個唯一的編號(ID),一直到尋路結束,它所有的數據包括位置、F值、G值、“父將”編號都將按這個ID存儲。
  • 每次有新的“開啓士兵”加入,二叉堆先生將它的編號加入“開啓士兵名錄”並重新排序,使F值最低的ID始終排在最前面
  • 當有“開啓士兵”晉爲“關閉將軍”,刪除“開啓士兵名錄”的第一個元素並重新排序
  • 當某個“開啓士兵”的F值被修改,更新其數據並重新排序

  注意,“開啓士兵名錄”裏存的只是人員的編號,數據全都另外存儲。不太明白?沒關係,元帥將在 第三部分 來次真刀實槍的大演兵。

鋒芒畢露 – AS3代碼和示例

  地形數據不屬於A*尋路的範圍,這裏定義一個 IMapTileModel 接口,由其它(模型)類來實現地圖通路的判斷。其它比如尋路超時的判斷這裏也不介紹,具體參考 AStar類及其測試代碼。這裏只介紹三部分主要內容:

數據存儲
  首先看看三個關鍵變量:

private var m_openList : Array; //開放列表,存放節點ID
private var m_openCount : int; //當前開放列表中節點數量
private var m_openId : int; //節點加入開放列表時分配的唯一ID(從0開始)

  開放列表 m_openList 是個二叉堆(一維數組),F值最小的節點始終排在最前。爲加快排序,開放列表中只存放節點ID ,其它數據放在各自的一維數組中:

private var m_xList : Array; //節點x座標
private var m_yList : Array; //節點y座標
private var m_pathScoreList : Array; //節點路徑評分F值
private var m_movementCostList : Array; //(從起點移動到)節點的移動耗費G值
private var m_fatherList : Array; //節點的父節點(ID)

  這些數據列表都以節點ID爲索引順序存儲。看看代碼如何工作:

//每次取出開放列表最前面的ID
currId = this.m_openList[0];
//讀取當前節點座標
currNoteX = this.m_xList[currId];
currNoteY = this.m_yList[currId];

  還有一個很關鍵的變量:

private var m_noteMap : Array; //節點(數組)地圖,根據節點座標記錄節點開啓關閉狀態和ID

  使用 m_noteMap 可以方便的存取任何位置節點的開啓關閉狀態,並可取其ID進而存取其它數據。m_noteMap 是個三維數組,第一維y座標(第幾行),第二維x座標(第幾列),第三維節點狀態和ID。判斷點(p_x, p_y)是否在開啓列表中:

return this.m_noteMap[p_y][p_x][NOTE_OPEN];

尋路過程
  AStar類 尋路的方法是 find() :

/**
* 開始尋路
* @param p_startX        起點X座標
* @param p_startY        起點Y座標
* @param p_endX        終點X座標
* @param p_endY        終點Y座標
* @return                 找到的路徑(二維數組 : [p_startX, p_startY], ... , [p_endX, p_endY])
*/
       
public function find(p_startX : int, p_startY : int, p_endX : int, p_endY : int) : Array{/* 尋路 */}

  注意這裏返回數據的形式:從起點到終點的節點數組,其中每個節點爲一維數組[x, y]的形式。爲了加快速度,類裏沒有使用Object或是Point,節點座標全部以數組形式存儲。如節點note的x座標爲note[0],y座標爲note[1]。
  下面開始尋路,第一步將起點添加到開啓列表:

this.openNote(p_startX, p_startY, 0, 0, 0);

  openNote() 方法將節點加入開放列表的同時分配一個唯一的ID、按此ID存儲數據對開啓列表排序。接下來是尋路過程:

while (this.m_openCount > 0)
{
    
//每次取出開放列表最前面的ID
    
currId = this.m_openList[0];
    
//將編碼爲此ID的元素列入關閉列表
    
this.closeNote(currId);
    
//如果終點被放入關閉列表尋路結束,返回路徑
    
if (currNoteX == p_endX && currNoteY == p_endY)
        
return this.getPath(p_startX, p_startY, currId);
    
//...每輪尋路過程
}
//開放列表已空,找不到路徑
return null;

  每輪的尋路:

//獲取周圍節點,排除不可通過和已在關閉列表中的
aroundNotes = this.getArounds(currNoteX, currNoteY);
//對於周圍每個節點
 
for each (var note : Array in aroundNotes)
{
    
//計算F和G值
    
cost = this.m_movementCostList[currId] + ((note[0] == currNoteX || note[1] == currNoteY) ? COST_STRAIGHT : COST_DIAGONAL);
    
score = cost + (Math.abs(p_endX - note[0]) + Math.abs(p_endY - note[1])) * COST_STRAIGHT;
    
if (this.isOpen(note[0], note[1])) //如果節點已在開啓列表中
    
{
        
//測試節點的ID
    
checkingId = this.m_noteMap[note[1]][note[0]][NOTE_ID];
    
//如果新的G值比節點原來的G值小,修改F,G值,換父節點
    
if(cost < this.m_movementCostList[checkingId])
    
{
        
this.m_movementCostList[checkingId] = cost;
        
this.m_pathScoreList[checkingId] = score;
        
this.m_fatherList[checkingId] = currId;
        
//對開啓列表重新排序
        
this.aheadNote(this.getIndex(checkingId));
    
}
    
} else //如果節點不在開放列表中
    
{
    
//將節點放入開放列表
    
this.openNote(note[0], note[1], score, cost, currId);
    
}
}

  從終點開始依次沿父節點回到到起點,返回找到的路徑:

/**
 * 獲取路徑
 * @param p_startX    起始點X座標
 * @param p_startY    起始點Y座標
 * @param p_id        終點的ID
 * @return             路徑座標數組
 */
       
private function getPath(p_startX : int, p_startY : int, p_id: int) : Array
{
    
var arr : Array = [];
    
var noteX : int = this.m_xList[p_id];
    
var noteY : int = this.m_yList[p_id];
    
while (noteX != p_startX || noteY != p_startY)
    
{
    
arr.unshift([noteX, noteY]);
    
p_id = this.m_fatherList[p_id];
    
noteX = this.m_xList[p_id];
    
noteY = this.m_yList[p_id];
    
}
    
arr.unshift([p_startX, p_startY]);
    
this.destroyLists();
    
return arr;
}

列表排序
  這部分看代碼和註釋就可以了,不多說:

/** 將(新加入開放別表或修改了路徑評分的)節點向前移動 */
private function aheadNote(p_index : int) : void
{
    
var father : int;
    
var change : int;
    
//如果節點不在列表最前
    
while(p_index > 1)
    
{
    
//父節點的位置
    
father = Math.floor(p_index2);
    
//如果該節點的F值小於父節點的F值則和父節點交換
    
if (this.getScore(p_index) < this.getScore(father))
    
{
        
change = this.m_openList[p_index - 1];
        
this.m_openList[p_index - 1] = this.m_openList[father - 1];
        
this.m_openList[father - 1] = change;
        
p_index = father;
    
} else
    
{
        
break;
    
}
    
}
}
/** 將(取出開啓列表中路徑評分最低的節點後從隊尾移到最前的)節點向後移動 */
private function backNote() : void
{
    
//尾部的節點被移到最前面
    
var checkIndex : int = 1;
    
var tmp : int;
    
var change : int;
    
while(true)
    
{
    
tmp = checkIndex;
    
//如果有子節點
    
if (2 * tmp <= this.m_openCount)
    
{
        
//如果子節點的F值更小
        
if(this.getScore(checkIndex) > this.getScore(2 * tmp))
        
{
        
//記節點的新位置爲子節點位置
        
checkIndex = 2 * tmp;
        
}
        
//如果有兩個子節點
        
if (2 * tmp + 1 <= this.m_openCount)
        
{
        
//如果第二個子節點F值更小
        
if(this.getScore(checkIndex) > this.getScore(2 * tmp + 1))
        
{
            
//更新節點新位置爲第二個子節點位置
            
checkIndex = 2 * tmp + 1;
        
}
        
}
    
}
    
//如果節點位置沒有更新結束排序
    
if (tmp == checkIndex)
    
{
        
break;
    
}
    
//反之和新位置交換,繼續和新位置的子節點比較F值
    
else
    
{
        
change = this.m_openList[tmp - 1];
        
this.m_openList[tmp - 1] = this.m_openList[checkIndex - 1];
        
this.m_openList[checkIndex - 1] = change;
    
}
    
}
}

  其中 getScore() 方法:

/**
 * 獲取某節點的路徑評分F值
 * @param p_index    節點在開啓列表中的索引(從1開始)
 */
       
private function getScore(p_index : int) : int
{
    
//開啓列表索引從1開始,ID從0開始,數組索引從0開始
    
return this.m_pathScoreList[this.m_openList[p_index - 1]];
}
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章