前言:尋路是游戲比較重要的一個組成部分。因為不僅AI還有很多地方(例如RTS游戲里操控人物點到地圖某個點,然后人物自動尋路走過去)都需要用到自動尋路的功能。
本文將介紹一個經常被使用且效率理想的尋路方法——A*尋路算法,並且提供額外的優化思路。
圖片及信息參考自:https://www.gamedev.net/articles/programming/artificial-intelligence/a-pathfinding-for-beginners-r2003/
A*算法介紹
尋路,即找到一條從某個起點到某個終點的可通過路徑。而因為實際情況中,起點和終點之間的直線方向往往有障礙物,便需要一個搜索的算法來解決。
有一定算法基礎的同學可能知道從某個起點到某個終點通常使用深度優先搜索(DFS),DFS搜索的搜索方向一般是8個方向(如果不允許搜索斜向,則有4個),但是並無優先之分。
為了讓DFS搜索更加高效,結合貪心思想,我們給搜索方向賦予了優先級,直觀上離終點最近的方向(直觀上的意思是無視障礙物的情況下)為最優先搜索方向,這就是A*算法。
A*算法步驟解析
(如下圖,綠色為起點,紅色為終點,藍色為不可通過的牆。)
從起點開始往四周各個方向搜索。
(這里的搜索方向有8個方向)
為了區分搜索方向的優先級,我們給每個要搜索的點賦予2個值。
G值(耗費值):指從起點走到該點要耗費的值。
H值(預測值):指從該點走到終點的預測的值(從該點到終點無視障礙物情況下預測要耗費的值,也可理解成該點到終點的直線距離的值)
在這里,值 = 要走的距離
(實際上,更復雜的游戲,因為地形不同(例如陷阱,難走的沙地之類的),還會有相應不同的權值:值 = 要走的距離 * 地形權值)
我們還定義直着走一格的距離等於10,斜着走一格的距離等於14(因為45°斜方向的長度= sqrt(10^2+10^2) ≈ 14)
F值(優先級值):F = G + H
這條公式意思:F是從起點經過該點再到達終點的預測總耗費值。通過計算F值,我們可以優先選擇F值最小的方向來進行搜索。
(每個點的左上角為F值,左下角為G值,右下角為H值)
計算出每個方向對應點的F,G,H值后,
還需要給這些點賦予當前節點的指針值(用於回溯路徑。因為一直搜下去搜到終點后,如果沒有前一個點的指針,我們將無從得知要上次經過的是哪個點,只知道走到終點最終耗費的最小值是多少)
然后我們將這些點放入openList(開啟列表:用於存放可以搜索的點)。
然后再將當前點放入closeList(關閉列表:用於存放已經搜索過的點,避免重復搜索同一個點)
然后再從openList取出一個F值最小(最優先方向)的點,進行上述同樣的搜索。
在搜索過程中,如果搜索方向上的點是障礙物或者關閉列表里的點,則跳過之。
通過遞歸式的搜索,多次搜索后,最終搜到了終點。
搜到終點后,然后通過前一個點的指針值,我們便能從終點一步步回溯通過的路徑點。
(紅色標記了便是回溯到的點)
A*算法優化思路
openList使用優先隊列(二叉堆)
可以看到openlist(開啟列表),需要實時添加點,還要每次取出最小值的點。
所以我們可以使用優先隊列(二叉堆)來作為openList的容器。
優先隊列(二叉堆):插入一個點的復雜度為O(logN),取出一個最值點復雜度為O(logN)
障礙物列表,closeList 使用二維表(二維數組)
由於障礙物列表和closeList僅用來檢測是否能通過,所以我們可以使用bool二維表來存放。
//假設已經定義Width和Height分別為地圖的長和寬 bool barrierList[Width][Height]; bool closetList[Width][Height];
有某個點(Xa,Yb),可以通過
if(barrierList[Xa][Yb]&&closeList[Xa][Yb])來判斷。
因為二維表用下標訪問,效率很高,但是耗空間比較多。(三維地圖使用三維表則更耗內存。不過現在計算機一般都不缺內存空間,所以盡量提升運算時間為主)
這是一個典型的犧牲內存空間換取運算時間的例子。
深度限制
有時要搜的路徑非常長,利用A*算法搜一次付出的代價很高,造成游戲的卡頓。
那么為了保證每次搜索不會超過一定代價,可以設置深度限制,每搜一次則深度+1,搜到一定深度限制還沒搜到終點,則返還失敗值。
A*算法實現(C++代碼)
1 #include <iostream> 2 #include <list> 3 #include <vector> 4 #include <queue> 5 6 struct OpenPoint{ 7 int x; 8 int y; 9 int cost; // 耗費值 10 int pred; // 預測值 11 OpenPoint* father; // 父節點 12 OpenPoint() = default; 13 OpenPoint(int pX,int pY, int endX, int endY, int c, OpenPoint* fatherp) : x(pX),y(pY),cost(c), father(fatherp) { 14 //相對位移x,y取絕對值 15 int relativeX = std::abs(endX - pX); 16 int relativeY = std::abs(endY - pY); 17 //x,y偏移值n 18 int n = relativeX - relativeY; 19 //預測值pred = (max–n)*14+n*10+c 20 pred = std::max(relativeX, relativeY) * 14 - std::abs(n) * 4 + c; 21 } 22 }; 23 24 //比較器,用以優先隊列的指針類型比較 25 struct OpenPointPtrCompare { 26 bool operator()(OpenPoint* a, OpenPoint* b) { 27 return a->pred > b->pred; 28 } 29 }; 30 31 const int width = 30; //地圖長度 32 const int height = 100; //地圖高度 33 char mapBuffer[width][height]; //地圖數據 34 int depth = 0; //記錄深度 35 const int depthLimit = 2000; //深度限制 36 bool closeAndBarrierList[width][height]; //記錄障礙物+關閉點的二維表 37 //八方的位置 38 int direction[8][2] = { {1,0},{0,1},{-1,0},{0,-1},{1,1},{ -1,1 },{ -1,-1 },{ 1,-1 } }; 39 //使用最大優先隊列 40 std::priority_queue<OpenPoint*, std::vector<OpenPoint*>, OpenPointPtrCompare> openlist; 41 //存儲OpenPoint的內存空間 42 std::list<OpenPoint> pointList(depthLimit); 43 44 //是否在障礙物或者關閉列表 45 inline bool inBarrierAndCloseList(int pX,int pY) { 46 if (pX < 0 || pY < 0 || pX >= width || pY >= height) 47 return true; 48 return closeAndBarrierList[pX][pY]; 49 } 50 51 //創建一個開啟點 52 inline OpenPoint* createOpenPoint(int pX,int pY,int endX,int endY, int c, OpenPoint* fatherp) { 53 pointList.emplace_back(pX,pY,endX,endY, c, fatherp); 54 return &pointList.back(); 55 } 56 57 // 開啟檢查,檢查父節點 58 void open(OpenPoint& pointToOpen, int endX,int endY) { 59 //將父節點從openlist移除 60 openlist.pop(); 61 //深度+1 62 depth++; 63 //檢查p點八方的點 64 for (int i = 0; i < 4; ++i) 65 { 66 int toOpenX = pointToOpen.x + direction[i][0]; 67 int toOpenY = pointToOpen.y + direction[i][1]; 68 if (!inBarrierAndCloseList(toOpenX,toOpenY)) { 69 openlist.push(createOpenPoint(toOpenX, toOpenY, endX,endY, pointToOpen.cost + 10, &pointToOpen)); 70 } 71 } 72 for (int i = 4; i < 8; ++i) 73 { 74 int toOpenX = pointToOpen.x + direction[i][0]; 75 int toOpenY = pointToOpen.y + direction[i][1]; 76 if (!inBarrierAndCloseList(toOpenX, toOpenY)) { 77 openlist.push(createOpenPoint(toOpenX, toOpenY, endX, endY, pointToOpen.cost + 14, &pointToOpen)); 78 } 79 } 80 //最后移入closelist 81 closeAndBarrierList[pointToOpen.x][pointToOpen.y] = true; 82 } 83 84 //開始搜索路徑 85 std::list<OpenPoint*> findway(int startX,int startY, int endX,int endY) { 86 std::list<OpenPoint*> road; 87 // 創建並開啟一個父節點 88 openlist.push(createOpenPoint(startX,startY, endX,endY, 0, nullptr)); 89 OpenPoint* toOpen = nullptr; 90 //重復尋找預測和花費之和最小節點開啟檢查 91 while (!openlist.empty()) 92 { 93 toOpen = openlist.top(); 94 // 找到終點后,則停止搜索 95 if (toOpen->x == endX && toOpen->y ==endY) {break;}//若超出一定深度(1000深度),則搜索失敗 96 if (depth >= depthLimit) { 97 toOpen = nullptr; 98 break; 99 } 100 open(*toOpen, endX,endY); 101 } 102 for (auto rs = toOpen; rs != nullptr; rs = rs->father) {road.push_back(rs);} 103 return road; 104 } 105 106 //創建地圖 107 void createMap() { 108 for (int i = 0; i < width; ++i) 109 for (int j = 0; j < height; ++j) { 110 //五分之一概率生成障礙物,不可走 111 if (rand() % 5 == 0) { 112 mapBuffer[i][j] = '*'; 113 closeAndBarrierList[i][j] = true; 114 } 115 else { 116 mapBuffer[i][j] = ' '; 117 closeAndBarrierList[i][j] = false; 118 } 119 } 120 } 121 122 //打印地圖 123 void printMap() { 124 for (int i = 0; i < width; ++i) { 125 for (int j = 0; j < height; ++j) 126 std::cout << mapBuffer[i][j]; 127 std::cout << std::endl; 128 } 129 std::cout << std::endl << std::endl << std::endl; 130 } 131 132 int main() { 133 //起點 134 int beginX = 0; 135 int beginY = 0; 136 //終點 137 int endX = 29; 138 int endY = 99; 139 //創建地圖 140 createMap(); 141 //保證起點和終點都不是障礙物 142 mapBuffer[beginX][beginY] = mapBuffer[endX][endY] = ' '; 143 closeAndBarrierList[beginX][beginY] = closeAndBarrierList[endX][endY] = false; 144 //A*搜索得到一條路徑 145 std::list<OpenPoint*> road = findway(beginX,beginY,endX,endY); 146 //將A*搜索的路徑經過的點標記為'O' 147 for (auto& p : road){mapBuffer[p->x][p->y] = 'O';} 148 //打印走過路后的地圖 149 printMap(); 150 system("pause"); 151 return 0; 152 }
示例效果:
額外
若想了解更多關於尋路的東西,可以去了解下:
游戲AI之路徑規划:https://www.cnblogs.com/KillerAery/p/10283768.html
JPS/JPS+,一個更加快速的基於A*的改進算法:https://www.cnblogs.com/KillerAery/p/12242445.html
游戲AI 系列文章:https://www.cnblogs.com/KillerAery/category/1229106.html
