Jump Point Search(JPS)算法總結與實現(附Demo)

關於這篇文章

第一次翻閱A星算法的文章,是爲了弄清楚A星在遊戲開發中的地位。當是在腦海中沒有造成它的算法模型,也苦於沒有一個自定義軌跡且又能展現每一步的細節的demo,因而本身動手寫了一個測試demo。後來又由於效率的問題接觸到了JPS,因而又實現了一版JPS的邏輯,後分別整理成文章發佈。html

這期間大約經歷了一個月時間,這段時間有不少人給我講,這些內容網上有不少,不必費那麼大的功夫。
但~ 我以爲,作一個領域的研究要有錙銖必較的心態,這纔是作程序開發本該有的素養。
從市場的角度來想,這個行業會不斷的有新鮮血液注入,因此翻閱的需求就一直存在,可否被看到可能只是一個機率的問題。git

但~ 在其餘方面,我也收穫了不少,好比我的網站的創建,博客的編輯發佈,以及找到了適合本身寫博文的工具鏈……github

Jps,Jump Point Search,跳點搜索,也有人稱之爲「拐點尋路」。Jps可追溯到2011年,由兩位澳大利亞的教授提出,有興趣的能夠翻閱一下原做者論文,github Harabor, Daniel Damir, and Alban Grastien. "Online Graph Pruning for Pathfinding On Grid Maps." AAAI. 2011.算法

Jps在A Star算法模型的基礎之上,優化了搜索後繼節點的操做。A星的處理是把周邊能搜索到的格子,加進OpenList,而後在OpenList中彈出最小值……。JPS也是這樣的操做,但相對於A星來講,JPS操做OpenList的次數不多,它會先用一種更高效的方法來搜索須要加進OpenList的點,而後在OpenList中彈出最小值……工具

先看兩個圖來對A星和JPS的差別有個簡單的認識。gitlab

M.Time 表示操做 openset 和 closedset 的時間
G.Time 表示搜索後繼節點的時間
A*大約有 58%的時間在操做 openset 和 closedset,42%時間在搜索後繼節點
JPS 大約 14%時間在操做 openset 和 closedset,86%時間在搜索後繼節點。測試

到這裏咱們已經知道若是,JPS保留了一些A星的算法模型,因此,在理解A星算法模型的基礎之上,再來閱讀JPS的算法模型,可能會事半功倍。
若是你還不理解A星的算法模型,能夠是這翻閱如下幾個連接。
維基百科-A* search algorithm
A星尋路算法介紹-莫水千流-博客園
A Star Algorithm總結與實現
A Star算法總結與實現(附Demo)優化

概念

點的容器

尋路過程當中須要保存有效點的集合,分爲可探索點集合openList,已探索點集合closeList網站

路徑權值

同A星的概念 g爲起點通過其餘點到當前點的代價和,h爲到目標點的代價,f爲當前點的與起點終點間價值的和即f=g+h。.net

強迫鄰居

節點 x 的8個鄰居中有障礙,且 x 的父節點 p 通過x 到達 n 的距離代價比不通過 x 到達的 n 的任意路徑的距離代價小,則稱 n 是 x 的強迫鄰居。
乍一看以爲這個定義有點難理解,舉一個簡單的栗子,下面的邏輯是代碼邏輯斷定強迫linkup。
在JPS的搜索中,強迫鄰居的判斷能夠分爲兩種,一是水平搜索方向上,一是對角搜索方向上。
先介紹水平方向上,以下圖(7,10)爲起點,向右進行橫向搜索。當搜索到(9,10)時,檢測到(9,11)是障礙點,(10,11)是可行走點,所以(9,10)會被認定爲跳躍點,而(10,11)是(9,10)的強迫鄰居。
同理,(9,9)是障礙點,(10,9)是可行走點,所以(9,10)會被認爲是跳躍點,(10,9)是(9,10)的強迫鄰居。

再就是對角方向上的搜索,(7,10)是搜索起點,對右下角的(8,9)進行判斷。
(8,9)左側(7,9)是障礙點且(8,8)是可行走點的狀況下,若(7,8)是可行走點,則認爲(7,8)就是強迫鄰居。

同理,若(8,10)是障礙點,(9,9)是可行走點的狀況下,若是(9,10)是可行走點 則認爲(9,10)是強迫鄰居。

簡而言之,其實強迫鄰居的判斷就是兩種狀況,一是橫向判斷,一是對角判斷。

跳躍點

  • 若是點 y 是起點或目標點,則 y 是跳點
  • -若是 y 有鄰居且是強迫鄰居則 y 是跳點, 從上文強迫鄰居的定義來看 neighbor 是強迫鄰居,current 是跳點,兩者的關係是伴生的,-
  • 若是 parent到 y 是對角線移動,而且 y 通過水平或垂直方向移動能夠到達跳點,則 y 是跳點。

通俗一點的講就是在路徑上改變移動方向的點就是跳躍點。

JPS尋路算法的運行軌跡

