前言:尋路是遊戲比較重要的一個組成部分。由於不只AI還有不少地方(例如RTS遊戲裏操控人物點到地圖某個點,而後人物自動尋路走過去)都須要用到自動尋路的功能。ios
本文將介紹一個常常被使用且效率理想的尋路方法——A*尋路算法,而且提供額外的優化思路。算法
圖片及信息參考自:https://www.gamedev.net/articles/programming/artificial-intelligence/a-pathfinding-for-beginners-r2003/數組
尋路,即找到一條從某個起點到某個終點的可經過路徑。而由於實際狀況中,起點和終點之間的直線方向每每有障礙物,便須要一個搜索的算法來解決。函數
有必定算法基礎的同窗可能知道從某個起點到某個終點一般使用深度優先搜索(DFS),DFS搜索的搜索方向通常是8個方向(若是不容許搜索斜向,則有4個),可是並沒有優先之分。優化
爲了讓DFS搜索更加高效,結合貪心思想,咱們給搜索方向賦予了優先級,直觀上離終點最近的方向(直觀上的意思是無視障礙物的狀況下)爲最優先搜索方向,這就是A*算法。spa
(以下圖,綠色爲起點,紅色爲終點,藍色爲不可經過的牆。).net
從起點開始往四周各個方向搜索。3d
(這裏的搜索方向有8個方向)指針
爲了區分搜索方向的優先級,咱們給每一個要搜索的點賦予2個值。code
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值最小(最優先方向)的點,進行上述一樣的搜索。
在搜索過程當中,若是搜索方向上的點是障礙物或者關閉列表裏的點,則跳過之。
經過遞歸式的搜索,屢次搜索後,最終搜到了終點。
搜到終點後,而後經過前一個點的指針值,咱們便能從終點一步步回溯經過的路徑點。
(紅色標記了即是回溯到的點)
能夠看到openlist(開啓列表),須要實時添加點,還要每次取出最小值的點。
因此咱們可使用優先隊列(二叉堆)來做爲openList的容器。
優先隊列(二叉堆):插入一個點的複雜度爲O(logN),取出一個最值點複雜度爲O(logN)
因爲障礙物列表和closeList僅用來檢測是否能經過,因此咱們可使用bool二維表來存放。
//假設已經定義Width和Height分別爲地圖的長和寬 bool barrierList[Width][Height]; bool closetList[Width][Height];
有某個點(Xa,Yb),能夠經過
if(barrierList[Xa][Yb]&&closeList[Xa][Yb])來判斷。
由於二維表用下標訪問,效率很高,可是耗空間比較多。(三維地圖使用三維表則更耗內存。不過如今計算機通常都不缺內存空間,因此儘可能提高運算時間爲主)
這是一個典型的犧牲內存空間換取運算時間的例子。
有時要搜的路徑很是長,利用A*算法搜一次付出的代價很高,形成遊戲的卡頓。
那麼爲了保證每次搜索不會超過必定代價,能夠設置深度限制,每搜一次則深度+1,搜到必定深度限制還沒搜到終點,則返還失敗值。
1 #include <iostream> 2 #include <list> 3 #include <vector> 4 #include <queue> 5 6 struct Point { 7 int x; 8 int y; 9 bool operator == (const Point&otherPoint) { 10 return x == otherPoint.x && y == otherPoint.y; 11 } 12 }; 13 14 struct OpenPoint : public Point { 15 int cost; // 耗費值 16 int pred; // 預測值 17 OpenPoint* father; // 父節點 18 OpenPoint() = default; 19 OpenPoint(const Point & p, const Point& end, int c, OpenPoint* fatherp) :Point(p), cost(c), father(fatherp) { 20 //相對位移x,y取絕對值 21 int relativex = std::abs(end.x - p.x); 22 int relativey = std::abs(end.y - p.y); 23 //x,y偏移值n 24 int n = relativex - relativey; 25 //預測值pred = (max–n)*14+n*10+c 26 pred = std::max(relativex, relativey) * 14 - std::abs(n) * 4 + c; 27 } 28 }; 29 30 //比較器,用以優先隊列的指針類型比較 31 struct OpenPointPtrCompare { 32 bool operator()(OpenPoint* a, OpenPoint* b) { 33 return a->pred > b->pred; 34 } 35 }; 36 37 const int width = 30; //地圖長度 38 const int height = 100; //地圖高度 39 char mapBuffer[width][height]; //地圖數據 40 int deepth; //記錄深度 41 bool closeAndBarrierList[width][height]; //記錄障礙物+關閉點的二維表 42 //八方的位置 43 Point direction[8] = { {1,0},{0,1},{-1,0},{0,-1},{1,1},{ -1,1 },{ -1,-1 },{ 1,-1 } }; 44 //使用最大優先隊列 45 std::priority_queue<OpenPoint*, std::vector<OpenPoint*>, OpenPointPtrCompare> openlist; 46 //存儲OpenPoint的內存空間 47 std::vector<OpenPoint> pointList = std::vector<OpenPoint>(width*height); 48 49 //檢查函數 返還成功與否值 50 inline bool inBarrierAndCloseList(const Point & pos) { 51 if (pos.x < 0 || pos.y < 0 || pos.x >= width || pos.y >= height) 52 return true; 53 return closeAndBarrierList[pos.x][pos.y]; 54 } 55 56 //建立一個開啓點 57 inline OpenPoint* createOpenPoint(const Point & p, const Point& end, int c, OpenPoint* fatherp) { 58 pointList.emplace_back(p, end, c, fatherp); 59 return &pointList.back(); 60 } 61 62 // 開啓檢查,檢查父節點 63 void open(OpenPoint& pointToOpen, const Point & end) { 64 //每檢查一次,深度+1 65 deepth++; 66 //將父節點從openlist移除 67 openlist.pop(); 68 Point toCreate; 69 //檢查p點八方的點 70 for (int i = 0; i < 4; ++i) 71 { 72 toCreate = Point{ pointToOpen.x + direction[i].x, pointToOpen.y + direction[i].y }; 73 if (!inBarrierAndCloseList(toCreate)) { 74 openlist.push(createOpenPoint(toCreate, end, pointToOpen.cost + 10, &pointToOpen)); 75 closeAndBarrierList[toCreate.x][toCreate.y] = true; 76 } 77 } 78 for (int i = 4; i < 8; ++i) 79 { 80 toCreate = Point{ pointToOpen.x + direction[i].x, pointToOpen.y + direction[i].y }; 81 if (!inBarrierAndCloseList(toCreate)) { 82 openlist.push(createOpenPoint(toCreate, end, pointToOpen.cost + 15, &pointToOpen)); 83 closeAndBarrierList[toCreate.x][toCreate.y] = true; 84 } 85 } 86 } 87 88 //開始搜索路徑 89 std::list<OpenPoint*> findway(const Point& start, const Point& end) { 90 std::list<OpenPoint*> road; 91 deepth = 0; 92 // 建立並開啓一個父節點 93 openlist.push(createOpenPoint(start, end, 0, nullptr)); 94 closeAndBarrierList[start.x][start.y] = false; 95 OpenPoint* toOpen = nullptr; 96 // 重複尋找預測和花費之和最小節點開啓檢查 97 while (!openlist.empty()) 98 { 99 toOpen = openlist.top(); 100 // 找到終點後,則中止搜索 101 if (*toOpen == end) { 102 break; 103 } 104 //若超出必定深度(1000深度),則搜索失敗 105 else if (deepth >= 1000) { 106 toOpen = nullptr; 107 break; 108 } 109 open(*toOpen, end); 110 } 111 for (auto rs = toOpen; rs != nullptr; rs = rs->father) { 112 road.push_back(rs); 113 } 114 return road; 115 } 116 117 //建立地圖 118 void createMap() { 119 for (int i = 0; i < width; ++i) 120 for (int j = 0; j < height; ++j) { 121 //五分之一律率生成障礙物,不可走 122 if (rand() % 5 == 0) { 123 mapBuffer[i][j] = '*'; 124 closeAndBarrierList[i][j] = true; 125 } 126 else { 127 mapBuffer[i][j] = ' '; 128 closeAndBarrierList[i][j] = false; 129 } 130 } 131 } 132 133 //打印地圖 134 void printMap() { 135 for (int i = 0; i < width; ++i) { 136 for (int j = 0; j < height; ++j) 137 std::cout << mapBuffer[i][j]; 138 std::cout << std::endl; 139 } 140 std::cout << std::endl << std::endl << std::endl; 141 } 142 143 int main() { 144 //起點 145 Point begin = { 0,0 }; 146 //終點 147 Point end = { 29,99 }; 148 //建立地圖 149 createMap(); 150 //打印初始化的地圖 151 printMap(); 152 //保證起點和終點都不是障礙物 153 mapBuffer[begin.x][begin.y] = mapBuffer[end.x][end.y] = ' '; 154 closeAndBarrierList[begin.x][begin.y] = closeAndBarrierList[end.x][end.y] = false; 155 //A*搜索獲得一條路徑 156 std::list<OpenPoint*> road = findway(Point{ begin.x,begin.y }, Point{ end.x,end.y }); 157 //將A*搜索的路徑通過的點標記爲'O' 158 for (auto& p : road) { 159 mapBuffer[p->x][p->y] = 'O'; 160 } 161 //打印走過路後的地圖 162 printMap(); 163 system("pause"); 164 return 0; 165 }
示例效果:
在總結A*尋路的時候,我還偶然發現了另外一個號稱效率更高的B*的算法,並且看了定義之後,發現概念也很簡單。
嘗試實現後,實際發現該算法的確效率很是高。
可是B*算法相似於水往低處流的思路,它的路徑結果每每不是最優的。
固然非最優解的路徑也適用於遊戲AI,由於這能讓玩家以爲AI路徑天然。
然而致命的是,當障礙物是凹多邊形時(凹口朝向與玩家的探索方向相反時),B*算法很難實現繞爬出來,從而致使無解。
(而網上博客展現的障礙每每沒有提到這種障礙狀況)
一種解決方法是回溯繞爬,即限制最多可回退若干個節點,每次回退嘗試一次繞爬,直到一次繞爬成功。
可是要是容許回溯過多節點,其複雜度也就和DFS差很少,喪失了其效率高的特性。
綜合考慮,B*算法可能更加適合簡單障礙的地圖而且所需尋路不用較好的解。