公司每周的程序例会都会轮流一个人分享一些经验和技术,本周轮到我来分享,想了想最近写过一些A*寻路的功能,索性上网了解了一下除A*以外的其它的寻路算法,汇总到一起就凑成一个寻路系统的专题。
一、 寻路系统简介
寻路技术属于人工智能领域中的一个部分,它主要负责模拟真人在虚拟世界中移动的过程。在游戏制作中主要应用于MMORPG中的Npc移动,玩家的自动寻路,以及一些迷宫、连连看等游戏。
二、将地图信息构建成一个图结构
我的构建方法就是将人物模型分别放在地图的每一个区块,来检测该区块是否有模型阻碍人物的移动。
有的时候地形本身就是阻碍物,比如一座山,一个斜度比较大的坡,都会阻碍人物的移动。我的检测方法是从这个区块的中心位置向下发射一条垂直的射线,与该点的地形法线做向量的点积,求出角度,如果角度大于45度,则标记该区块为阻碍物。
一个简单场景的图数据
int map_data[100] =
{
1,1,1,1,1,1,1,1,1,1,
1,1,1,1,1,0,1,1,1,1,
1,1,1,1,1,0,1,1,1,1,
1,1,1,1,1,0,0,0,0,1,
1,1,0,1,1,0,1,1,1,1,
1,1,0,1,0,0,1,0,0,0,
1,1,0,1,0,1,1,1,1,1,
1,1,1,0,1,1,1,1,1,1,
1,1,1,0,1,1,1,1,1,1,
1,1,1,0,1,1,1,1,1,1
};
三、基本的状态空间搜索方式
几乎所有的寻路算法都是基于以下两种方式来进行的
深度优先搜索比较像一般人在三维迷宫中行走的过程,一条道走到黑,然后退回到上一个分支点,继续在一条道走到黑。它的效率是非常低的,完全比拼RP,如果RP不好的话它几乎要遍例到所有的结点,而且在一些超大型的地图中,深度优先搜索常常会求不到解。
以下地图 #为起点 @为终点 *为阻碍物
000000000000000000000000
00000000000#000000000000
000000000000000000000000
000000000*****0000000000
000000000*0@0*0000000000
000000000*000*0000000000
000000000000000000000000
广度优先搜索过程如下:
000876543211123456780000
000876543210123456780000
000876543211123456780000
000876544*****4456780000
000876555*090*5556780000
000876666*808*6666780000
000877777780877777780000
四、Dijkstra算法 (迪卡斯特拉)
Dijkstra算法是很典型的最短路径算法,主要特点是以起始点为中心向外层
层扩展,直到扩展到终点,在从终点回溯到起始点从而找到路径。
Dijkstra算法的搜索形态如下图:
如果横竖每走一格花费10的话,那么
斜着每走一格花费 10 * sqrt(2) 约等于14 ,由于计算机处理开方运算非常慢,所以我们用10和14的近似比例,避免了求根运算和小数。
Dijkstra算法使用到了两个列表,这两个列表同样也是A*和其它算法常常用到的。
Open列表 : 开启列表存放将要进行处理的结点。
Close列表: 关闭列表存放已经处理过的结点。
Dijkstra算法寻路步骤(演试):
1.从原点出发,遍历检查所有与之相连的节点,将原点和这些节点存放到表A中,并记录下两节点之间的代价。
2.将代价最小的代价值和这两节点移动到表B中(其中一个是原点)。
3.把这个节点所连接的子节点找出,放入到表A中,算出子节点到原点的代价
4.重复第二步和第三步直到表A为空。然后从终点沿父结点回溯到起始点从而找到路径。
五、双向dijkstra算法
很多算法都是通过对dijkstra算法改进、优化而产生的,比如下图的双向dijkstra算法,从开始点和结束点同时同周围搜索,相交于一点,在由这点反溯回开始和结束点从而找到路径
六、大名鼎鼎的A* 算法
前面的算法有一个很大的缺陷就是他们都是在一个给定的地图空间中穷举,这在地图不大的情况下是很合适的算法,可是当地图十分巨大,且不预测的情况下就不可取了,穷举的效率实在太低,甚至不可完成。在这里就要用到启发式搜索了。
启发式搜索就是对每一个搜索的位置进行评估,得到最好的位置,再从这个位置进行搜索直到目标。这样可以省略遍例大量无畏的结点,提高了效率。在启发式搜索中,对位置的估价是十分重要的。采用了不同的估价可以有不同的效果
dijkstra算法的基础上加上启发函数,不让它盲目的寻找,就衍生出很多启发式搜索算法。A* 是其中的一种。之所以加一个 * 号,是因为它对启发式函数是有限制和约束条件的。
A*算法的结点:
//结点类
class Node
{
public:
//该结点所在地图数组的索引
unsigned int x , z;
//该结点的父结点
Node* mParentNode;
//是否可走
bool bPass;
//该结点的估价值变量
unsigned int f , h , g;
};
f(n) = g(n) + h(n)
f(n)是节点n的总估价值
g(n)就是在地图中从初始点到该节点的花费
h(n)就是从该节点到终点的估计代价。
其中g(n)是已知的。
g'(n)是起点到n点的最短路径值。
h'(n)是n点到终点的最短路径值。
A*算法对估价函数的约束条件就是
1.g(n) >= g'(n); //大多数情况下都是满足的,可以不用考虑
2.h(n) <= h'(n);//可以证明应用这样的估价函数是可以找到最短路径的,也就是可采纳的
那么,采用这种满足以上两个条件的估价函数的启发式算法就叫A*算法。
由此可见,Dijkstra算法本身其实就是A*算法的一个特例,其中g(n)和A*算法一样,h(n) = 0,这种h(n)肯定小于h'(n),所以Dijkstra也是一种满足A*算法约束条件的,当然它也是所有A*算法中最挫的一个。
如果约束条件越多则排除的节点就越多,排除的节点越多那么说明估价函数越好,但在游戏开发中由于实时性的问题,估价函数的约束越多,它的计算量就越大,耗费的时间就越多,应该适当的减小h(n)的约束条件,但是算法的准确性就差了,这里是一个平衡的问题,想要很完美的平衡非常难。
常用估价函数:
设当前点的坐标为(x,y),结束点的坐标为(xB,yB)
1)Manhattan distance(曼哈顿距离):
曼哈顿距离是标准的启发式函数,即:两点在南北方向上的距离加上在东西方向上的距离。之所以被称为曼哈顿,是因为它看起来像计算城市中从一个街道走到另外一个街道的步数,由于城市中是不可能沿对角线斜着穿过建筑物的,所以该方法很适合估算街道多的城镇地图。
公式:
h(n) = abs(x-xB) + abs(y-yB );
2)对角线距离:
对角线距离比较适合寻路中允许对角运动的。
公式:
h(xi)=max(abs(x-xB),abs(y-yB))
3)Euclid distance (欧几里德距离):
如果寻路中单位可以沿着任意角度移动,那么可以使用直线距离。
h(n) = sqrt((x-xB)^2 + (y-yB)^2);
该函数运算代价较大,但是准确度较高,比较适合地图比较自由的野外地图。
A*寻路步骤(演试):
1) 将起点周围结点计算估价值并放入open表中。起点本身放入close表中;
2) 从Open表中取估价值最小的节点n , 判断如果是终点则结束寻路;
3) 遍例n节点周围所有结点 x 并计算G值;
While(n节点周围所有结点x)
If ( x结点在open表)
{
If(新G值 < x在open表中的旧G值)
{
更新open表中的x结点的父结点指针指向 n结点,并采用新G值计算x结点的f值;
}
}
Else If (x结点在close表)
{
If(新G值 < x在close表中的旧G值)
{
更新close表中的x结点的父结点指针指向 n结点,并采用新G值计算x结点的f值;
把X结点取出放入open表;
}
}
Else
{
求X结点的估价值;
将X结点放入OPEN表中;
}
把n结点从open取出,放入close表中;
按照估价值将open表中的所有节点排序,使f值最低的结点排在最前面;
返回重新执行第二步,直到找到终点,或者open表为空说明没有路径可以到达终点;
七、D*算法
D* 的全称实际上就是 Dynamic A Star ,最初的应用是做为美国航天局的火星探测器所采用的寻路算法。
上面的A* 算法在静态地图中寻路非常有效,但不适于动态地图,如阻碍物不断变化的动态环境下 , D*算法则更为有效。
上图细黑线为第一次计算出的最短路,红点部分为路径上发生变化的堵塞点,当机器人位于982点时,检测到前面发生路段堵塞,在该点重新根据新的信息计算
路径,圆圈为计算出的绕开堵塞部分的新的最短路径。