國慶閒來沒事把NavMesh鞏固一下。以Unity3D引擎爲例寫一個底層c# NavMesh尋路。因爲Unity3D中本身自帶的NavMesh尋路不能很好的融入到遊戲項目當中,所以重寫一個NavMesh尋路是個必經之路。NavMesh在很多遊戲中應用廣泛,不同種類的框架下NavMesh尋路發揮的淋漓盡致。與傳統的A星尋路相比,NavMesh不僅減少了內存空間佔有量,加快了尋路速度,還可以加入尋路角色的寬高限制,以及動態物體尋路等功能,基本上適應了大部分項目變化多端的需求。
我把寫NavMesh的過程分成好幾個部分,一一進行描述:
一.首先要理解NavMesh核心算法。NavMesh的核心算法就是用三角形代替傳統尋路的方格,用計算拐點優化尋路路徑來代替合併路徑直線。
如下圖1NavMesh尋路:
以及如下圖2傳統的方格尋路:
看到兩者的差別了吧,NavMesh已三角形爲尋路塊,而傳統以方格爲尋路塊。其實兩者都使用A*尋路,但就是其網格生成不一樣,導致當有大範圍尋路時,其效率和要求也不一樣。
二.NavMesh尋路中的路徑優化之拐點計算。其實NavMesh中比較常用的是光照射線法,但這裏不做詳細介紹,光照射淺法詳細內容地址:http://www.cnblogs.com/neoragex2002/archive/2007/09/09/887556.html
拐點計算優化路徑就是到達目的地需要經過的一堆三角形中計算出最簡潔的移動方式。其核心算法就是從當前點到另一個三角形中的點之間的線段,與這條線段相交的線段全部是路徑所穿越的線段,就是拐點,把所有的拐點找出來,並得到一條最長的拐點,那個拐點就是最佳的拐點位置。
三.NavMesh類設計詳解(這裏只設計2D的尋路,對於3D方向的尋路,其實是可以2D尋路代替的):
1.所有類都在同一的命名空間NavMesh內 namespace NavMesh
Triangle 三角形基礎類
NavTriangle 尋路三角形類 (繼承Triangle)
Line2D 線段類
Polygon 多邊形類
Seeker 尋路主算法類
—————————————– 讓大家久等了 ————————————
在尋路前,我們需要建立MESH三角形網格,這是NAV_MESH的重點之一。
1.首先我們先要畫出一個範圍來確定我們的可行走範圍。
2.再在可行走範圍中去添加不可行走的範圍。
3.我們用多個多邊形Polygon代替以上的範圍,也就是說,一個大的可行走Polygon內包含了若干個小的不可行走的Polygon。
這是生成MESH前我們需要知道的生成範圍,然後再由這些多邊形的各個頂點來生成三角形網格,三角形網格的生成算法如下:
Step 1 : 用可行走Polygon的任意一條邊作爲起點,將其推入堆棧列表。到Step2.
Step 2: 從堆棧中推出一條邊,在所有三角形中計算出邊的DT點,構成約束Delaunay三角形,到Step3。如果沒有DT點就重複做Step2,直到堆棧爲空就結束整個程序。
Step 3: 將所構成的三角形,另兩邊做如下處理:檢查堆棧中是否已存在,如果存在就刪除該邊,如果不存在就加入到堆棧中。
生成mesh後如圖:(綠色的爲多邊形邊框,藍色的爲尋路路徑,紅色的爲編輯器選中的多邊形)
核心源碼爲:
- /// <summary>
- /// 創建導航網格
- /// </summary>
- /// <param name=”polyAll”>所有阻擋區域</param>
- /// <param name=”triAll”>輸出的導航網格</param>
- /// <returns></returns>
- public NavResCode CreateNavMesh(List<Polygon> polyAll , ref int id , int groupid , ref List<Triangle> triAll)
- {
- triAll.Clear();
- List<Line2D> allLines = new List<Line2D>(); //線段堆棧
- //Step1 保存頂點和邊
- NavResCode initRes = InitData(polyAll);
- if (initRes != NavResCode.Success)
- return initRes;
- int lastNeighborId = -1;
- Triangle lastTri = null;
- //Step2.遍歷邊界邊作爲起點
- {
- Line2D sEdge = startEdge;
- allLines.Add(sEdge);
- Line2D edge = null;
- do
- {
- //Step3.選出計算出邊的DT點,構成約束Delaunay三角形
- edge = allLines[allLines.Count – 1];
- allLines.Remove(edge);
- Vector2 dtPoint;
- bool isFindDt = FindDT(edge, out dtPoint);
- if (!isFindDt)
- continue;
- Line2D lAD = new Line2D(edge.GetStartPoint(), dtPoint);
- Line2D lDB = new Line2D(dtPoint, edge.GetEndPoint());
- //創建三角形
- Triangle delaunayTri = new Triangle(edge.GetStartPoint(), edge.GetEndPoint(), dtPoint, id++ , groupid);
- // 保存鄰居節點
- // if (lastNeighborId != -1)
- // {
- // delaunayTri.SetNeighbor(lastNeighborId);
- // if(lastTri != null)
- // lastTri.SetNeighbor(delaunayTri.ID);
- // }
- //save result triangle
- triAll.Add(delaunayTri);
- // 保存上一次的id和三角形
- lastNeighborId = delaunayTri.GetID();
- lastTri = delaunayTri;
- int lineIndex;
- //Step4.檢測剛創建的的線段ad,db;如果如果它們不是約束邊
- //並且在線段堆棧中,則將其刪除,如果不在其中,那麼將其放入
- if (!Line2D.CheckLineIn(allEdges, lAD, out lineIndex))
- {
- if (!Line2D.CheckLineIn(allLines, lAD, out lineIndex))
- allLines.Add(lAD);
- else
- allLines.RemoveAt(lineIndex);
- }
- if (!Line2D.CheckLineIn(allEdges, lDB, out lineIndex))
- {
- if (!Line2D.CheckLineIn(allLines, lDB, out lineIndex))
- allLines.Add(lDB);
- else
- allLines.RemoveAt(lineIndex);
- }
- //Step5.如果堆棧不爲空,則轉到第Step3.否則結束循環
- } while (allLines.Count > 0);
- }
- // 計算鄰接邊和每邊中點距離
- for (int i = 0; i < triAll.Count; i++)
- {
- Triangle tri = triAll[i];
- //// 計算每個三角形每邊中點距離
- //tri.calcWallDistance();
- // 計算鄰居邊
- for (int j = 0; j < triAll.Count; j++)
- {
- Triangle triNext = triAll[j];
- if (tri.GetID() == triNext.GetID())
- continue;
- int result = tri.isNeighbor(triNext);
- if (result != -1)
- {
- tri.SetNeighbor(result , triNext.GetID() );
- }
- }
- }
- return NavResCode.Success;
- }
這裏對如何計算DT點進行一個說明:
Step1. 構造三角形的外接圓,以及外接圓的包圍盒
Step2. 依次訪問網格包圍盒內的每個網格單元:
若某個網格單元中存在可見點 p, 並且 ∠p1pp2 > ∠p1p3p2,則令 p3=p,轉Step1;
否則,轉Step3.
Step3. 若當前網格包圍盒內所有網格單元都已被處理完,也即C(p1,p2,p3)內無可見點,則
p3 爲的 p1p2 的 DT 點
核心源碼爲:
- /// <summary>
- /// 找到指定邊的約束邊DT
- /// </summary>
- /// <param name=”line”></param>
- /// <returns></returns>
- private bool FindDT(Line2D line, out Vector2 dtPoint)
- {
- dtPoint = new Vector2();
- if (line == null)
- return false;
- Vector2 ptA = line.GetStartPoint();
- Vector2 ptB = line.GetEndPoint();
- List<Vector2> visiblePnts = new List<Vector2>();
- foreach (Vector2 point in allPoints)
- {
- if (IsPointVisibleOfLine(line, point))
- visiblePnts.Add(point);
- }
- if (visiblePnts.Count == 0)
- return false;
- bool bContinue = false;
- dtPoint = visiblePnts[0];
- do
- {
- bContinue = false;
- //Step1.構造三角形的外接圓,以及外接圓的包圍盒
- Circle circle = NMath.CreateCircle(ptA, ptB, dtPoint);
- Rect boundBox = NMath.GetCircleBoundBox(circle);
- //Step2. 依次訪問網格包圍盒內的每個網格單元:
- //若某個網格單元中存在可見點 p, 並且 ∠p1pp2 > ∠p1p3p2,則令 p3=p,轉Step1;
- //否則,轉Step3.
- float angOld = (float)Math.Abs(NMath.LineRadian(ptA, dtPoint, ptB));
- foreach (Vector2 pnt in visiblePnts)
- {
- if (pnt == ptA || pnt == ptB || pnt == dtPoint)
- continue;
- if (!boundBox.Contains(pnt))
- continue;
- float angNew = (float)Math.Abs(NMath.LineRadian(ptA, pnt, ptB));
- if (angNew > angOld)
- {
- dtPoint = pnt;
- bContinue = true;
- break;
- }
- }
- //false 轉Step3
- } while (bContinue);
- //Step3. 若當前網格包圍盒內所有網格單元都已被處理完,
- // 也即C(p1,p2,p3)內無可見點,則 p3 爲的 p1p2 的 DT 點
- return true;
- }
爲了讓各位能更容易讀懂此文,此文仍會繼續補充。現在我將所有源碼都存放在了Github上,請各位跟隨我到Github去取源碼:https://github.com/luzexi/Unity3DNavMesh
出處:http://www.luzexi.com