蜂巢型六邊形A星尋路演算法unity完整流程

雲影發表於2020-07-27
一、需求

我們需要一個這樣的函式:給定六邊形網格的一個起點和一個終點,函式能夠返回從起點到終點的通路經過哪些六邊形。

適應場景:戰棋類遊戲,常常因為六邊形網格更好的美術表現及遊戲性選擇蜂巢型的網格作為遊戲內尋路的基礎,而一些moba類遊戲也常常因為NaviMesh網格型尋路消耗過大、服務端客戶端網格演算法不同(例如unity內建的網格尋路功能,服務端就沒辦法使用)等原因,最終選擇A星尋路演算法來製作功能(節點需要足夠小,且尋路結果需要平滑優化)。

二、演算法講解

1. 搜尋區域

節點:就是單個六邊形,整個搜尋區域需要被節點鋪滿。

首先需要明確的一點是,正方形網格和六邊形的網格沒有本質上的區別,只不過正方形可以行走的方向是4個,六邊形是6個,這個在程式上的區別主要體現在一個節點的相鄰節點集上,正方形的相鄰節點在4個以內,六邊形的相鄰節點在6個以內,不過對於演算法理解沒有影響。

2. 開始搜尋

開啟列表:需要考慮的節點都會被放到開啟列表中,剛開始的時候開啟列表只有起點一個節點,然後根據節點的相鄰節點集,會逐漸把附近的節點都加到開啟列表中。

關閉列表:所有不在考慮的節點的集合。

蜂巢型六邊形A星尋路演算法unity完整流程
六邊形網格,紅色為不可通過點

我們以節點11為起點,節點8為終點。

先把節點11加入開啟列表,再通過節點11找到他的相鄰節點,也就是節點1、3、12、14、15、10,把他們放入開啟列表,併為他們設定父節點為節點11。將節點11放入關閉列表。

3. 估值

上面講的是如何擴大搜尋範圍,而如果想要獲得一條最優路徑,那麼必然是有一個估值方式,在A星演算法中,我們為每個節點都做估值,估算值F = G + H:

G:從起點,沿著產生的路徑,移動到網格上指定方格的移動耗費。在這裡,我們認為相鄰的六邊形,移動消耗是1,例如,從節點11移動到3,移動消耗就是1.

H:從網格上那個方格移動到終點B的預估移動耗費。這經常被稱為啟發式的,可因為它只是個猜測。這個H值的估算方式有很多種,我們暫時使用兩個節點的直線距離,對應unity的實現就是Vector3.Distance(curHex.transform.position, tarHex.transform.position)。

蜂巢型六邊形A星尋路演算法unity完整流程
F = G + H估值

很容易得到,節點3的估值是最低,估值最低則代表最優先。

4. 繼續搜尋

找到F值最低的節點3作為當前結點,我們把節點3從開啟列表中刪除,然後新增到關閉列表中。

找到節點3的所有相鄰節點,跳過那些已經在關閉列表中、不可通過的點(節點2、4)。

如果該節點已經在開啟列表中,則計算從當前節點到這個節點的G估算值為多少,例如節點12已經在開啟列表中了,從當前節點(也就是節點3)到節點12的G估算值等於節點3當前G估值 + 1(從3走到12),也就是2。如果這個G估值比原先的G估值大,則什麼都不做;如果比原來的G估值小,則把該節點的父節點改為當前節點,並將該節點的G估值設為更小的那個值。

再從開啟列表中選取F值最低的那個,設為當前節點,重複以上過程。

在圖中示例的六邊形網格中,節點3的相鄰網格有1、12、16,節點1和12已經在開啟列表中了,節點3的G估值1+從3走到12的移動消耗1 = 2,2>節點12的當前G估值,所以什麼都不做(節點1類似)。現在開啟列表中有1、10、15、14、12、16,計算F估值,節點16最小,所以再以節點16為當前節點,重複以上過程。最終得到路徑11、3、16、13、8。

三、在unity內的具體實現

1. 使用6個cube(每個cube旋轉角差距60度)拼成一個六邊形,用這些六邊形拼出來一塊網格,為方便除錯,以數字依次命名。

