JPS寻路

强迫邻居

定义:节点X的邻居节点有障碍物,且X的父节点P经过X到达N的距离代价,比不经过X到大N的任一路径的距离代价都小,则称N是X的强迫邻居。

 

直线移动时的强迫邻居规定

下面的图中字母的表示:P表示父节点,B表示障碍,X表示当前节点,N表示强迫邻居(Forced Neighbor)

1,向右移动时,上方为障碍时,右上可走则为强迫邻居

image

2,向右移动时,下方为障碍时,右下可走则为强迫邻居

image

3,向左移动时,上方为障碍时,左上可走则为强迫邻居

image

4,向左移动时,下方为障碍时,左下可走则为强迫邻居

image

5,向上移动时,左侧为障碍时,左上可走则为强迫邻居

image

6,向上移动时,右侧为障碍时,右上可走则为强迫邻居

image

7,向下移动时,左侧为障碍时,左下可走则为强迫邻居

image

8,向下移动时,右侧为障碍时,右下可走则为强迫邻居

image

获取强迫邻居在哪个方向

/// <param name="dir">搜索方向</param>
/// <param name="pos">节点X</param>
/// <param name="forceNeighborDirs">强制邻居所在方向</param>
private bool CheckForcedNeighborsLine(Vector2Int dir, Vector2Int pos, List<Vector2Int> forceNeighborDirs)
{
    if (dir.x != 0) // 水平方向
    {
        bool ret = false;
        if (CellType.Obstacle == GetCellType(pos + new Vector2Int(0, 1)) && CellType.Walkable == GetCellType(pos + new Vector2Int(dir.x, 1))) 
        {
            //下方为障碍: 往右时, 右下可走时为强迫邻居; 往左时, 左下可走为强迫邻居
            forceNeighborDirs.Add(new Vector2Int(dir.x, 1));
            ret = true;
        }

        if (CellType.Obstacle == GetCellType(pos + new Vector2Int(0, -1)) && CellType.Walkable == GetCellType(pos + new Vector2Int(dir.x, -1)))
        {
            //上方为障碍: 往右时, 右上可走为强迫邻居; 往左时, 左上可走为强迫邻居
            forceNeighborDirs.Add(new Vector2Int(dir.x, -1));
            ret = true;
        }
        return ret;
    }
    else // 垂直方向
    {
        bool ret = false;
        if ((CellType.Obstacle == GetCellType(pos + new Vector2Int(1, 0)) && CellType.Walkable == GetCellType(pos + new Vector2Int(1, dir.y))))
        {
            //右侧为障碍: 往上时, 右上可走为强迫邻居; 往下时, 右下可走为强迫邻居
            forceNeighborDirs.Add(new Vector2Int(1, dir.y));
            ret = true;
        }

        if (CellType.Obstacle == GetCellType(pos + new Vector2Int(-1, 0)) && CellType.Walkable == GetCellType(pos + new Vector2Int(-1, dir.y)))
        {
            //左侧为障碍: 往上时, 左上可走为强迫邻居; 往下时, 左下可走为强迫邻居
            forceNeighborDirs.Add(new Vector2Int(-1, dir.y));
            ret = true;
        }
        return ret;
    }
}

 

斜向移动时强迫邻居的规定

1,右上移动时,左侧为障碍时,左上可走则为强迫邻居

image

2,右上移动时,下侧为障碍时,右下可走则为强迫邻居

image

3,右下移动时,左侧为障碍,左下可走则为强迫邻居

image

4,右下移动时,上侧为障碍,右上可走则为强迫邻居

image

5,左下移动时,上侧为障碍,左上可走则为强迫邻居

image

6,左下移动时,右侧为障碍,右下可走则为强迫邻居

 image

 7,左上移动时,右侧为障碍,右上可走则为强迫邻居

image

8,左上移动时,下侧为障碍,左下可走则为强迫邻居

image

获取强迫邻居在哪个方向