以淺藍色爲起點,深藍色爲終點。有透明度的格子表明該格子被搜索過(有可能會被重複搜索),有FGH值的格子表明有跳躍點(終點會被認爲是一個特殊的跳躍點)。

起點與終點之間,無障礙狀況下

起點與終點之間,有直線障礙的狀況下

起點與終點之間有U型障礙的狀況下

A星與JPS的軌跡動圖

大體的流程應該就是這樣

A星的軌跡動圖

JPS的軌跡動圖

JPS算法裏只有跳點纔會被加入openlist裏,排除了大量沒必要要的點,最後找出來的最短路徑也是由跳點組成。這也是 JPS/JPS+ 高效的主要緣由。

僞代碼算法

橫向縱向的格子的單位消耗爲10,對角單位消耗爲14。

定義一個OpenList,用於存儲和搜索當前最小值的格子。

定義一個CloseList,用於標記已經處理過的格子,以防止重複搜索。

def 獲取鄰居點  
    if 當前點是起點  
        返回當前點九宮格內的非障礙點  
        
 elseif 當前點與父節點是對角向  
        判斷並添加相對位置右方的鄰居點
        判斷並添加相對位置下方的鄰居點
        判斷並添加相對位置對角的鄰居點
        判斷並添加相對位置左下角的強迫鄰居
        判斷並添加相對位置左上角的強迫鄰居
        
 elseif 當前點與其父節點是橫向  
        判斷並添加相對位置右方的鄰居點
        判斷並添加相對位置上方的強迫鄰居
        判斷並添加相對位置下方的強迫鄰居
            
 elseif 當前點與父節點是縱向  
        同橫向邏輯,判斷並處理下方,左右向強迫鄰居
    

def 遞歸尋找跳躍點
    if 傳入點是終點
        返回終點
    if 傳入朝向是對角向
        if 傳入點存在強迫鄰居
            返回此傳入點
        
        if (遞歸尋找跳躍點 傳入點:橫向+1 朝向:橫向)結果不爲空
            返回此傳入點
            
        if (遞歸尋找跳躍點 傳入點:縱向+1 朝向:縱向)結果不爲空
            返回此傳入點
    elseif 橫向
        if 上下方有強迫鄰居
            返回此傳入點
    
    elseif 縱向
        if 左右方有強迫鄰居
            返回此傳入點
    返回 遞歸尋找跳躍點 傳入點:橫向+1,縱向+1 朝向 對角

def Main
    起點加進OpenList中
    While(OpenList.Count > 0):
        從OpenList中取出F值最小的點並設置爲當前點
        把當前點加進CloseList
        鄰居點s = 獲取鄰居點(當前點)
        for 鄰居點s
            跳躍點 = 遞歸尋找跳躍點(鄰居點)
            if 跳躍點再也不CloseList中
                計算並設置當前點與跳躍點的G值
                計算並設置當前點與跳躍點的H值
                計算並設置跳躍點的F值
                將當前點設置爲跳躍點的父節點
        
若是鄰居點在OpenList中
    計算當前值的G與該鄰居點的G值
    若是G值比該鄰居點的G值小
        將當前點設置爲該鄰居點的父節點
        更新該鄰居點的GF值
若不在
    計算並設置當前點與該鄰居點的G值
    計算並設置當前點與該鄰居點的H值
    計算並設置該鄰居點的F值
    將當前點設置爲該鄰居點的父節

代碼相關

獲取鄰居點