2. 複製以下程式碼,命名為Hexagon,附加到每個六邊形上:


  1. using System.Collections;
  2. using System.Collections.Generic;
  3. using UnityEngine;

  4. public class Hexagon : MonoBehaviour
  5. {
  6.     Hexagon father = null;
  7.     public Hexagon[] neighbors = new Hexagon[6];
  8.     public bool passFlag = true;
  9.     float gValue = 999f;
  10.     float hValue = 999f;

  11.     public Material redMat;
  12.     public Material greenMat;

  13.     void Start()
  14.     {
  15.     }

  16.     public void reset()
  17.     {

  18.     }

  19.     public Hexagon[] getNeighborList()
  20.     {
  21.         return neighbors;
  22.     }

  23.     public void setFatherHexagon(Hexagon f)
  24.     {
  25.         father = f;
  26.     }

  27.     public Hexagon getFatherHexagon()
  28.     {
  29.         return father;
  30.     }

  31.     public void setCanPass(bool f)
  32.     {
  33.         passFlag = f;


  34.     }

  35.     public bool canPass()
  36.     {
  37.         return passFlag;
  38.     }

  39.     public float computeGValue(Hexagon hex)
  40.     {
  41.         return 1f;
  42.     }

  43.     public void setgValue(float v)
  44.     {
  45.         gValue = v;
  46.     }

  47.     public float getgValue()
  48.     {
  49.         return gValue;
  50.     }

  51.     public void sethValue(float v)
  52.     {
  53.         hValue = v;
  54.     }

  55.     public float gethValue()
  56.     {
  57.         return hValue;
  58.     }

  59.     public float computeHValue(Hexagon hex)
  60.     {
  61.         return Vector3.Distance(transform.position, hex.transform.position);
  62.     }
  63. }
複製程式碼


