JPS(Jump Point Search)寻路
参考资料:2018腾讯移动游戏技术评审标准与实践案例
https://blog.csdn.net/yuxikuo_1/article/details/50406651
http://blog.sina.com.cn/s/blog_4a5c75d40102wo5l.html
https://github.com/SkylerAlvarez/JumpPointSearch
一、什么是Jump Point Search?
JPS 又名跳点搜索算法(Jump Point Search),是由澳大利亚两位教授于 2011年提出的基于 Grid 格子的寻路算法。JPS算法在保留A算法的框架的同时,进一步优化了 A算法寻找后继节点的操作。
个人理解,A*是寻找所有的当前点的邻居,会有频繁的点集合加入和删除到点集合操作,jps的优点是会根据当前点的方向及当前点周围邻居的特点进行选择某些特殊的点才执行加入和删除到点集合操作。
JPS类比于KMP算法,有个共同点在于把重复多余的计算通过数据的特性省略,kmp中的next数组是对子字符串的特性的记录,在匹配时根据这些特性跳过多余的计算。jps也是根据邻居点的特性跳过其他多余的点。
这个也是多数优化算法的一个方式。
从论文中的图也可以看出这个特点,M.Time 表示操作 openset 和 closedset 的时间,G.Time 表示搜索后继节点的时间。可见 A*大约有 58%的时间在操作 openset 和 closedset,42%时间在搜索后继节点;而 JPS 大约 14%时间在操作 openset 和 closedset,86%时间在搜索后继节点。
二、寻路流程
P S : 本 文 定 义 无 法 直 接 走 到 斜 对 角 , 比 如 ( 1 , 1 ) 点 无 法 直 接 走 到 ( 2 , 2 ) 点 , 需 要 的 话 也 可 以 , 要 改 点 逻 辑 \color{red}{PS:本文定义无法直接走到斜对角,比如(1,1)点无法直接走到(2,2)点,需要的话也可以,要改点逻辑} PS:本文定义无法直接走到斜对角,比如(1,1)点无法直接走到(2,2)点,需要的话也可以,要改点逻辑
一.定义
1.点:当前点为:current,邻居点为neighbor,上一点parent
2.点集合:寻路过程中需要保存有效点的集合,分为可探索点集合openset,已探索点集合closedset。
3.路径权值:gCost为起点经过其他点到当前点的代价和,hCost为到目标点的代价,fCost为当前点的与起点终点间价值的和即fCost=gCost+hCost。
4.强迫邻居 (forced neighbour):如果点 neighbor 是 current 的邻居,并且点 neighbor 的邻居有阻挡(不可行走的格子),并且从 parent、current、neighbor 的路径长度比其他任何从 parent到neighbor 且不经过 current 的路径短,其中 parent为路径中 current 的前一个点,则 neighbor 为 current 的强迫邻居,current 为 neighbor 的跳点)
5.跳点(jump point):(1)如果点 y 是起点或目标点,则 y 是跳点,(2)如果 y 有邻居且是强迫邻居则 y 是跳点, 从上文强迫邻居的定义来看 neighbor 是强迫邻居,current 是跳点,二者的关系是伴生的,(3)如果 parent到 y 是对角线移动,并且 y 经过水平或垂直方向移动可以到达跳点,则 y 是跳点。
个人理解:附近有障碍才有强迫邻居,跳点及强迫邻居都可以改变当前的方向,有强迫邻居就是跳点。看文字理解不便,可以看图,主要特点是在当前点旁边有障碍,并且障碍上方可到达。就有强迫邻居。就是图中标黑点部分。
二.规则
规则一,JPS 搜索跳点的过程中,如果直线方向(为了和对角线区分,直线方向代表水平方向、垂直方向,下文所说的直线均为水平方向和垂直方向)、对角线方向都可以移动,则首先在直线方向搜索跳点,再在对角线方向搜索跳点。
规则二,(1)如果从 parent到current 是直线移动,n 是 current 的邻居,若有从 parent到 n 的路径不经过 current且路径长度小于或等于从 parent经过 x 到 n 的路径,则走到 current 后下一个点不会走到 n;(2)如果从 parent(到 current是对角线移动,n 是 current的邻居,若有从 parent到n 的路径不经过 current且路径长度小于从 parent经过 current 到 n 的路径,则走到 current 后下一个点不会走到 n。
规则三,只有跳点才会加入 openset,因为跳点会改变行走方向,而非跳点不会改变行走方向,最后寻找出来的路径点也只会是跳点集合的子集。
继续看图
简单概括,假如方向是往右上角方向,则判断右上方,上方,右方,上寻找跳点,如果邻居中有强迫邻居,当前点是跳点。
垂直方向则同理。
三.举例
5*5 的网格,黑色代表阻挡区,S 为起点,E 为终点。JPS 要寻找从 S 到 E 的最短路径,首先初始化将 S 加入 openset。从 openset 取出 F 值最小的点 S,并从 openset 删除,加入 closedset,S 的当前方向为空,则沿八个方向寻找跳点,在该图中只122有下、右、右下三个方向可走,但向下遇到边界,向右遇到阻挡,因此都没有找到跳点,然后沿右下方向寻找跳点,在 G 点,根据上文定义二的第(3)条,parent(G)为 S,praent(G)到 S 为对角线移动,并且 G 经过垂直方向移动(向下移动)可以到达跳点 I,因此 G 为跳点 ,将 G 加入 openset。从 openset 取出F 值最小的点 G,并从 openset 删除,加入 closedset,因为 G 当前方向为对角线方向(从 S 到 G 的方向),因此在右、下、右下三个方向寻找跳点,在该图中只有向下可走,因此向下寻找跳点,根据上文定义二的第(2)条找到跳点 I,将I 加入 openset。从 openset 取出 F 值最小的点 I,并从 openset 删除,加入closedset,因为 I 的当前方向为直线方向(从 G 到 I 的方向),在 I 点时 I 的左后方不可走且左方可走,因此沿下、左、左下寻找跳点,但向下、左下都遇到边界,只有向左寻找到跳点 Q(根据上文定义二的第(2)条)),因此将 Q 加入openset。从openset取出F值最小的点Q,并从openset删除,加入closedset,因为 Q 的当前方向为直线方向,Q 的左后方不可走且左方可走,因此沿右、左、左上寻找跳点,但向右、左上都遇到边界,只有向左寻找到跳点 E(根据上文定义二的第(1)条)),因此将 E 加入 openset。从 openset 取出 F 值最小的点E,因为 E 是目标点,因此寻路结束,路径是 S、G、I、Q、E。
出处:2018腾讯移动游戏技术评审标准与实践案例
三、实现重点代码分析
截取重点实现代码解析:
//Node包含路径代价,因为openset每次从中取fCost中最小值得点,openset采用堆的结构,每次加入点后自动堆排序
public int gCost;
public int hCost;
public int fCost
{
get
{
return gCost + hCost;
}
}
-----------------------------------------------
//比较fCost,C#中的基本类型都提供了默认的比较算法,C#可以调用比较算法为基本类型的数组进行排序。
若希望对自建类进行比较或排序,那么可以使用IComparable<T>和IComparer<T>接口。openSet需要用到堆类型数据
public interface IHeapItem<T> : IComparable<T>
{
int HeapIndex
{
get; set;
}
}
//自定义比较函数,
public int CompareTo(Node nodeToCompare)
{
int compare = fCost.CompareTo(nodeToCompare.fCost);
if (compare == 0)
{
compare = hCost.CompareTo(nodeToCompare.hCost);
}
return -compare;
}
//二叉堆排序主要功能
private void _SortUp(T item)
{
int parentIndex = (item.HeapIndex - 1) / 2;
while (true)
{
T parentItem = _items[parentIndex];
if (item.CompareTo(parentItem) > 0)
_Swap(item, parentItem);
else
break;
parentIndex = (item.HeapIndex - 1) / 2;
}
}
private void _SortDown(T item)
{
while (true)
{
int childLeftIndex = (item.HeapIndex * 2) + 1;
int childRightIndex = (item.HeapIndex * 2) + 2;
int swapIndex = 0;
if (childLeftIndex < _currentItemCount)
{
swapIndex = childLeftIndex;
if (childRightIndex < _currentItemCount)
if (_items[childLeftIndex].CompareTo(_items[childRightIndex]) < 0)
swapIndex = childRightIndex;
if (item.CompareTo(_items[swapIndex]) < 0)
_Swap(item, _items[swapIndex]);
else
return;
}
else
return;
}
}
------------------------------------
//获取最短路径
openSet.Add(_startNode);
openSetContainer.Add(_startNode);
while (openSet.Count > 0)
{
currentNode = openSet.RemoveFirst();
openSetContainer.Remove(_startNode);
............
}
//先将起点加入openSet,再取出fCost最小值点,即堆第一个值。
//获取此点的邻居,
//起点则parent点为null,遍历邻居非障碍点加入。
if (parentNode == null)
{
for (int x = -1; x <= 1; x++)
{
for (int y = -1; y <= 1; y++)
{
if (x == 0 && y == 0)
continue;
if (IsWalkable(x + currentNode.x, y + currentNode.y))
{
neighbours.Add(_grid[x + currentNode.x, y + currentNode.y]);
}
}
}
}
//非起点邻居点判断
int xDirection = Mathf.Clamp(currentNode.x - parentNode.x, -1, 1);
int yDirection = Mathf.Clamp(currentNode.y - parentNode.y, -1, 1);
//判断是否水平方向
if (xDirection != 0 && yDirection != 0)
{
//assumes positive direction for variable naming
bool neighbourUp = IsWalkable(currentNode.x, currentNode.y + yDirection);
bool neighbourRight = IsWalkable(currentNode.x + xDirection, currentNode.y);
bool neighbourLeft = IsWalkable(currentNode.x - xDirection, currentNode.y);
bool neighbourDown = IsWalkable(currentNode.x, currentNode.y - yDirection);
// 当前方向上可走点判断,
if (neighbourUp)
neighbours.Add(_grid[currentNode.x, currentNode.y + yDirection]);
if (neighbourRight)
neighbours.Add(_grid[currentNode.x + xDirection, currentNode.y]);
if (neighbourUp || neighbourRight)
if (IsWalkable(currentNode.x + xDirection, currentNode.y + yDirection))
neighbours.Add(_grid[currentNode.x + xDirection, currentNode.y + yDirection]);
//是否有强迫邻居
if (!neighbourLeft && neighbourUp)
if (IsWalkable(currentNode.x - xDirection, currentNode.y + yDirection))
neighbours.Add(_grid[currentNode.x - xDirection, currentNode.y + yDirection]);
if (!neighbourDown && neighbourRight)
if (IsWalkable(currentNode.x + xDirection, currentNode.y - yDirection))
neighbours.Add(_grid[currentNode.x + xDirection, currentNode.y - yDirection]);
}
else
{
//y水平方向
if (xDirection == 0)
{
if (IsWalkable(currentNode.x, currentNode.y + yDirection))
{
neighbours.Add(_grid[currentNode.x, currentNode.y + yDirection]);
if (!IsWalkable(currentNode.x + 1, currentNode.y))
if (IsWalkable(currentNode.x + 1, currentNode.y + yDirection))
neighbours.Add(_grid[currentNode.x + 1, currentNode.y + yDirection]);
if (!IsWalkable(currentNode.x - 1, currentNode.y))
if (IsWalkable(currentNode.x - 1, currentNode.y + yDirection))
neighbours.Add(_grid[currentNode.x - 1, currentNode.y + yDirection]);
}
}
else
{
//x水平方向
if (IsWalkable(currentNode.x + xDirection, currentNode.y))
{
neighbours.Add(_grid[currentNode.x + xDirection, currentNode.y]);
if (!IsWalkable(currentNode.x, currentNode.y + 1))
neighbours.Add(_grid[currentNode.x + xDirection, currentNode.y + 1]);
if (!IsWalkable(currentNode.x, currentNode.y - 1))
neighbours.Add(_grid[currentNode.x + xDirection, currentNode.y - 1]);
}
}
}
------------------------------------------------------------------------------
//根据可走邻居,判断是否满足跳点条件,假设可走到邻居,判断是否可以到达跳点。
//如果是斜方向,有强迫邻居,直接返回。
if ((!_grid.IsWalkable(currentNode.x - xDirection, currentNode.y) && _grid.IsWalkable(currentNode.x - xDirection, currentNode.y + yDirection)) ||
(!_grid.IsWalkable(currentNode.x, currentNode.y - yDirection) && _grid.IsWalkable(currentNode.x + xDirection, currentNode.y - yDirection)))
{
return currentNode;
}
//递归判断,斜方向,水平垂直方向可走,没有强迫邻居,继续斜方向寻找跳点。有则返回当前点。
Node nextHorizontalNode = _grid.GetNodeFromIndex(currentNode.x + xDirection, currentNode.y);
Node nextVerticalNode = _grid.GetNodeFromIndex(currentNode.x, currentNode.y + yDirection);
if (_Jump(nextHorizontalNode, currentNode, xDirection, 0) != null || _Jump(nextVerticalNode, currentNode, 0, yDirection) != null)
{
if (!_forced)
{
UnityEngine.Debug.Log(currentNode);
Node temp = _grid.GetNodeFromIndex(currentNode.x + xDirection, currentNode.y + yDirection);
if (temp != null && _grid.showDebug)
UnityEngine.Debug.DrawLine(new Vector3(currentNode.x, 1, currentNode.y), new Vector3(temp.x, 1, temp.y), Color.green, Mathf.Infinity);
return _Jump(temp, currentNode, xDirection, yDirection);
}
else
{
return currentNode;
}
}
//如果水平方向移动,没有强迫邻居,继续查找下一个点
if (xDirection != 0)
{
//
if ((_grid.IsWalkable(currentNode.x + xDirection, currentNode.y + 1) && !_grid.IsWalkable(currentNode.x, currentNode.y + 1)) ||
(_grid.IsWalkable(currentNode.x + xDirection, currentNode.y - 1) && !_grid.IsWalkable(currentNode.x, currentNode.y - 1)))
{
_forced = true;
return currentNode;
}
}
else
{
if ((_grid.IsWalkable(currentNode.x + 1, currentNode.y + yDirection) && !_grid.IsWalkable(currentNode.x + 1, currentNode.y)) ||
(_grid.IsWalkable(currentNode.x - 1, currentNode.y + yDirection) && !_grid.IsWalkable(currentNode.x - 1, currentNode.y)))
{
_forced = true;
return currentNode;
}
}
Node nextNode = _grid.GetNodeFromIndex(currentNode.x + xDirection, currentNode.y + yDirection);
if (nextNode!= null && _grid.showDebug)
UnityEngine.Debug.DrawLine(new Vector3(currentNode.x, 1, currentNode.y),
new Vector3(nextNode.x, 1, nextNode.y), Color.green, Mathf.Infinity);
return _Jump(nextNode, currentNode, xDirection, yDirection);
//设置返回跳点的cost值,并加入openset。并更新堆排序
int newGCost = currentNode.gCost + _GetDistance(currentNode, node);
if (newGCost < node.gCost || !openSetContainer.Contains(node))
{
node.gCost = newGCost;
node.hCost = _GetDistance(node, _targetNode);
node.parent = currentNode;
if (!openSetContainer.Contains(node))
{
openSetContainer.Add(node);
openSet.Add(node);
}
else
{
openSet.UpdateItem(node);
}
}
---------------------------------------------------------
//判断距离,因为cpu计算*和+速度比较快,所以在计算gCost和hCost值通过*和+采取近似值计算,格子和边的类比长度为,
10,14(边如果是1,斜边为1.41....),根据xy上不同距离,进行映射。
private int _GetDistance(Node a, Node b)
{
int distX = Mathf.Abs(a.x - b.x);
int distY = Mathf.Abs(a.y - b.y);
if (distX > distY)
return 14 * distY + 10 * (distX - distY);
return 14 * distX + 10 * (distY - distX);
}
//2018腾讯移动游戏技术评审标准与实践案例中提到的jps算法优化部分在本项目中未更新