文章摘要
JPS(跳点搜索)是一种高效的路径优化算法,相比传统A*算法,它通过识别关键“跳点”和“强迫邻居”减少节点扩展,提升寻路速度。核心机制包括直线/对角线方向的跳点检测和剪枝策略。优化版本如JPS-Bit(位运算加速)和JPS+(预处理跳点)进一步将性能提升15-273倍,但JPS+不支持动态地图。适用场景包括规则网格和高频寻路(如RTS游戏),但需注意路径直线化和动态权重限制。工程实践中,结合预处理(连通性检查、位图存储)和后处理(路径平滑)能显著优化效果。JPS犹如“聪明的快递员”,仅需在必要拐点停留,大幅提升效率。
一、核心机制
1. 跳点与强迫邻居的形象理解
-
JPS的本质:
想象你在一张棋盘上走迷宫,普通A*每走一步都要“左顾右盼”,每个格子都要考虑所有方向。而JPS像是“会飞的棋子”,它会沿着一个方向一直冲,直到遇到“必须转弯”的地方才停下,这些“必须转弯”的点就是跳点。 -
强迫邻居:
假设你正朝东走,突然发现正北方向有堵墙,但东北方向却是空的,这时你就被“强迫”考虑往东北拐弯。这个“被墙逼出来的选择”就是强迫邻居。 -
跳点的三种情况:
- 起点/终点:自然是跳点。
- 遇到强迫邻居:如上例,被障碍“逼”出来的转折点。
- 对角线跳点:如果你斜着走,发现可以直接横着或竖着跳到下一个跳点,也算跳点。
跳点示意
S = 起点, G = 终点, # = 障碍, . = 空地
S . . . . . G
. # . # . .
. . . . . .
- 普通A*每格都扩展,JPS会从S直线冲到第一个“必须拐弯”的点才停。
2. 节点扩展规则(形象流程)
- 直线方向:比如一直往右走,只有遇到障碍或强迫邻居才停。
- 对角线方向:比如右上,分解为“右”和“上”两个方向,分别递归跳。
二、优化策略
优化版本 | 技术手段 | 性能提升倍数 | 支持动态阻挡 |
---|---|---|---|
JPS基础版 | 跳点剪枝 | 15x | 是 |
JPS-Bit | 位运算加速跳点检测 | 81x | 是 |
JPS-BitPrune | 剪枝中间跳点 | 110x | 是 |
JPS+ | 预处理跳点数据 | 273x | 否 |
- 形象比喻:
- JPS基础版像是“会飞的棋子”,只在关键点停。
- JPS-Bit像是“带望远镜的棋子”,用位运算一眼看清前方障碍。
- JPS+像是“提前记好所有捷径的棋子”,查表就能知道下一个跳点在哪,但地图一变就得重记。
三、性能对比
时间消耗对比
内存占用对比
算法 | 预处理内存 | 运行时内存 |
---|---|---|
A* | 0MB | 13MB |
JPS | 0MB | 28MB |
JPS+ | 150MB | 5MB |
- 形象理解:JPS+用“空间换时间”,提前把所有跳点都记下来,查表飞快,但地图一大内存就涨。
四、应用场景与限制
适用场景
- 短距离寻路:如AI追击、RTS单位集群。
- 规则网格:如2D格子地图。
- 高频寻路:如大批量单位同时寻路。
主要限制
- 不支持动态权重:不能处理不同地形消耗。
- 路径直线化:路径很“直”,不够自然,需后处理平滑。
- 超大地图需层次化:否则内存和预处理压力大。
五、工程实践
1. 预处理优化
- 连通区域预计算:提前判断起点终点是否可达,避免无效寻路。
- 跳点数据烘焙:JPS+核心,查表加速。
- 位图存储:用bitmask存储障碍,节省空间,便于位运算。
2. 路径后处理(平滑)
- Floyd算法:让路径更自然,去掉多余拐点。
- 示例代码(Python伪代码,C#同理):
optimized_path = [path[0]]
i = 0
while i < len(path) - 1:
j = i + 1
while j < len(path) and can_reach_direct(optimized_path[-1], path[j]):
j += 1
optimized_path.append(path[j-1])
i = j - 1
- 形象理解:像“拉直绳子”,能直走就不拐弯。
总结形象比喻
- JPS就像“聪明的快递员”,不在每个路口都停,而是一路飞奔到必须拐弯的地方才停下,极大减少了“问路”的次数。
- 优化策略让快递员更聪明(用望远镜、记笔记、查表),但有的策略(如JPS+)不适合经常变动的城市(动态地图)。
- 工程实践中,JPS适合“规则、变化不大、寻路频繁”的场景,配合路径平滑算法,能让AI走得又快又自然。
下面给出Jump Point Search(JPS)算法的C#完整实现,包括跳点检测(Jump Point Detection)核心代码。JPS是A*的优化变体,常用于网格寻路,极大减少了节点扩展数量。实现以4方向(上下左右)为主,如需8方向可扩展。
1. 基本数据结构
public class Node
{
public int X, Y;
public bool Walkable;
public float G, H, F;
public Node Parent;
public Node(int x, int y, bool walkable)
{
X = x; Y = y; Walkable = walkable;
}
}
2. JPS主流程
using System;
using System.Collections.Generic;
public class JPS
{
private int width, height;
private Node[,] grid;
private static readonly int[,] DIRS = { {0,1}, {1,0}, {0,-1}, {-1,0} }; // 4方向
public JPS(bool[,] map)
{
height = map.GetLength(0);
width = map.GetLength(1);
grid = new Node[height, width];
for (int y = 0; y < height; y++)
for (int x = 0; x < width; x++)
grid[y, x] = new Node(x, y, map[y, x]);
}
public List<Node> FindPath(int sx, int sy, int ex, int ey)
{
var open = new SimplePriorityQueue<Node>();
var closed = new HashSet<Node>();
Node start = grid[sy, sx], end = grid[ey, ex];
start.G = 0; start.H = Heuristic(start, end); start.F = start.H;
open.Enqueue(start, start.F);
while (open.Count > 0)
{
Node cur = open.Dequeue();
if (cur == end)
return ReconstructPath(end);
closed.Add(cur);
foreach (var dir in DIRS)
{
Node jumpNode = Jump(cur.X, cur.Y, dir[0], dir[1], ex, ey);
if (jumpNode == null || closed.Contains(jumpNode)) continue;
float ng = cur.G + Heuristic(cur, jumpNode);
if (!open.Contains(jumpNode) || ng < jumpNode.G)
{
jumpNode.G = ng;
jumpNode.H = Heuristic(jumpNode, end);
jumpNode.F = jumpNode.G + jumpNode.H;
jumpNode.Parent = cur;
if (!open.Contains(jumpNode))
open.Enqueue(jumpNode, jumpNode.F);
else
open.UpdatePriority(jumpNode, jumpNode.F);
}
}
}
return null; // no path
}
private List<Node> ReconstructPath(Node end)
{
var path = new List<Node>();
Node cur = end;
while (cur != null)
{
path.Add(cur);
cur = cur.Parent;
}
path.Reverse();
return path;
}
private float Heuristic(Node a, Node b)
{
// 曼哈顿距离
return Math.Abs(a.X - b.X) + Math.Abs(a.Y - b.Y);
}
private bool InBounds(int x, int y)
{
return x >= 0 && x < width && y >= 0 && y < height;
}
private bool Walkable(int x, int y)
{
return InBounds(x, y) && grid[y, x].Walkable;
}
3. 跳点检测(Jump Point Detection)核心
// dx, dy为方向(如(1,0)右,(0,1)下)
private Node Jump(int x, int y, int dx, int dy, int ex, int ey)
{
int nx = x + dx, ny = y + dy;
if (!Walkable(nx, ny)) return null;
if (nx == ex && ny == ey) return grid[ny, nx];
// 检查强制邻居(forced neighbor)
// 4方向下,只有直行时,若左右有障碍且旁边可走,则为跳点
if (dx != 0)
{
// 水平移动,检测上下
if ((Walkable(nx, ny + 1) && !Walkable(x, y + 1)) ||
(Walkable(nx, ny - 1) && !Walkable(x, y - 1)))
{
return grid[ny, nx];
}
}
else if (dy != 0)
{
// 垂直移动,检测左右
if ((Walkable(nx + 1, ny) && !Walkable(x + 1, y)) ||
(Walkable(nx - 1, ny) && !Walkable(x - 1, y)))
{
return grid[ny, nx];
}
}
// 递归向前跳
return Jump(nx, ny, dx, dy, ex, ey);
}
}
优先队列实现说明
JPS需要一个支持按F值优先出队的优先队列。你可以用第三方库(如 Priority Queue for C#),或者简单实现如下:
public class SimplePriorityQueue<T>
{
private List<(T item, float priority)> list = new List<(T, float)>();
public int Count => list.Count;
public void Enqueue(T item, float priority)
{
list.Add((item, priority));
}
public T Dequeue()
{
int minIdx = 0;
float minPriority = list[0].priority;
for (int i = 1; i < list.Count; i++)
{
if (list[i].priority < minPriority)
{
minPriority = list[i].priority;
minIdx = i;
}
}
var result = list[minIdx].item;
list.RemoveAt(minIdx);
return result;
}
public bool Contains(T item)
{
return list.Exists(x => EqualityComparer<T>.Default.Equals(x.item, item));
}
public void UpdatePriority(T item, float newPriority)
{
for (int i = 0; i < list.Count; i++)
{
if (EqualityComparer<T>.Default.Equals(list[i].item, item))
{
list[i] = (item, newPriority);
break;
}
}
}
}
用法举例
// 假设map为bool[,],true为可走,false为障碍
bool[,] map = new bool[5,5] {
{ true, true, true, true, true },
{ true, false, false, false, true },
{ true, true, true, true, true },
{ true, false, false, false, true },
{ true, true, true, true, true }
};
JPS jps = new JPS(map);
var path = jps.FindPath(0, 0, 4, 4);
if (path != null)
{
foreach (var node in path)
Console.WriteLine($"({node.X},{node.Y})");
}
else
{
Console.WriteLine("No path found.");
}
说明与扩展
- 该实现为4方向JPS,如需8方向(斜对角),需扩展DIRS和Jump函数的斜向逻辑。
- 跳点检测的核心思想是:遇到强制邻居或到达终点时返回跳点,否则递归前进。
- 若需更高性能,建议用二叉堆实现优先队列。
- 若需可视化或调试,可在Jump函数中打印跳点过程。