3. 為所有六邊形新增一個空的父gameobject,命名為HexManager,為HexManager新增以下程式碼:

  1. using System.Collections;
  2. using System.Collections.Generic;
  3. using UnityEngine;
  4. using System;

  5. public class HexManager : MonoBehaviour {
  6.     Dictionary name2Hex = new Dictionary();

  7.     static List openList = new List();
  8.     static List closeList = new List();

  9.     void Start()
  10.     {
  11.         foreach (Transform child in transform)
  12.         {
  13.             name2Hex.Add(child.name, child.GetComponent());
  14.         }
  15.     }

  16.     public Hexagon GetHexByName(string i)
  17.     {
  18.         Hexagon v = new Hexagon();
  19.         name2Hex.TryGetValue(i, out v);
  20.         return v;
  21.     }
  22.     public Dictionary GetAllHex()
  23.     {
  24.         return name2Hex;
  25.     }


  26.     public static List searchRoute(Hexagon thisHexagon, Hexagon targetHexagon)
  27.     {
  28.         Hexagon nowHexagon = thisHexagon;
  29.         nowHexagon.reset();

  30.         openList.Add(nowHexagon);
  31.         bool finded = false;
  32.         while (!finded)
  33.         {
  34.             openList.Remove(nowHexagon);//將當前節點從openList中移除  
  35.             closeList.Add(nowHexagon);//將當前節點新增到關閉列表中  
  36.             Hexagon[] neighbors = nowHexagon.getNeighborList();//獲取當前六邊形的相鄰六邊形  
  37.             //print("當前相鄰節點數----" + neighbors.size());  
  38.             foreach (Hexagon neighbor in neighbors)
  39.             {
  40.                 if (neighbor == null) continue;

  41.                 if (neighbor == targetHexagon)
  42.                 {//找到目標節點  
  43.                     //System.out.println("找到目標點");  
  44.                     finded = true;
  45.                     neighbor.setFatherHexagon(nowHexagon);
  46.                 }
  47.                 if (closeList.Contains(neighbor) || !neighbor.canPass())
  48.                 {//在關閉列表裡  
  49.                     //print("無法通過或者已在關閉列表");  
  50.                     continue;
  51.                 }

  52.                 if (openList.Contains(neighbor))
  53.                 {//該節點已經在開啟列表裡  
  54.                     //print("已在開啟列表,判斷是否更改父節點");  
  55.                     float assueGValue = neighbor.computeGValue(nowHexagon) + nowHexagon.getgValue();//計算假設從當前節點進入,該節點的g估值  
  56.                     if (assueGValue < neighbor.getgValue())
  57.                     {//假設的g估值小於於原來的g估值  
  58.                         openList.Remove(neighbor);//重新排序該節點在openList的位置  
  59.                         neighbor.setgValue(assueGValue);//從新設定g估值  
  60.                         openList.Add(neighbor);//從新排序openList。
  61.                     }
  62.                 }
  63.                 else
  64.                 {//沒有在開啟列表裡  
  65.                     //print("不在開啟列表,新增");  
  66.                     neighbor.sethValue(neighbor.computeHValue(targetHexagon));//計算好他的h估值  
  67.                     neighbor.setgValue(neighbor.computeGValue(nowHexagon) + nowHexagon.getgValue());//計算該節點的g估值(到當前節點的g估值加上當前節點的g估值)  
  68.                     openList.Add(neighbor);//新增到開啟列表裡  
  69.                     neighbor.setFatherHexagon(nowHexagon);//將當前節點設定為該節點的父節點  
  70.                 }
  71.             }

  72.             if (openList.Count <= 0)
  73.             {
  74.                 //print("無法到達該目標");  
  75.                 break;
  76.             }
  77.             else
  78.             {
  79.                 nowHexagon = openList[0];//得到f估值最低的節點設定為當前節點  
  80.             }
  81.         }
  82.         openList.Clear();
  83.         closeList.Clear();

  84.         List route = new List();
  85.         if (finded)
  86.         {//找到後將路線存入路線集合  
  87.             Hexagon hex = targetHexagon;
  88.             while (hex != thisHexagon)
  89.             {
  90.                 route.Add(hex);//將節點新增到路徑列表裡

  91.                 Hexagon fatherHex = hex.getFatherHexagon();//從目標節點開始搜尋父節點就是所要的路線  
  92.                 hex = fatherHex;
  93.             }
  94.             route.Add(hex);


  95.         }
  96.         route.Reverse();
  97.         return route;
  98.         //      resetMap();  
  99.     }

  100.     //通過無阻擋尋路確定兩個六邊形的距離
  101.     public static  int GetRouteDis(Hexagon thisHexagon, Hexagon targetHexagon)
  102.     {
  103.         Hexagon nowHexagon = thisHexagon;
  104.         nowHexagon.reset();

  105.         openList.Add(nowHexagon);
  106.         bool finded = false;
  107.         while (!finded)
  108.         {
  109.             openList.Remove(nowHexagon);//將當前節點從openList中移除  
  110.             closeList.Add(nowHexagon);//將當前節點新增到關閉列表中  
  111.             Hexagon[] neighbors = nowHexagon.getNeighborList();//獲取當前六邊形的相鄰六邊形  
  112.             //print("當前相鄰節點數----" + neighbors.size());  
  113.             foreach (Hexagon neighbor in neighbors)
  114.             {
  115.                 if (neighbor == null) continue;

  116.                 if (neighbor == targetHexagon)
  117.                 {//找到目標節點  
  118.                     //System.out.println("找到目標點");  
  119.                     finded = true;
  120.                     neighbor.setFatherHexagon(nowHexagon);
  121.                 }
  122.                 if (closeList.Contains(neighbor))
  123.                 {//在關閉列表裡  
  124.                     //System.out.println("無法通過或者已在關閉列表");  
  125.                     continue;
  126.                 }

  127.                 if (openList.Contains(neighbor))
  128.                 {//該節點已經在開啟列表裡  
  129.                     //System.out.println("已在開啟列表,判斷是否更改父節點");  
  130.                     float assueGValue = neighbor.computeGValue(nowHexagon) + nowHexagon.getgValue();//計算假設從當前節點進入,該節點的g估值  
  131.                     if (assueGValue < neighbor.getgValue())
  132.                     {//假設的g估值小於於原來的g估值  
  133.                         openList.Remove(neighbor);//重新排序該節點在openList的位置  
  134.                         neighbor.setgValue(assueGValue);//從新設定g估值  
  135.                         openList.Add(neighbor);//從新排序openList。
  136.                     }
  137.                 }
  138.                 else
  139.                 {//沒有在開啟列表裡  
  140.                     //System.out.println("不在開啟列表,新增");  
  141.                     neighbor.sethValue(neighbor.computeHValue(targetHexagon));//計算好他的h估值  
  142.                     neighbor.setgValue(neighbor.computeGValue(nowHexagon) + nowHexagon.getgValue());//計算該節點的g估值(到當前節點的g估值加上當前節點的g估值)  
  143.                     openList.Add(neighbor);//新增到開啟列表裡  
  144.                     neighbor.setFatherHexagon(nowHexagon);//將當前節點設定為該節點的父節點  
  145.                 }
  146.             }

  147.             if (openList.Count <= 0)
  148.             {
  149.                 //System.out.println("無法到達該目標");  
  150.                 break;
  151.             }
  152.             else
  153.             {
  154.                 nowHexagon = openList[0];//得到f估值最低的節點設定為當前節點  
  155.             }
  156.         }
  157.         openList.Clear();
  158.         closeList.Clear();

  159.         List route = new List();
  160.         if (finded)
  161.         {//找到後將路線存入路線集合  
  162.             Hexagon hex = targetHexagon;
  163.             while (hex != thisHexagon)
  164.             {
  165.                 route.Add(hex);//將節點新增到路徑列表裡

  166.                 Hexagon fatherHex = hex.getFatherHexagon();//從目標節點開始搜尋父節點就是所要的路線  
  167.                 hex = fatherHex;
  168.             }
  169.             route.Add(hex);


  170.         }
  171.         return route.Count - 1;
  172.     }
  173. }
複製程式碼


4. 在其他地方呼叫searchRoute介面,檢查除錯。

作者:雲影
來源: Unity官方平臺
原地址:https://mp.weixin.qq.com/s/NWuD9G3ArekC0wyLnqXFJQ

相關文章