A*尋路算法
A*尋路算是遊戲開發中很常見的一種尋路算法,網絡上相關的介紹也非常多,這次從其他尋路算法談起,來看一看A星算法是如何誕生的,本文所有尋路算法使用TypeScript實現
算法默認權重不爲負數
廣度優先搜索
廣度優先搜索,就如同洪水一樣,從搜索起點不斷往外擴張
搜索實現
- 構建一個搜索隊列searchList,列表中的節點就是等待搜索的節點,初始化時將起點加入到列表中; 構建一個searchRecord記錄被搜索過的節點
- 從searchList搜索隊列中取出一個節點,獲取到該節點的臨近節點(這裏取上下左右4個節點),若臨近節點沒有被搜索過,將臨近節點加入到searchList中
- 重複步驟2,直到搜索完所有節點
提前結束
上面的搜索步驟是地圖中所有節點進行遍歷,實際上尋路都存在一個或多個目標,當搜索到目標就可以停止算法,避免不必要的查詢
關於searchRecord
serachRecord用來記錄搜索過的節點,當尋找到最終目標,最終的搜索路徑就通過serachRecord構建出來, 我這裏使用的是一個Dictionay來實現的,key是由節點的xy值構成的一個字符串,以表示一個唯一的節點,
比如,當我們以(5,5)爲起點,[(4, 5), (5, 4), (5, 6), (6, 5)]這四個點將會被搜索,那麼searchReocrd的結構就是這樣的
searchRecord = {
'4_5': Node(5, 5),
'5_4': Node(5, 5),
'5_6': Node(5, 5),
'6_5': Node(5, 5)
}
這樣的結構表明了key與value的父子關係: (5, 5) -> (4, 5), 當搜索繼續下去,可能會出現下面的結果
searchRecord = {
'4_5': Node(5, 5),
'3_5': Node(4, 5),
'2_5': Node(3, 5),
'1_5': Node(2, 5),
......
}
假如(1, 5)是我們的終點,那麼根據searchRecord我們可以輕鬆構建出最終的搜索路徑: (5, 5) -> (4, 5) -> (3, 5) -> (2, 5) -> (1, 5)
核心代碼實現
let searchQueue: Array<Node> = [start];
let searchRecord = {}
searchRecord[start.toString()] = true;
while(searchQueue.length){
let current = searchQueue.shift();
//找到目標,終止搜索
if(current.toString() == end.toString()){
break;
}
//獲取搜索節點的所有鄰居節點
let neighbors = this.getNeighbors(current);
for(let node of neighbors){
if(!searchRecord[node.toString()]){
searchQueue.push(node);
//確定父子關係
searchRecord[node.toString()] = current;
}
}
}
Dijkstra搜索
通常在遊戲中,角色在不同的環境,機動能力是不同的,角色通過一格平原可能花費1點行動力,通過山地就要花費3點,還有河流,冰川… 在廣度優先搜索中,searchList中的節點沒有權重的概念,而Dijkstra算法在廣度優先搜索的基礎上加入了權重,權重高(遊戲中可能表現爲移動損耗低)的節點會優先進行搜索, 以尋找移動損耗更低的路徑
搜索節點加權
Dijkstra搜索中,每個節點都有一個權重值,代表着移動所需要的消耗,廣度優先搜索其實也是特殊的Dijkstra搜索,所有的節點權重都是1
在上圖中,以綠色點爲起點進行搜索,首先將4個黃色的鄰居節點加入到搜索列表中,他們以降序權重(值越小權重越高)在列表中排列:[1, 3, 4, 5],下一輪搜索將取“1”節點進行搜索,重複上述的步驟,直到搜索結束
在算法中,新加入了一個Dictionary costRecord用來記錄從起點到每一個節點的消耗,每個節點的消耗=父節點的消耗+節點的權重值; 如果節點已經搜索過,就不再搜索
優先級隊列
這裏不能簡單的使用廣度優先搜索的list,在加入新的節點到searchList中時,我們需要對節點進行排序,搜索的時候直接從隊頭或隊尾(取決於排序的升降序)取出節點進行搜索
如何保證最後的路徑就是cost最低的路徑?
因爲在搜索循環中,始終是先搜索cost最低的節點,最先抵達終點的路徑必定是cost最低的路徑
上圖中,箭頭指向的節點表示是箭頭所在的節點是被箭頭指向的節點搜索到的,搜索的範圍像洪水一樣往外擴展,左上角由於被過高cost的節點阻擋,終止了搜索,searchRecord記錄了所有搜索路徑,因爲是先搜索最低cost的節點,所以最先抵達終點的路徑就是最低cost路徑(試想一下,算法以最低代價進行搜索找到的節點,還會有另一條代價更低的路徑到這個節點嗎)
核心代碼實現
let searchQueue: PriorityQueue = new PriorityQueue();
let searchRecord = {};
let costRecord = {};
searchQueue.put(start, 0);
searchRecord[start.toString()] = true;
costRecord[start.toString()] = 0;
while(searchQueue.length){
let current = searchQueue.get();
if(current.getNode().toString() == end.toString()){
break;
}
let neighbors = this.getNeighbors(current.node);
for(let node of neighbors){
let newCost = costRecord[current.node.toString()] + node.cost;
if(costRecord[node.toString()] === undefined){
searchRecord[node.toString()] = current.getNode();
costRecord[node.toString()] = newCost;
searchQueue.put(node, newCost);
}
}
}
最佳優先搜索
上面提到的兩種算法,都是在不斷的搜索地圖裏面的節點,當“碰巧”遇到了目標節點,才結束查詢,期間有太多無意義的搜索; 而我們尋路是知道目標的方位的,查找的時候直接朝這個放下搜索不久可以省去多餘的搜索了麼,最佳優先搜索就是這麼幹的
以離目標的距離爲優先級
該算法計算每一個節點到終點的距離,優先搜索距離目標最近的節點,這樣搜索就是朝着目標不斷前進的
何如結算節點與目標的距離
在我們的例子中,只有上下左右四個移動方向,使用曼哈頓距離最爲合適,所謂曼哈頓距離,就是兩個節點的x軸與y軸的距離之和
heuristics(start: Node, end: Node){
return Math.abs(start.x - end.x) + Math.abs(start.y - end.y);
}
核心代碼實現
let searchQueue: PriorityQueue = new PriorityQueue();
let searchRecord = {};
searchQueue.put(start, this.heuristics(start, end));
searchRecord[start.toString()] = true;
while(searchQueue.length){
let current = searchQueue.get();
if(current.getNode().toString() == end.toString()){
break;
}
let neighbors = this.getNeighbors(current.node);
for(let node of neighbors){
if(!searchRecord[node.toString()]){
searchRecord[node.toString()] = current.getNode();
searchQueue.put(node, this.heuristics(node, end));
}
}
}
最佳優先搜索的結果可能不是最短路徑
在多個結點cost相同的情況下,上圖是優先往上搜索,然而最佳優先搜索只是貪婪的朝目標前進,最後被障礙物擋住,只得向下繞行,所以本算法在存在障礙物的情況下(多數尋路都存在障礙物),不能夠保證是最短路徑,想要高效的找到最短路徑,就需要試試集各家之所長的A*算法了
A*搜索
A*融合了最佳優先搜索和Dijkstra搜索的優點,同時使用節點移動權重和節點離終點的距離,對節點的搜索優先級進行評估
有如下公式:
F(x) = G(x) + H(x)
- F(x)爲x節點的搜索權重,權重越小越先被搜索
- G(x)爲從起點移動到x節點的cost
- H(x)爲從x節點到終點的距離
核心代碼實現
我們只需要對最佳搜索算法稍加改造
let searchQueue: PriorityQueue = new PriorityQueue();
let searchRecord = {};
let costRecord = {};
searchRecord[start.toString()] = true;
costRecord[start.toString()] = 0;
searchQueue.put(start, this.heuristics(start, end) + costRecord[start.toString()]);
while(searchQueue.length){
let current = searchQueue.get();
if(current.getNode().toString() == end.toString()){
break;
}
let neighbors = this.getNeighbors(current.getNode());
for(let node of neighbors){
let newCost = node.cost + costRecord[current.getNode().toString()];
if(costRecord[node.toString()] === undefined){
costRecord[node.toString()] = newCost;
searchRecord[node.toString()] = current.getNode();
//節點的搜索優先級將由cost和離終點距離決定
searchQueue.put(node, newCost + this.heuristics(node, end));
}
}
}
各個算法的對比
A*算法
最佳優先算法
Dijkstra算法
廣度優先算法
最後
本文只是簡單講述了一下AStar是怎樣演變而來的,使用的Astar算法也是非常原始,現在Astar算法也在不斷的變化,出現了很多優化版本,有興趣深入研究的可以直接在網絡上查詢
尋路測試demo源碼地址:https://github.com/cdjinwei/SearchPath
參考文章:https://www.redblobgames.com/pathfinding/a-star/introduction.html