客户端地图内寻路总结与优化
首先关于客户端的坐标体系:
菱形框是客户端使用的单位方格,也就是游戏里雷达显示的坐标。客户端中采用的等距视角,使用菱形方格能与平面的场景地图表现出2.5D的效果。红色矩形框则是客户端和服务端公用的坐标格。
寻路方法入口: bool StartFindPath(CPos start, CPos end, vector<Cvector2f>& path, int IgnoreSteps, int nRatio, bool bAnyDir, int nMaxStep)
(下面讲解具体的寻路实现时涉及到A*寻路和Dijkstra最短路算法的知识点,这里就不对这两者做详细介绍了。)
start和end寻路的起点和终点,均为像素点。path为引用参数,用于存储寻路结果。IgnoreSteps为忽略距离终点的格数,即离终点还有几格时即可停下。nMaxSteps是最大寻路步数,用于限制使用A*算法寻路时的超时时间。
寻路成功返回true,将线路的“拐点集”(即相邻两点间是可以直线连通没有障碍的,且任意非相邻的两点是不能直线连通的)存储在path中。否则返回false。
StartFindPath的寻路过程:
1.先用LinePath()检验起点与终点是否可以直线连通:
(1)LinePath需保证起点在场景内;终点参数在场景外则直接返回起点坐标。
(2)然后由起点和重点坐标得出直线方向,以最小单位(即上面说的菱形格)遍历从起点至终点的所有格判断是否有高障碍(高障碍一般为地形,人物角色和怪物一般为低障碍),成功返回true;否则返回遇到的第一个障碍格。
2.若直线寻路没有成功,接下来检查输入参数的起点和终点是否均不超过场景。
3.进入A*寻路算法FindPath():
(1)检查如果起点和终点相同则直接返回-1。
(2)将起点添加进open list中。
(3)循环执行下面操作直至open list为空 或 找到终点节点 或 超出最大寻路步数:
I. 在open list中找到并移除总花费值最小的节点(总花费=G+H;G为从起点到该节点的花费;H为从该点到终点的估计花费。H这里采用的是“曼哈顿距离”来估计从该节点到终点的花费,公式为横纵坐标差值的和:H=abs(start.x-end.x)+abs(start.y-end.y) )。
II. 在该处理节点的八个相邻方向上遍历,对于到达的新节点若没存入过open list则直接加入;否则检查由该处理节点到新节点的G是否更小,如果更小就更新其花费和前置节点;并重新调整open list的顺序。
实现该A*算法的过程中三个重要的数据结构:
(1) XPS_Node:存储位置节点的结构,包含了节点位置、G花费、H花费和到达该节点的前置节点等内容。
(2) XPS_Node** m_pOpenList:用数组实现的、以总花费做权值比较的最小堆。
(3) XPS_Node* m_aBacket:用于记录加入过open list的XPS_Node哈希表;用x、y坐标值计算哈希值;解决哈希冲突的方式是将哈希值相同的XPS_Node以链表结构存储在同一数组下标上。
4.如果A*寻路没有成功,则进入下面的Dijkstra最短路算法:
(1) 加载对应地图数据目录下的roadPoint.csv和pathlink.csv(这两个文件是在地图上预设好的各坐标顶点和相邻顶点间的距离),构建出Dijkstra算法需要的图。
(2) 找到距离start和end最近的且能够与之直线连通的顶点。将该两个顶点作为Dijkstra图中的起点和终点。
(3) 循环N次执行以下操作:
I. 找出到达花费最小的节点node,并作标记 使下次查找时忽略该点。
II. 对于所有与node节点相邻的顶点,检查由node到其相邻节点的到达花费是否更小( cost[node] + link[node][i] < cost[i] ),如果是则更新其到达花费,并将其前置节点置为node。
5.若Dijkstra寻路未成功(没有csv文件或找不到与起/终直连的顶点等),则进行大范围A*寻路(这一步往往耗时就很严重了玩家会有明显的寻路延迟)。
(如下示,虚线为起始点寻找能够直线连通的顶点;蓝色实线为Dijkstra得出的路径)
对寻路代码做的优化:
1.对Dijkstra算法使用堆优化:
原先实现中,每次寻找最小花费的节点步骤中,是通过遍历一次所有节点得出的。可以用个最小堆来存储节点的到达花费,那么每次寻找最小花费节点的复杂度由O(N)降为O(logN)。 (我这里实现最小堆是直接用的std的优先队列priority_queue)。
2.在使用Dijkstra算法寻路时,第一步是遍历图中的所有顶点中,找到能够与起点、终点位置直线连通的顶点。因为需要直线连通这一条件,因此在起/终点周围存在较多障碍格的时候很容易失效,而进入下面非常耗时的大范围A*寻路。因此将这一步改为:当直线连通失效时,则寻找与起/终点直线距离最近的若干顶点使用A*算法(超时步数限制在较小范围),来确定Dijkstra的图中起点和终点。
下面给出巨**谷地图(还是把游戏里具体的地图名遮上吧 :D)上的一些测试结果:
起点 |
终点 |
原始耗时 |
优化后 |
(79,42) |
(570,1097) |
151ms |
26ms |
(17,1104) |
(538,13) |
114ms |
32ms |
(246,47) |
(625,688) |
97ms |
23ms |
(577,808) |
(295,584) |
31ms |
29ms |
(36,105) |
(419,910) |
39ms |
39ms |
上述优化有些限制,就是需要寻路地图要要有相应的csv地图,并且有数量可观的结点,否则起不到较好的优化效果。而且第一步的Dijkstra算法的堆优化的实际作用并不大,毕竟数据量有限。