public List<Point> GetNeighbors(Point point)
    {
        var points = new List<Point>();
        Point parent = point.ParentPoint;
        if (parent == null)
        {
            //獲取此點的鄰居
            //起點則parent點爲null,遍歷鄰居非障礙點加入。
            for (int x = -1; x <= 1; x++)
            {
                for (int y = -1; y <= 1; y++)
                {
                    if (x == 0 && y == 0)
                        continue;

                    if (IsWalkable(x + point.X, y + point.Y))
                    {
                        points.Add(new Point(x + point.X, y + point.Y));
                    }
                }
            }
            return points;
        }

        //非起點鄰居點判斷
        int xDirection = Mathf.Clamp(point.X - parent.X, -1, 1);
        int yDirection = Mathf.Clamp(point.Y - parent.Y, -1, 1);
        if (xDirection != 0 && yDirection != 0)
        {
            //對角方向
            bool neighbourForward =IsWalkable(point.X, point.Y + yDirection);
            bool neighbourRight =IsWalkable(point.X + xDirection, point.Y);
            bool neighbourLeft =IsWalkable(point.X - xDirection, point.Y);
            bool neighbourBack =IsWalkable(point.X, point.Y - yDirection);
            if (neighbourForward)
            {
                points.Add(new Point(point.X, point.Y + yDirection));
            }
            if (neighbourRight)
            {
                points.Add(new Point(point.X + xDirection, point.Y));
            }
            if ((neighbourForward || neighbourRight) && IsWalkable(point.X + xDirection, point.Y + yDirection))
            {
                points.Add(new Point(point.X + xDirection, point.Y + yDirection));
            }
            //強迫鄰居的處理
            if (!neighbourLeft && neighbourForward)
            {
                if (IsWalkable(point.X - xDirection, point.Y + yDirection))
                {
                    points.Add(new Point(point.X - xDirection, point.Y + yDirection));
                }
            }
            if (!neighbourBack && neighbourRight)
            {
                if (IsWalkable(point.X + xDirection, point.Y - yDirection))
                {
                    points.Add(new Point(point.X + xDirection, point.Y - yDirection));
                }
            }
        }
        else
        {
            if (xDirection == 0)
            {
                //縱向
                if (IsWalkable(point.X, point.Y + yDirection))
                {
                    points.Add(new Point(point.X, point.Y + yDirection));
                    //強迫鄰居
                    if (!IsWalkable(point.X + 1, point.Y) &&IsWalkable(point.X + 1, point.Y + yDirection))
                    {
                        points.Add(new Point(point.X + 1, point.Y + yDirection));
                    }
                    if (!IsWalkable(point.X - 1, point.Y) &&IsWalkable(point.X - 1, point.Y + yDirection))
                    {
                        points.Add(new Point(point.X - 1, point.Y + yDirection));
                    }
                }
            }
            else
            {
                //橫向
                if (IsWalkable(point.X + xDirection, point.Y))
                {
                    points.Add(new Point(point.X, point.Y + yDirection));
                    //強迫鄰居
                    if (!IsWalkable(point.X, point.Y + 1) &&IsWalkable(point.X + xDirection, point.Y + 1))
                    {
                        points.Add(new Point(point.X + xDirection, point.Y + 1));
                    }
                    if (!IsWalkable(point.X, point.Y - 1) &&IsWalkable(point.X + xDirection, point.Y - 1))
                    {
                        points.Add(new Point(point.X + xDirection, point.Y - 1));
                    }
                }
            }
        }
        return points;
    }

遞歸跳躍

private Point Jump(int curPosx, int curPosY, int xDirection, int yDirection, int depth, Point end)
    {
        if (!IsWalkable(curPosx, curPosY))
            return null;
        CallSearch(curPosx, curPosY);
        //遞歸最大深度 ||  搜索到終點
        if (depth == 0 || (end.X == curPosx && end.Y == curPosY))
            return new Point(curPosx, curPosY);

        //對角向
        if (xDirection != 0 && yDirection != 0)
        {
            if ((IsWalkable(curPosx + xDirection, curPosY - yDirection) && !IsWalkable(curPosx, curPosY - yDirection)) || (IsWalkable(curPosx - xDirection, curPosY + yDirection) && !IsWalkable(curPosx - xDirection, curPosY)))
            {
                return new Point(curPosx, curPosY);
            }
            //橫向遞歸尋找強迫鄰居
            if (Jump(curPosx + xDirection, curPosY, xDirection, 0, depth - 1, end) != null)
            {
                return new Point(curPosx, curPosY);
            }

            //縱向向遞歸尋找強迫鄰居
            if (Jump(curPosx, curPosY + yDirection, 0, yDirection, depth - 1, end) != null)
            {
                return new Point(curPosx, curPosY);
            }
        }
        else if (xDirection != 0)
        {
            //橫向
            if ((IsWalkable(curPosx + xDirection, curPosY + 1) && !IsWalkable(curPosx, curPosY + 1)) || (IsWalkable(curPosx + xDirection, curPosY - 1) && !IsWalkable(curPosx, curPosY - 1)))
            {
                return new Point(curPosx, curPosY);
            }
        }
        else if (yDirection != 0)
        {
            //縱向
            if ((IsWalkable(curPosx + 1, curPosY + yDirection) && !IsWalkable(curPosx + 1, curPosY)) || (IsWalkable(curPosx - 1, curPosY + yDirection) && !IsWalkable(curPosx - 1, curPosY)))
            {
                return new Point(curPosx, curPosY);
            }
        }
        return Jump(curPosx + xDirection, curPosY + yDirection, xDirection, yDirection, depth - 1, end);
    }

計算G,H值

protected int CalcG(Point start, Point point)
    {
        int distX = Math.Abs(point.X - start.X);
        int distY = Math.Abs(point.Y - start.Y);
        int G = 0;
        if (distX > distY)
            G = 14 * distY + 10 * (distX - distY);
        else
            G = 14 * distX + 10 * (distY - distX);

        int parentG = point.ParentPoint != null ? point.ParentPoint.G : 0;
        return G + parentG;
    }

    protected int CalcH(Point end, Point point)
    {
        int step = Math.Abs(point.X - end.X) + Math.Abs(point.Y - end.Y);
        return step * 10;
    }

傳送門

A*與JPS尋路算法的實現Demo

專業的各類尋路算法的Demo

我本身的WebGl Demo

AStarDemo的Github工程

參考與引用

JPS/JPS+ 尋路算法-KillerAery-博客園

JPS(Jump Point Search)尋路及實現代碼分析

相關文章
相關標籤/搜索