private bool CheckForcedNeighborsDiagonal(Vector2Int dir, Vector2Int pos, List<Vector2Int> forceNeighborDirs)
{
    bool ret = false;
    if (CellType.Obstacle == GetCellType(pos + new Vector2Int(-dir.x, 0)) && CellType.Walkable == GetCellType(pos + new Vector2Int(-dir.x, dir.y)))
    {
        forceNeighborDirs.Add(new Vector2Int(-dir.x, dir.y));
        ret = true;
    }

    if (CellType.Obstacle == GetCellType(pos + new Vector2Int(0, -dir.y)) && CellType.Walkable == GetCellType(pos + new Vector2Int(dir.x, -dir.y)))
    {
        forceNeighborDirs.Add(new Vector2Int(dir.x, -dir.y));
        ret = true;
    }
    return ret;
}

 

没有强迫邻居的情况示例

2,3为可走节点,但为啥他们不能成为强迫邻居?

  2可以直接由P斜向到达,不需要经过X

  3可以通过P>2>3到达,也存在不经过X的路径;他和P>X>3这条路径是等价的,而JPS的核心思想就是排除冗余,避免评估等价路径,所以这种会出现等价路径的情况在JPS中是直接排除掉的。

image

 


跳点(Jump Point)

什么样的节点可以作为跳点?

  (1) 起点、终点是跳点

  (2) 节点A至少有一个强迫邻居时是跳点

  (3) 父节点在斜方向(斜向搜索),节点A的水平或者垂直方向上有起点或终点、或有强迫邻居的节点。

下图中A的父节点P在斜方向,他的水平方向上节点2有强迫邻居N,所以A是跳点;节点2有一个强迫邻居,所以也是跳点。

image

 

跳点搜索规则

(1) 起点要在8个方向上搜索,每个方向在遇到跳点,障碍或超出边界时结束

(2) 后续从某个跳点继续搜索,但不是8个方向都要搜索,根据父节点方向决定在哪些方向上搜索跳点,有强制邻居时还要对强制邻居方向进行搜索。

  (2-1) 如果父节点在斜方向(红色箭头),则搜索方向为:(pdir.x, 0), (0, pdir.y), (pdir.x, pdir.y)这3个方向(蓝色箭头)

  image

  (2-2) 如果父节点在直线方向(红色箭头),则搜索方向为:(pdir.x, 0), 因为A有强制邻居,所以还要加入强制邻居所在方向(有几个强制邻居就要加几个)

  image

 

/// <summary>
/// 在水平或垂直方向上搜索跳点 
/// </summary>
/// <param name="pos">从该点开始搜索</param>
/// <param name="dir">搜索方向</param>
/// <param name="jumpPoint">搜索到的跳点</param>
/// <param name="forceNeighborDirs">有强迫邻居的节点作为跳点时, 存放强制邻居方向</param>
private bool FindJumpPointLine(Vector2Int pos, Vector2Int dir, out Vector2Int jumpPoint, List<Vector2Int> forceNeighborDirs)
{
    Vector2Int tempPos = pos;
    while (true) //不断沿dir方向搜索
    {
        tempPos += dir;

        if (CellType.Walkable != GetCellType(tempPos)) //超出边界或遇到障碍
        {
            jumpPoint = Vector2Int.zero;
            return false;
        }

        if (tempPos == m_EndPos) // 如果是终点, 则为跳点
        {
            jumpPoint = tempPos;
            return true;
        }

        if (CheckForcedNeighborsLine(dir, tempPos, forceNeighborDirs)) // 如果该节点有强迫邻居, 则为跳点
        {
            jumpPoint = tempPos;
            return true;
        }
    }
}

 

// 斜向搜索跳点
private bool FindJumpPointDiagonal(Vector2Int pos, Vector2Int dir, out Vector2Int jumpPoint, List<Vector2Int> forceNeighborDirs)
{
    Vector2Int tempPos = pos;
    while (true) //不断沿dir方向搜索
    {
        tempPos += dir;

        if (CellType.Walkable != GetCellType(tempPos)) //超出边界或遇到障碍
        {
            jumpPoint = Vector2Int.zero;
            return false;
        }

        if (tempPos == m_EndPos) // 如果是终点, 则为跳点
        {
            jumpPoint = tempPos;
            return true;
        }

        if (CheckForcedNeighborsDiagonal(dir, tempPos, forceNeighborDirs)) // 如果该节点有强迫邻居, 则为跳点
        {
            jumpPoint = tempPos;
            return true;
        }

        if (FindJumpPointLine(tempPos, new Vector2Int(dir.x, 0), out var pt, null)) //斜向搜索时, 水平方向有跳点时, 自己则为跳点
        {
            jumpPoint = tempPos;
            return true;
        }

        if (FindJumpPointLine(tempPos, new Vector2Int(0, dir.y), out pt, null)) //斜向搜索时, 垂直方向有跳点时, 自己则为跳点
        {
            jumpPoint = tempPos;
            return true;
        }
    }
}

 


