前言:寻路是游戏比较重要的一个组成部分。因为不仅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 #include2 #include 3 #include
4 #include 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 , OpenPointPtrCompare> openlist; 46 //存储OpenPoint的内存空间 47 std::vector pointList = std::vector (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 findway(const Point& start, const Point& end) { 90 std::list 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 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 }
示例效果:
题外话:关于B*寻路
在总结A*寻路的时候,我还偶然发现了另一个号称效率更高的B*的算法,而且看了定义以后,发现概念也很简单。
尝试实现后,实际发现该算法的确效率非常高。
但是B*算法类似于水往低处流的思路,它的路径结果往往不是最优的。
当然非最优解的路径也适用于游戏AI,因为这能让玩家觉得AI路径自然。
然而致命的是,当障碍物是凹多边形时(凹口朝向与玩家的探索方向相反时),B*算法很难实现绕爬出来,从而导致无解。
(而网上博客展示的障碍往往没有提到这种障碍情况)
一种解决方法是回溯绕爬,即限制最多可回退若干个节点,每次回退尝试一次绕爬,直到一次绕爬成功。
但是要是允许回溯过多节点,其复杂度也就和DFS差不多,丧失了其效率高的特性。
综合考虑,B*算法可能更加适合简单障碍的地图并且所需寻路不用较好的解。