整体流程

和AStar有点类似

1) 从起点开始,作为跳点加入OpenList队列

2) 然后不断的找跳点,加入OpenList队列,每次从OpenList找一个代价最小的跳点继续找跳点

3) 直到找到终点或OpenList队列为空

 

public class JPSPathFinder
{
    public char[,] m_MapData;

    private Vector2Int m_EndPos;
    private PriorityQueue<Node> m_OpenList = new PriorityQueue<Node>();
    private HashSet<Vector2Int> m_ClosedList = new HashSet<Vector2Int>();
    private List<Vector2Int> m_SearchDirs = new List<Vector2Int>();
    private List<Vector2Int> m_ForcedNeighborDirs = new List<Vector2Int>();
    private List<Vector2Int> m_Path = new List<Vector2Int>();

    // 核心寻路方法
    public bool FindPath(Vector2Int startPos, Vector2Int endPos)
    {
        Debug.Log($"FindPath: start:{startPos}, end:{endPos}");
        m_EndPos = endPos;

        m_Path.Clear();

        if (startPos == endPos)
        {
            m_Path.Add(startPos);
            return true;
        }

        if (CellType.Walkable != GetCellType(startPos) || CellType.Walkable != GetCellType(endPos))
        {
            Debug.Log($"start or end is not walkable");
            return false;
        }

        var curNode = new Node(startPos);
        curNode.g = 0;
        curNode.f = CalcH(startPos, endPos);

        //起点要在8个方向上搜索
        m_SearchDirs.Add(Vector2Int.up);
        m_SearchDirs.Add(Vector2Int.right);
        m_SearchDirs.Add(Vector2Int.down);
        m_SearchDirs.Add(Vector2Int.left);
        m_SearchDirs.Add(new Vector2Int(1, 1)); // 右上
        m_SearchDirs.Add(new Vector2Int(1, -1)); // 右下
        m_SearchDirs.Add(new Vector2Int(-1, 1)); // 左上
        m_SearchDirs.Add(new Vector2Int(-1, -1)); // 左下

        do
        {
            Debug.Log($"best: {curNode.pos}");
            m_ClosedList.Add(curNode.pos);

            foreach (var searchDir in m_SearchDirs)
            {
                Vector2Int jumpPoint = Vector2Int.zero;
                if (searchDir.x * searchDir.y == 0)
                {
                    if (!FindJumpPointLine(curNode.pos, searchDir, out jumpPoint, m_ForcedNeighborDirs))
                    {
                        m_ForcedNeighborDirs.Clear();
                        continue;
                    }
                }
                else
                {
                    if (!FindJumpPointDiagonal(curNode.pos, searchDir, out jumpPoint, m_ForcedNeighborDirs))
                    {
                        m_ForcedNeighborDirs.Clear();
                        continue;
                    }
                }

                if (m_ClosedList.Contains(jumpPoint))
                {
                    m_ForcedNeighborDirs.Clear();
                    continue;
                }
                Debug.Log($"{curNode.pos} -> 在方向{searchDir}找到跳点 -> {jumpPoint}");

                Node successor = new Node(jumpPoint);
                int gCost = curNode.g + CalcG(curNode.pos, jumpPoint);
                successor.g = gCost;
                int hCost = CalcH(jumpPoint, endPos);
                successor.f = gCost + hCost;
                successor.parent = curNode;
                successor.forcedNeighborDirs.AddRange(m_ForcedNeighborDirs);                m_OpenList.Enqueue(successor);
                m_ForcedNeighborDirs.Clear();
            }
            m_SearchDirs.Clear();

            if (m_OpenList.Count <= 0)
                break;

            curNode = m_OpenList.Dequeue();
            if (curNode.pos == endPos) // 如果找到终点,则返回路径
            {
                RetracePath(curNode);
                FindSuccCleanup();
                return true;
            }

            UpdateSearchDirs(curNode, m_SearchDirs);
        } while (true);

        FindFailCleanup();
        return false;
    }

    private void FindFailCleanup()
    {
        m_ClosedList.Clear();
    }

    private void FindSuccCleanup()
    {
        m_OpenList.Clear();
        m_ClosedList.Clear();
    }

    //根据父节点以及是否有强制邻居, 确定要搜索的方向
    private void UpdateSearchDirs(Node node, List<Vector2Int> searchDirs)
    {
        var dir = node.pos - node.parent.pos; //上一个位置到这个位置的方向
        dir.x = Mathf.Clamp(dir.x, -1, 1);
        dir.y = Mathf.Clamp(dir.y, -1, 1);

        if (dir.x != 0 && dir.y != 0) //父节点在斜方向
        {
            searchDirs.Add(new Vector2Int(dir.x, 0));
            searchDirs.Add(new Vector2Int(0, dir.y));
            searchDirs.Add(dir);

            if (node.forcedNeighborDirs.Count > 0) //有强制邻居时, 强制邻居所在的方向也要搜索
            {
                foreach (var item in node.forcedNeighborDirs)
                {
                    if (item != dir)
                        searchDir.Add(item);
                }
            }
        }
        else
        {
            if (dir.x != 0) //父节点在水平方向
            {
                searchDirs.Add(new Vector2Int(dir.x, 0));
            }
            else if (dir.y != 0) //父节点在垂直方向
            {
                searchDirs.Add(new Vector2Int(0, dir.y));
            }

            if (node.forcedNeighborDirs.Count > 0) //有强制邻居时, 强制邻居所在的方向也要搜索
            {
                searchDirs.AddRange(node.forcedNeighborDirs);
            }
        }
    }

    //计算实际代价, 八进制距离优化版 -斜向和直线移动代价不同时
    private int CalcG(Vector2Int a, Vector2Int b)
    {
        int dx = Mathf.Abs(a.x - b.x);
        int dy = Mathf.Abs(a.y - b.y);
        if (dx == dy)
            return dx * 14;
        elsereturn Mathf.Min(dx, dy) * 14 + Mathf.Abs(dx - dy) * 10;
    }

    // 计算启发代价
    private int CalcH(Vector2Int a, Vector2Int b)
    {
        return CalcG(a, b); //斜向和直线移动代价不同, 所以使用和G一样的计算模型
    }

    // 回溯路径
    private void RetracePath(Node node)
    {
        while (node != null)
        {
            m_Path.Add(node.pos);
            node = node.parent;
        }
        m_Path.Reverse();
    }

    //...

}

 

计算代价的几种方式

// 计算实际代价, 八进制距离标准版 - 斜向和直线移动代价不同时
private float CalcG(Vector2Int a, Vector2Int b)
{
    int dx = Mathf.Abs(a.x - b.x);
    int dy = Mathf.Abs(a.y - b.y);
    const float SQRT2 = 1.41421356f;
    return dx + dy + (SQRT2 - 2) * Mathf.Min(dx, dy);
}

 

// 计算实际代价, 欧几里得距离
private float CalcG(Vector2Int a, Vector2Int b)
{
    return Vector2Int.Distance(curNode.pos, jumpPoint);
}

 

// 计算启发代价, 曼哈顿距离 - 斜向和直线移动代价相同时
private int CalcH(Vector2Int a, Vector2Int b)
{
    return Mathf.Abs(a.x - b.x) + Mathf.Abs(a.y - b.y);
}

 

 

地图数据相关代码

public enum CellType
{
    Walkable = 0,
    Obstacle,
    Outside,
}

// 判断节点类型
private CellType GetCellType(Vector2Int pos)
{
    if (pos.y < 0 || pos.y >= m_MapData.GetLength(0))
        return CellType.Outside; //超出边界
    if (pos.x < 0 || pos.x >= m_MapData.GetLength(1))
        return CellType.Outside; //超出边界

    char c = m_MapData[pos.y, pos.x];
    if (c == '1')
        return CellType.Obstacle;
    return CellType.Walkable;
}

 

 节点类

public class Node
{
    public Vector2Int pos; // 节点位置
    public int g; // g: 起点到当前节点的实际代价, h: 当前节点到终点的估计代价
    public int f; // 总代价(G + H)
    public Node parent;
    public List<Vector2Int> forcedNeighborDirs = new List<Vector2Int>(); //强制邻居

    public Node(Vector2Int pos)
    {
        this.pos = pos;
    }
}

 

用列表模拟的优先队列,生产环境一定要改成用堆(Heap)实现

public class PriorityQueue<T> where T : JPSPathFinder.Node
{
    private readonly List<T> elements = new List<T>();

    public int Count => elements.Count;

    public void Enqueue(T item)
    {
        elements.Add(item);
        elements.Sort((a, b) => a.f.CompareTo(b.f)); // 按F值排序
    }

    public T Dequeue()
    {
        T item = elements[0];
        elements.RemoveAt(0);
        return item;
    }

    public void Clear()
    {
        elements.Clear();
    }

}

 


使用示例

public class JpsPathFindMain : MonoBehaviour
{

    private static void ReadMapData(out char[,] mapData, out Vector2Int start, out Vector2Int end)
    {
        start = Vector2Int.zero;
        end = Vector2Int.zero;

        var mapDataAsset = AssetDatabase.LoadAssetAtPath<TextAsset>("Assets/Jps/MapData.txt");
        StringReader sr = new StringReader(mapDataAsset.text);
        string line = null;
        var lineDatas = new List<char[]>();
        int cols = 0;
        int y = 0;
        int lineNum = 0;
        while ((line = sr.ReadLine()) != null)
        {
            lineNum++;
            if (line == "" || line.StartsWith('#')) continue;

            var splitArr = line.Split(',');
            if (cols == 0)
            {
                cols = splitArr.Length;
            }
            else if (cols != splitArr.Length)
            {
                throw new Exception($"数据列数不一致: lineNum: {lineNum}, cols: {cols}");
            }

            var lineData = new char[splitArr.Length];
            lineDatas.Add(lineData);

            int x = 0;
            foreach (var item in splitArr)
            {
                if (item == "s")
                {
                    start = new Vector2Int(x, y);
                }
                else if (item == "e")
                {
                    end = new Vector2Int(x, y);
                }
                lineData[x++] = item[0];
            }

            y++;
        }

        mapData = new char[lineDatas.Count, cols];
        for (y = 0; y < mapData.GetLength(0); y++)
        {
            for (int x = 0; x < mapData.GetLength(1); x++)
            {
                mapData[y, x] = lineDatas[y][x];
            }
        }
    }

    void Start()
    {
        var jpsPathFind = new JPSPathFinder();
        ReadMapData(out var mapData, out var start, out var end);
        jpsPathFind.m_MapData = mapData;

        // 执行JPS寻路
        if (jpsPathFind.FindPath(start, end))
        {
            Debug.Log("Path found:");
            var path = jpsPathFind.Path;
            foreach (var node in path)
            {
                //Debug.Log(node);
                char c = mapData[node.y, node.x];
                if (c == 's' || c == 'e')
                {
                    //忽略
                }
                else
                {
                    mapData[node.y, node.x] = '*';
                }
            }

            var sb = new StringBuilder();
            sb.Append("\n");
            for (int y = 0; y < mapData.GetLength(0); ++y)
            {
                for (int x = 0; x < mapData.GetLength(1); ++x)
                {
                    if (x > 0)
                        sb.Append(",");
                    sb.Append($"{mapData[y, x]}");
                }
                sb.Append("\n");
            }
            Debug.Log($"{sb.ToString()}");
        }
        else
        {
            Debug.Log("No path found.");
        }
    }

}

 


参考

JPS(jump point search)寻路算法_jps算法-CSDN博客

 【Reality文章投稿——《JPS算法概述》】 - 哔哩哔哩JPS算法:让寻路速度提升百倍-CSDN博客

游戏开发技术杂谈8:JPS寻路算法 - 知乎

JPS寻路可视化

 

JPS/JPS+ 寻路算法 - KillerAery - 博客园

JPS算法:让寻路速度提升百倍-CSDN博客,对比了几种jps变种

由浅入深:无伤理解JPS寻路算法及代码实现 - 知乎

 A* JPS寻路算法的探讨-腾讯云开发者社区-腾讯云 AStar

实战|JPS跳点寻路实现运行路径规划-腾讯云开发者社区-腾讯云 

JPS(Jump Point Search)寻路及实现代码分析_jps 寻路-CSDN博客 4方向

 

posted @ 2026-06-19 23:54  yanghui01  阅读(4)  评论(0)    收藏  举报