提交完美

This commit is contained in:
PC-20230316NUNE\Administrator
2024-10-17 20:36:24 +08:00
parent 0d600a2786
commit b0a2e4a900
1522 changed files with 40000 additions and 15615 deletions

View File

@@ -0,0 +1,16 @@
{
"name": "JNGame.Root",
"rootNamespace": "",
"references": [
"GUID:8c4dd21966739024fbd72155091d199e"
],
"includePlatforms": [],
"excludePlatforms": [],
"allowUnsafeCode": false,
"overrideReferences": false,
"precompiledReferences": [],
"autoReferenced": true,
"defineConstraints": [],
"versionDefines": [],
"noEngineReferences": false
}

View File

@@ -0,0 +1,7 @@
fileFormatVersion: 2
guid: a035c483e4ff50f4c92f84afd22778cf
AssemblyDefinitionImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -0,0 +1,7 @@
namespace JNGame.Root
{
public class JNGameRoot
{
}
}

View File

@@ -0,0 +1,3 @@
fileFormatVersion: 2
guid: 79eb859ad36942599009f35808d9cd2d
timeCreated: 1729156991

View File

@@ -0,0 +1,3 @@
fileFormatVersion: 2
guid: 2506063f96674267bf87277fdf1884ff
timeCreated: 1715151805

View File

@@ -0,0 +1,3 @@
fileFormatVersion: 2
guid: 414fc775b19c49b987762f028d6b815a
timeCreated: 1715152183

View File

@@ -0,0 +1,8 @@
fileFormatVersion: 2
guid: 34971619b0c1f46b4b29e418d346d483
folderAsset: yes
DefaultImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -0,0 +1,131 @@
// 定义BspTree的Unity可视化扩展
//#define SHOW_BSP_TREE_GIZOMS
#if UNITY_EDITOR // && SHOW_BSP_TREE_GIZOMS
using System.Collections.Generic;
using JNGame.Math;
using UnityEngine;
namespace JNGame.PathFinding
{
// BspTreeNode的调试信息
public partial class BspTreeNode
{
public static int s_AutoIncretID = 0;
public int debugNodeId;
public BspTreeNode()
{
debugNodeId = ++s_AutoIncretID;
}
}
// BspTree的可视化绘制
public partial class BspTree
{
#region Debug Gizoms
public static Transform s_DebugTrans;
public static Material s_DebugMat;
public void DebugDraw()
{
var tran = DrawNode(m_RootNode, 0, s_DebugTrans);
tran.transform.position += Vector3.up * 0.05f;
}
private Vector3 Hash13(int id)
{
var val = (Mathf.Sin(15741.254f * id) + 1) / 2;
var val2 = (Mathf.Sin(7331.5147f * id) + 1) / 2;
var val3 = (Mathf.Sin(24317.433f * id) + 1) / 2;
return new Vector3(val, val2, val3);
}
private Transform DrawNode(BspTreeNode node, int depth, Transform parent)
{
var val = Hash13(node.debugNodeId);
Color color = new Color(val.x, val.y, val.z, 1);
if (node.IsLeaf)
{
var tran = CreateGo(node.debugNodeId.ToString(), node.tris, color).transform;
tran.SetParent(parent, true);
return tran;
}
else
{
var tran = CreateGo(node.debugNodeId.ToString(), node.splitLine, color).transform;
tran.SetParent(parent, true);
DrawNode(node.LeftChild, depth + 1, tran);
DrawNode(node.RightChild, depth + 1, tran);
return tran;
}
}
private GameObject CreateGo(string name, in SplitLine plane, in Color color)
{
return new GameObject(name);
#if false
var dir = plane.Direction.Normalized;
var or = plane.vertexA;
var s = or - dir * worldSize;
var e = or + dir * worldSize;
var perp = new LVector2(-dir.y, dir.x);
const float lineSize = 0.3f;
var s1 = perp * lineSize.ToLFloat() + s;
var e1 = perp * lineSize.ToLFloat() + e;
List<Vector3> vertices = new List<Vector3>();
List<int> tirs = new List<int>();
vertices.Add(s.ToVector3XZ());
vertices.Add(e.ToVector3XZ());
vertices.Add(e1.ToVector3XZ());
vertices.Add(s1.ToVector3XZ());
int triIdx = 0;
tirs.Add(0);
tirs.Add(1);
tirs.Add(2);
tirs.Add(0);
tirs.Add(2);
tirs.Add(3);
return CreateGo(name, vertices, tirs, color);
#endif
}
private GameObject CreateGo(string name, List<TriangleRef> allRawTriangle, in Color color)
{
List<Vector3> vertices = new List<Vector3>();
List<int> tirs = new List<int>();
int triIdx = 0;
foreach (var tri in allRawTriangle)
{
vertices.Add(tri.vertexA.ToVector3XZ());
vertices.Add(tri.vertexB.ToVector3XZ());
vertices.Add(tri.vertexC.ToVector3XZ());
tirs.Add(triIdx++);
tirs.Add(triIdx++);
tirs.Add(triIdx++);
}
return CreateGo(name, vertices, tirs, color);
}
private GameObject CreateGo(string name, List<Vector3> vertices, List<int> tirs, in Color color)
{
var go = new GameObject(name);
var render = go.AddComponent<MeshRenderer>();
var mat = new Material(s_DebugMat) { color = color };
render.material = mat;
var tempMesh = new Mesh();
tempMesh.vertices = vertices.ToArray();
tempMesh.triangles = tirs.ToArray();
go.AddComponent<MeshFilter>().mesh = tempMesh;
return go;
}
#endregion
}
}
#endif

View File

@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: ba3cb62d3cc64ae4a94be3cbd1bf6ef8
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -0,0 +1,63 @@
using System.Collections.Generic;
using JNGame.Math;
namespace JNGame.PathFinding
{
/// <summary>
/// 二叉空间分割树
/// </summary>
public partial class BspTree
{
private readonly List<TriangleRef> m_AllTriangle = new List<TriangleRef>();
/// <summary>
/// 根节点
/// </summary>
private BspTreeNode m_RootNode;
/// <summary>
/// 查找点邻接三角形的缓存,每次查找都会清理,外部不可缓存此列表,只可一次性使用
/// </summary>
private readonly List<int> m_TempNeareastTrianges = new List<int>(16);
/// <summary>
/// 构造二叉空间分割树
/// </summary>
/// <param name="rawTriangles">原始的三角形数据</param>
public void Init(List<NavTriangle> rawTriangles)
{
foreach (var tri in rawTriangles)
{
m_AllTriangle.Add(new TriangleRef(tri));
}
m_RootNode = new BspTreeNode();
m_RootNode.Init(m_AllTriangle);
}
/// <summary>
/// 根据位置检索BSP树快速定位点所在三角形区域
/// </summary>
/// <param name="pos"></param>
/// <param name="nearTriangleIndices"></param>
/// <returns></returns>
public int GetTriangle(in LVector3 pos, out List<int> nearTriangleIndices)
{
m_TempNeareastTrianges.Clear();
var pos2d = pos.ToLVector2XZ();
int triangleIndex = m_RootNode.GetTriangle(pos2d, out _, out List<TriangleRef> nearTriangles);
if (triangleIndex == -1)
{ // 未找到落位的三角形,说明在阻挡内,获取阻挡周边的一些三角形
for (int i = 0; i < nearTriangles.Count; ++i)
{
if (!m_TempNeareastTrianges.Contains(nearTriangles[i].index))
{
m_TempNeareastTrianges.Add(nearTriangles[i].index);
}
}
}
nearTriangleIndices = m_TempNeareastTrianges;
return triangleIndex;
}
}
}

View File

@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 477985406605c4a0c9d9718247233d1d
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -0,0 +1,315 @@
using System.Collections.Generic;
using JNGame.Math;
namespace JNGame.PathFinding
{
/// <summary>
/// 二叉空间分割树节点
/// </summary>
public partial class BspTreeNode
{
/// <summary>
/// 划分到三角形数小于此则停止,可以性能测试下,选择一个合适的值
/// </summary>
private const int kMinTriangleCount = 3;
/// <summary>
/// 找到树叶节点,向上取三层,获取包含的三角形,用于查找落位阻挡点的临近三角形
/// </summary>
private const int kNearestDeepSearch = 3;
/// <summary>
/// 当前节点中包含的三角形数据
/// </summary>
public List<TriangleRef> tris;
/// <summary>
/// 当前节点的子节点
/// </summary>
private BspTreeNode[] m_Children;
/// <summary>
/// 当前节点的树深
/// </summary>
public int depth = 0;
/// <summary>
/// 节点的切割线
/// </summary>
public SplitLine splitLine;
/// <summary>
/// 是否是叶子节点
/// </summary>
public bool IsLeaf => m_Children == null;
/// <summary>
/// 左子树
/// </summary>
public BspTreeNode LeftChild => m_Children?[0];
/// <summary>
/// 右子树
/// </summary>
public BspTreeNode RightChild => m_Children?[1];
/// <summary>
/// 获取指定坐标所在的三角形索引
/// </summary>
/// <param name="pos"></param>
/// <returns></returns>
public int GetTriangle(in LVector2 pos, out int searchDeep, out List<TriangleRef> triangeGroup)
{
if (IsLeaf)
{
searchDeep = -1;
triangeGroup = null;
foreach (var tri in tris)
{
if (tri.Contain(pos))
{
return tri.index;
}
}
searchDeep = kNearestDeepSearch;
return -1;
}
else
{
var isLeft = LVector2.Cross(splitLine.vertexB - splitLine.vertexA, pos - splitLine.vertexA) >= 0;
if (isLeft)
{
int targetIndex = LeftChild.GetTriangle(pos, out searchDeep, out triangeGroup);
if (targetIndex == -1)
{
--searchDeep;
if (searchDeep == 0)
{
triangeGroup = tris;
}
}
return targetIndex;
}
else
{
int targetIndex = RightChild.GetTriangle(pos, out searchDeep, out triangeGroup);
if (targetIndex == -1)
{
--searchDeep;
if (searchDeep == 0)
{
triangeGroup = tris;
}
}
return targetIndex;
}
}
}
/// <summary>
/// 初始化二叉空间树的节点
/// </summary>
/// <param name="tris"></param>
/// <param name="depth"></param>
public void Init(List<TriangleRef> tris, int depth = 0)
{
this.depth = depth;
this.tris = tris;
if (tris.Count <= kMinTriangleCount)
{ // 三角形数少于指标,停止
return;
}
if (depth > 20)
{ // 树深超过20层停止
return;
}
// 选择分割线
splitLine = PickSplitLine(tris);
m_Children = new BspTreeNode[] { new BspTreeNode(), new BspTreeNode() };
var lTris = new List<TriangleRef>();
var rTris = new List<TriangleRef>();
foreach (var tri in tris)
{
var val = SplitLine.GetSplitResult(in splitLine, tri);
if (val == EnumSplitType.Left)
{ // 三角形位于切割线左侧,加入左子树三角形列表
lTris.Add(tri);
}
else if (val == EnumSplitType.Right)
{ // 三角形位于切割线右侧,加入右子树三角形列表
rTris.Add(tri);
}
else
{ // 切割线经过三角形,将三角形分为几个多边形
SplitTriangle(lTris, rTris, tri);
}
}
LeftChild.Init(lTris, depth + 1);
RightChild.Init(rTris, depth + 1);
}
private readonly List<LVector2> m_RightVerts = new List<LVector2>();
private readonly List<LVector2> m_LeftVerts = new List<LVector2>();
/// <summary>
/// 将三角形按切割线切割为若干个三角形
/// </summary>
/// <param name="lTris"></param>
/// <param name="rTris"></param>
/// <param name="tri"></param>
private void SplitTriangle(List<TriangleRef> lTris, List<TriangleRef> rTris, TriangleRef tri)
{
int numVerts = 3;
m_RightVerts.Clear();
m_LeftVerts.Clear();
var a = tri[2];
var aSide = SplitLine.ClassifyPointToPlane(splitLine, a);
for (int i = 0; i < numVerts; i++)
{
var b = tri[i];
var bSide = SplitLine.ClassifyPointToPlane(splitLine, b);
if (bSide == EnumSplitType.Right)
{
if (aSide == EnumSplitType.Left)
{
var p = SplitLine.GetIntersectPoint(a, b, splitLine.vertexA, splitLine.vertexB);
m_RightVerts.Add(p);
m_LeftVerts.Add(p);
}
else if (aSide == EnumSplitType.OnPlane)
{
if (m_RightVerts.Count == 0 || a != m_RightVerts[m_RightVerts.Count - 1])
{
m_RightVerts.Add(a);
}
}
m_RightVerts.Add(b);
}
else if (bSide == EnumSplitType.Left)
{
if (aSide == EnumSplitType.Right)
{
var p = SplitLine.GetIntersectPoint(a, b, splitLine.vertexA, splitLine.vertexB);
m_RightVerts.Add(p);
m_LeftVerts.Add(p);
}
else if (aSide == EnumSplitType.OnPlane)
{
if (m_LeftVerts.Count == 0 || a != m_LeftVerts[m_LeftVerts.Count - 1])
{
m_LeftVerts.Add(a);
}
}
m_LeftVerts.Add(b);
}
else
{
if (aSide == EnumSplitType.Right)
{
if (!(m_RightVerts.Count == 3 && b == m_RightVerts[0]))
{
m_RightVerts.Add(b);
}
}
else if (aSide == EnumSplitType.Left)
{
if (!(m_LeftVerts.Count == 3 && b == m_LeftVerts[0]))
{
m_LeftVerts.Add(b);
}
}
}
a = b;
aSide = bSide;
}
//push into tri list
if (m_LeftVerts.Count >= 3)
{
if (m_LeftVerts.Count == 3)
{
AddTriangle(lTris, m_LeftVerts[0], m_LeftVerts[1], m_LeftVerts[2], tri);
}
else
{
// Debug.Assert(m_LeftVerts.Count == 4);
AddTriangle(lTris, m_LeftVerts[0], m_LeftVerts[1], m_LeftVerts[2], tri);
AddTriangle(lTris, m_LeftVerts[0], m_LeftVerts[2], m_LeftVerts[3], tri);
}
}
if (m_RightVerts.Count >= 3)
{
if (m_RightVerts.Count == 3)
{
AddTriangle(rTris, m_RightVerts[0], m_RightVerts[1], m_RightVerts[2], tri);
}
else
{
// Debug.Assert(m_RightVerts.Count == 4);
AddTriangle(rTris, m_RightVerts[0], m_RightVerts[1], m_RightVerts[2], tri);
AddTriangle(rTris, m_RightVerts[0], m_RightVerts[2], m_RightVerts[3], tri);
}
}
}
private void AddTriangle(List<TriangleRef> triangles, in LVector2 vertexA, in LVector2 vertexB, in LVector2 vertexC, TriangleRef tri)
{
if (vertexA == vertexB || vertexB == vertexC || vertexC == vertexA) { return; }
triangles.Add(new TriangleRef(vertexA, vertexB, vertexC, tri));
}
/// <summary>
/// 选择一条合适的切割线
/// </summary>
/// <param name="tris">待切割的三角形集合</param>
/// <returns></returns>
private static SplitLine PickSplitLine(List<TriangleRef> tris)
{
int minScore = int.MinValue;
int minLCount = 0;
int minRCount = 0;
SplitLine bestLine = new SplitLine();
int[] splitCounter = new int[(int)EnumSplitType.EnumCount]; // 每次切割,三角形位于切割线左右和线上的次数统计
var tirCount = tris.Count;
foreach (var tri in tris)
{ // 遍历所有三角形的边框线,作为切割线来切割空间,计算每次切割的分数,从而选取最优的分割线
foreach (var borderLine in tri.borders)
{
for (int i = 0; i < (int)EnumSplitType.EnumCount; i++)
{
splitCounter[i] = 0;
}
foreach (var otherTri in tris)
{
var val = (int)SplitLine.GetSplitResult(borderLine, otherTri);
splitCounter[val]++;
}
// Clock wise =>Left ++ CCW=>Right++// self tri is on the left
var leftCount = splitCounter[(int)EnumSplitType.Left];
var rightCount = splitCounter[(int)EnumSplitType.Right];
var splitCount = splitCounter[(int)EnumSplitType.OnPlane];
if ((leftCount == 0 || rightCount == 0) && (leftCount + rightCount) == tirCount)
{
// 所有的三角形都在同一侧,忽略
continue;
}
// 根据切割线左右的三角形数,以及切割的三角形数,计算本次分割的得分
var balanceVal = LMath.Abs(leftCount - rightCount);
var sameVal = LMath.Min(leftCount, rightCount);
int score = sameVal * 3 - balanceVal - splitCount * 2;
if (score > minScore)
{ // 选取得分最高的作为切割线
minLCount = leftCount;
minRCount = rightCount;
minScore = score;
bestLine = borderLine;
}
}
}
return bestLine;
}
}
}

View File

@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 133bf2d5724b64ac0945954588c88155
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -0,0 +1,10 @@
namespace JNGame.PathFinding
{
public enum EnumSplitType
{
Left,
Right,
OnPlane,
EnumCount
}
}

View File

@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: b9b5e74642ed142cd83c3453ce2d5894
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -0,0 +1,115 @@
using JNGame.Math;
namespace JNGame.PathFinding
{
/// <summary>
/// 二叉空间树的切割线
/// </summary>
public struct SplitLine
{
/// <summary>
/// 切割线起点
/// </summary>
public LVector2 vertexA;
/// <summary>
/// 切割线终点
/// </summary>
public LVector2 vertexB;
/// <summary>
/// 切割线方向
/// </summary>
public readonly LVector2 Direction => vertexB - vertexA;
/// <summary>
/// 构造切割线
/// </summary>
/// <param name="vertexA"></param>
/// <param name="vertexB"></param>
public SplitLine(in LVector2 vertexA, in LVector2 vertexB)
{
this.vertexA = vertexA;
this.vertexB = vertexB;
}
/// <summary>
/// 获取三角形的切割结果
/// </summary>
/// <param name="line">切割线</param>
/// <param name="tri">切割三角形</param>
/// <returns>切割线左侧、右侧或经过切割线</returns>
public static EnumSplitType GetSplitResult(in SplitLine line, TriangleRef tri)
{
var lineDir = line.Direction;
// 三角形三个顶点与切割线的位置关系
var valA = LVector2.Cross(lineDir, tri.vertexA - line.vertexA);
var valB = LVector2.Cross(lineDir, tri.vertexB - line.vertexA);
var valC = LVector2.Cross(lineDir, tri.vertexC - line.vertexA);
var isRight = false;
if (valA != 0) isRight = valA < 0;
if (valB != 0) isRight = valB < 0;
if (valC != 0) isRight = valC < 0;
var isA = valA <= 0;
var isB = valB <= 0;
var isC = valC <= 0;
if (isA == isB && isB == isC)
{
return isRight ? EnumSplitType.Right : EnumSplitType.Left;
}
isA = valA >= 0;
isB = valB >= 0;
isC = valC >= 0;
if (isA == isB && isB == isC)
{
return isRight ? EnumSplitType.Right : EnumSplitType.Left;
}
return EnumSplitType.OnPlane;
}
/// <summary>
/// 获取顶点和切割线的位置关系
/// </summary>
/// <param name="line">切割线</param>
/// <param name="vertex">顶点</param>
/// <returns></returns>
public static EnumSplitType ClassifyPointToPlane(in SplitLine line, in LVector2 vertex)
{
var val = LVector2.Cross(line.Direction, vertex - line.vertexA);
if (val == 0)
{
return EnumSplitType.OnPlane;
}
else
{
return val < 0 ? EnumSplitType.Right : EnumSplitType.Left;
}
}
/// <summary>
/// 计算p0p1,p2p3的交点
/// </summary>
/// <param name="p0"></param>
/// <param name="p1"></param>
/// <param name="p2"></param>
/// <param name="p3"></param>
/// <returns></returns>
public static LVector2 GetIntersectPoint(in LVector2 p0, in LVector2 p1, in LVector2 p2, in LVector2 p3)
{
var diff = p2 - p0;
var d1 = p1 - p0;
var d2 = p3 - p2;
var demo = LMath.Cross(d1, d2); //det
if (LMath.Abs(demo) < LFloat.EPSILON)
{
// 叉积结果为0说明两个向量平行
return p0;
}
var t1 = LMath.Cross(diff, d2) / demo; // Cross2D(diff,-d2)
return p0 + (p1 - p0) * t1;
}
}
}

View File

@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 6a160827a7e254845beb3b10a0644f26
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -0,0 +1,137 @@
using System;
// using JNGame.Collision2D;
using JNGame.Math;
// using Debug = JNGame.Logging.Debug;
namespace JNGame.PathFinding
{
/// <summary>
/// 导航三角形在二叉空间分割树中的引用
/// <para>一个导航三角形可能会被树分割为多个三角形引用</para>
/// </summary>
public class TriangleRef
{
/// <summary>
/// 当前三角形引用关联的导航三角形索引
/// </summary>
public int index;
/// <summary>
/// 三角形顶点A
/// </summary>
public LVector2 vertexA;
/// <summary>
/// 三角形顶点B
/// </summary>
public LVector2 vertexB;
/// <summary>
/// 三角形顶点C
/// </summary>
public LVector2 vertexC;
/// <summary>
/// 是否是分割的三角形
/// </summary>
public bool IsSplit { get; private set; } = false;
/// <summary>
/// 三角形的边框
/// </summary>
public SplitLine[] borders;
/// <summary>
/// 根据下标获取三角形的顶点合法下标0/1/2
/// </summary>
/// <param name="index"></param>
/// <returns></returns>
/// <exception cref="IndexOutOfRangeException"></exception>
public LVector2 this[int index]
{
get
{
return index switch
{
0 => vertexA,
1 => vertexB,
2 => vertexC,
_ => throw new IndexOutOfRangeException("vector idx invalid" + index),
};
}
}
/// <summary>
/// 根据原始导航三角形创建引用
/// <para>由此构造的三角形引用认为是未切割过的三角形</para>
/// </summary>
/// <param name="srcTriangle">原始的导航三角形</param>
public TriangleRef(NavTriangle srcTriangle)
: this(srcTriangle.vertexA.ToLVector2XZ(), srcTriangle.vertexB.ToLVector2XZ(), srcTriangle.vertexC.ToLVector2XZ(), srcTriangle.index)
{ }
/// <summary>
/// 根据分割的三个顶点和原始导航三角形创建引用
/// <para>由此构造的三角形引用认为是切割过的三角形</para>
/// </summary>
/// <param name="vertexA">三角形顶点A</param>
/// <param name="vertexB">三角形顶点B</param>
/// <param name="vertexC">三角形顶点C</param>
/// <param name="srcTriangle">原始的导航三角形</param>
public TriangleRef(in LVector2 vertexA, in LVector2 vertexB, in LVector2 vertexC, TriangleRef srcTriangle)
: this(vertexA, vertexB, vertexC, srcTriangle.index)
{
IsSplit = true;
}
/// <summary>
/// 私有构造
/// </summary>
/// <param name="vertexA">三角形顶点A</param>
/// <param name="vertexB">三角形顶点B</param>
/// <param name="vertexC">三角形顶点C</param>
/// <param name="idx">引用的导航三角形索引</param>
private TriangleRef(in LVector2 vertexA, in LVector2 vertexB, in LVector2 vertexC, int idx)
{
this.vertexA = vertexA;
this.vertexB = vertexB;
this.vertexC = vertexC;
index = idx;
borders = new SplitLine[]
{
new SplitLine(vertexA, vertexB),
new SplitLine(vertexB, vertexC),
new SplitLine(vertexC, vertexA)
};
//check valid
CheckValid();
}
/// <summary>
/// 三角形三点不可重合
/// </summary>
private void CheckValid()
{
for (int i = 0; i < 3; i++)
{
if (borders[i].Direction == LVector2.Zero)
{
// Debug.Assert(false);
}
}
}
/// <summary>
/// 判断点是否在此三角形内
/// </summary>
/// <param name="pos"></param>
/// <returns></returns>
public bool Contain(in LVector2 pos)
{
// ABC三个顶点绕序为顺时针三个叉积都需要为负则点在三角形内
var isRightA = LVector2.Cross(vertexB - vertexA, pos - vertexA) > 0;
if (isRightA) return false;
var isRightB = LVector2.Cross(vertexC - vertexB, pos - vertexB) > 0;
if (isRightB) return false;
var isRightC = LVector2.Cross(vertexA - vertexC, pos - vertexC) > 0;
if (isRightC) return false;
return true;
}
}
}

View File

@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 63961ae30f8cb4ca7a519fa7a7cdc394
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -0,0 +1,83 @@
using UnityEngine;
using JNGame.Math;
namespace JNGame.Map
{
/// <summary>
/// 地图数据
/// </summary>
[System.Serializable]
public class StaticMapData
{
/// <summary>
/// 地图ID
/// </summary>
public int mapID;
/// <summary>
/// 地图名
/// </summary>
public string mapName;
/// <summary>
/// 地图导航网格数据
/// </summary>
public PathFinding.NavMeshData navMeshData;
}
/// <summary>
/// 生成地图配置逻辑数据的脚步,仅作为编辑器导出地图场景数据的入口,运行时无作用
/// </summary>
public class GenMapConfigHelper : MonoBehaviour
{
/// <summary>
/// 地图ID
/// </summary>
public int mapID;
/// <summary>
/// 地图名
/// </summary>
public string mapName;
/// <summary>
/// Unity的Navigation组件
/// </summary>
public Unity.AI.Navigation.NavMeshSurface navMeshSurface;
private void OnDrawGizmos()
{
// if (m_MapConfig == null) { return; }
// DrawNavMesh();
}
// private void DrawNavMesh()
// {
// if (m_MapConfig == null || m_MapConfig.navMeshData == null || m_MapConfig.navMeshData.pathTriangles == null) { return; }
// int i = 0;
// Gizmos.color = Color.green;
// while (i < m_MapConfig.navMeshData.pathTriangles.Length)
// {
// int aIndex = m_MapConfig.navMeshData.pathTriangles[i++];
// int bIndex = m_MapConfig.navMeshData.pathTriangles[i++];
// int cIndex = m_MapConfig.navMeshData.pathTriangles[i++];
// Vector3 vertexA = m_MapConfig.navMeshData.pathVertices[aIndex].ToVector3();
// Vector3 vertexB = m_MapConfig.navMeshData.pathVertices[bIndex].ToVector3();
// Vector3 vertexC = m_MapConfig.navMeshData.pathVertices[cIndex].ToVector3();
// Gizmos.DrawLine(vertexA, vertexB);
// Gizmos.DrawLine(vertexB, vertexC);
// Gizmos.DrawLine(vertexC, vertexA);
// }
// Gizmos.color = Color.blue;
// Vector3 bottomLeft = new LVector3(m_MapConfig.navMeshData.xMin, 0.ToLFloat(), m_MapConfig.navMeshData.zMin).ToVector3();
// Vector3 bottomRight = new LVector3(m_MapConfig.navMeshData.xMax, 0.ToLFloat(), m_MapConfig.navMeshData.zMin).ToVector3();
// Vector3 topLeft = new LVector3(m_MapConfig.navMeshData.xMin, 0.ToLFloat(), m_MapConfig.navMeshData.zMax).ToVector3();
// Vector3 topRight = new LVector3(m_MapConfig.navMeshData.xMax, 0.ToLFloat(), m_MapConfig.navMeshData.zMax).ToVector3();
// Gizmos.DrawLine(bottomLeft, bottomRight);
// Gizmos.DrawLine(bottomLeft, topLeft);
// Gizmos.DrawLine(bottomRight, topRight);
// Gizmos.DrawLine(topLeft, topRight);
// }
}
}

View File

@@ -0,0 +1,3 @@
fileFormatVersion: 2
guid: c7bfc1bbc6a14b83a73b6c061d4f8227
timeCreated: 1715151863

View File

@@ -0,0 +1,8 @@
fileFormatVersion: 2
guid: 7f7bde9205575458d94afea935c783d8
folderAsset: yes
DefaultImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -0,0 +1,8 @@
fileFormatVersion: 2
guid: 6fa8733a350b745f291097ea3eb01303
folderAsset: yes
DefaultImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -0,0 +1,242 @@
using JNGame.Math;
namespace JNGame.PathFinding
{
public static class GeometryUtil
{
/// <summary>
/// Projects a point to a line segment.
/// <para>This implementation is thread-safe.</para>
/// </summary>
/// <param name="nearest">输出点point在线段的投影点</param>
/// <param name="start">线段起点</param>
/// <param name="end">线段终点</param>
/// <param name="point">被投影的原始点</param>
/// <returns>投影点与原始点的距离平方</returns>
public static LFloat NearestSegmentPointSquareDistance(ref LVector3 nearest, in LVector3 start, in LVector3 end, in LVector3 point)
{
nearest = start;
// 线段AB
var abX = end.x - start.x;
var abY = end.y - start.y;
var abZ = end.z - start.z;
// 线段AB的长度平方
var abLen2 = abX * abX + abY * abY + abZ * abZ;
if (abLen2 > 0)
{ // Avoid NaN due to the indeterminate form 0/0
// 投影点在线段上的比例位置
var t = ((point.x - start.x) * abX + (point.y - start.y) * abY + (point.z - start.z) * abZ) / abLen2;
var s = LMath.Clamp01(t);
nearest.x += abX * s;
nearest.y += abY * s;
nearest.z += abZ * s;
}
return LVector3.Distance2(nearest, point);
}
/// <summary>
/// Find the closest point on the triangle, given a measure point.
/// This is the optimized algorithm taken from the book "Real-Time Collision Detection".
/// <para>This implementation is NOT thread-safe.</para>
/// </summary>
/// <param name="vertexA"></param>
/// <param name="vertexB"></param>
/// <param name="vertexC"></param>
/// <param name="p"></param>
/// <param name="_out"></param>
/// <returns></returns>
public static LFloat GetClosestPointOnTriangle(in LVector3 vertexA, in LVector3 vertexB, in LVector3 vertexC, in LVector3 p, ref LVector3 _out)
{
// Check if P in vertex region outside A
var ab = vertexB - vertexA;
var ac = vertexC - vertexA;
var ap = p - vertexA;
var d1 = LVector3.Dot(ab, ap);
var d2 = LVector3.Dot(ac, ap);
if (d1 <= 0 && d2 <= 0)
{
_out = vertexA;
return LVector3.Distance2(p, vertexA);
}
// Check if P in vertex region outside B
var bp = p - vertexB;
var d3 = LVector3.Dot(ab, bp);
var d4 = LVector3.Dot(ac, bp);
if (d3 >= 0 && d4 <= d3)
{
_out = vertexB;
return LVector3.Distance2(p, vertexB);
}
// Check if P in edge region of AB, if so return projection of P onto AB
var vc = d1 * d4 - d3 * d2;
if (vc <= 0 && d1 >= 0 && d3 <= 0)
{
var v = d1 / (d1 - d3);
_out = vertexA + ab * v; // barycentric coordinates (1-v,v,0)
return LVector3.Distance2(p, _out);
}
// Check if P in vertex region outside C
var cp = p - vertexC;
var d5 = LVector3.Dot(ab, cp);
var d6 = LVector3.Dot(ac, cp);
if (d6 >= 0 && d5 <= d6)
{
_out = vertexC;
return LVector3.Distance2(p, vertexC);
}
// Check if P in edge region of AC, if so return projection of P onto AC
var vb = d5 * d2 - d1 * d6;
if (vb <= 0 && d2 >= 0 && d6 <= 0)
{
var w = d2 / (d2 - d6);
_out = vertexA + ac * w; // barycentric coordinates (1-w,0,w)
return LVector3.Distance2(_out, p);
}
// Check if P in edge region of BC, if so return projection of P onto BC
var va = d3 * d6 - d5 * d4;
if (va <= 0 && (d4 - d3) >= 0 && (d5 - d6) >= 0)
{
var w = (d4 - d3) / ((d4 - d3) + (d5 - d6));
_out = vertexB + (vertexC - vertexB) * w; // barycentric coordinates (0,1-w,w)
return LVector3.Distance2(_out, p);
}
// P inside face region. Compute Q through its barycentric coordinates (u,v,w)
var denom = 1 / (va + vb + vc);
{
LFloat v = vb * denom;
LFloat w = vc * denom;
_out = vertexA + ab * v + ac * w;
}
return LVector3.Distance2(_out, p);
}
public static bool IntersectRayTriangle(Ray ray, in LVector3 vertexA, in LVector3 vertexB, in LVector3 vertexC, out LVector3 intersection)
{
intersection = LVector3.Zero;
// AB边向量
LVector3 edgeAB = vertexB - vertexA;
// AC边向量
LVector3 edgeAC = vertexC - vertexA;
LVector3 pvec = LVector3.Cross(ray.direction, edgeAC);
LFloat det = LVector3.Dot(edgeAB, pvec);
if (LMath.Abs(det) <= LFloat.EPSILON)
{
var p = new Plane(vertexA, vertexB, vertexC);
if (p.TestPoint(ray.origin) == EnumPlaneSide.OnPlane && IsPointInTriangle(ray.origin, vertexA, vertexB, vertexC))
{
intersection = ray.origin;
return true;
}
return false;
}
det = 1 / det;
LVector3 tvec = ray.origin - vertexA;
LFloat u = LVector3.Dot(tvec, pvec) * det;
if (u < 0 || u > 1)
{
return false;
}
LVector3 qvec = LVector3.Cross(tvec, edgeAB);
LFloat v = LVector3.Dot(ray.direction, qvec) * det;
if (v < 0 || u + v > 1)
{
return false;
}
LFloat t = LVector3.Dot(edgeAC, qvec) * det;
if (t < 0)
{
return false;
}
if (LMath.Abs(t) <= LFloat.EPSILON)
{
intersection = ray.origin;
}
else
{
intersection = ray.GetEndPoint(t);
}
return true;
}
/// <summary>
/// 检测点point是否在三角形ABC内重心法
/// </summary>
/// <param name="point"></param>
/// <param name="vertexA"></param>
/// <param name="vertexB"></param>
/// <param name="vertexC"></param>
/// <returns></returns>
public static bool IsPointInTriangle(in LVector3 point, in LVector3 vertexA, in LVector3 vertexB, in LVector3 vertexC)
{
var vPA = vertexA - point;
var vPB = vertexB - point;
var vPC = vertexC - point;
var ab = LVector3.Dot(vPA, vPB);
var ac = LVector3.Dot(vPA, vPC);
var bc = LVector3.Dot(vPB, vPC);
var cc = LVector3.Dot(vPC, vPC);
if (bc * ac - cc * ab < 0)
return false;
var bb = LVector3.Dot(vPB, vPB);
if (ab * bc - ac * bb < 0)
return false;
return true;
}
/// <summary>
/// 计算点P是否在有向直线AB的左侧返回值>0s说明在左侧
/// <para>Computes the signed distance from a line connecting the
/// specified points to a specified point.</para>
/// </summary>
/// <param name="linePointA">直线上一点A</param>
/// <param name="linePointB">直线上一点B</param>
/// <param name="p">查询点P</param>
/// <returns>Positive when the point p lies to the left of the line ab.</returns>
public static LFloat IsLineLeft(in LVector2 linePointA, in LVector2 linePointB, in LVector2 p)
{
return LMath.Cross(linePointA - p, linePointB - linePointA);
}
/// <summary>
/// 返回点P到直线AB的距离平方
/// </summary>
/// <param name="linePointA">直线上一点A</param>
/// <param name="linePointB">直线上一点B</param>
/// <param name="pointP">点P</param>
/// <returns></returns>
public static LFloat SqrDistPointLineSegment(in LVector2 linePointA, in LVector2 linePointB, in LVector2 pointP)
{
LFloat r = LMath.Dot(pointP - linePointA, linePointB - linePointA) / (linePointB - linePointA).SqrMagnitude;
if (r < LFloat.Zero)
{
return (pointP - linePointA).SqrMagnitude;
}
if (r > LFloat.One)
{
return (pointP - linePointB).SqrMagnitude;
}
return (pointP - (linePointA + r * (linePointB - linePointA))).SqrMagnitude;
}
}
}

View File

@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 36c93c611b97d4495b67967418da8216
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -0,0 +1,158 @@
using JNGame.Math;
namespace JNGame.PathFinding
{
/// <summary>
/// 平面的数据结构
/// </summary>
public class Plane
{
/// <summary>
/// 平面法向量,单位向量,法向量方向为面的正面
/// </summary>
public LVector3 normal = LVector3.Zero;
/// <summary>
/// 平面到原点的距离
/// </summary>
public LFloat distFromOrigin = LFloat.Zero;
/// <summary>
/// 默认构造
/// </summary>
public Plane() { }
/// <summary>
/// 拷贝构造
/// </summary>
/// <param name="plane"></param>
public Plane(Plane plane)
{
this.normal = plane.normal;
this.distFromOrigin = plane.distFromOrigin;
}
/// <summary>
/// 方向向量和平面到原点的距离构成平面
/// </summary>
/// <param name="normal">平面的方向向量</param>
/// <param name="distance">平面到原点的距离</param>
public Plane(in LVector3 normal, LFloat distance)
{
this.normal = normal.Normalized;
this.distFromOrigin = distance;
}
/// <summary>
/// 方向向量和平面内一点构成平面
/// </summary>
/// <param name="normal">平面的方向向量</param>
/// <param name="point">平面内一点,也可视为原点到平面内一点的向量</param>
public Plane(in LVector3 normal, in LVector3 point)
{
this.normal = normal.Normalized;
this.distFromOrigin = -LVector3.Dot(this.normal, point);
}
/// <summary>
/// 三个点构成一个平面
/// </summary>
/// <param name="point1"></param>
/// <param name="point2"></param>
/// <param name="point3"></param>
public Plane(in LVector3 point1, in LVector3 point2, in LVector3 point3)
{
Set(point1, point2, point3);
}
/// <summary>
/// 根据三个点,设置平面信息
/// </summary>
/// <param name="point1"></param>
/// <param name="point2"></param>
/// <param name="point3"></param>
public void Set(in LVector3 point1, in LVector3 point2, in LVector3 point3)
{
LVector3 vector12 = point2 - point1; // 点1指向点2的向量
LVector3 vector23 = point3 - point2; // 点2指向点3的向量
normal = LVector3.Cross(vector12, vector23).Normalized;
// 规定原点在面的背面,因此距离为负
distFromOrigin = -LVector3.Dot(point1, normal);
}
/// <summary>
/// 根据点和法线,设置平面信息
/// </summary>
/// <param name="point"></param>
/// <param name="normal"></param>
public void Set(in LVector3 point, in LVector3 normal)
{
this.normal = normal.Normalized;
distFromOrigin = -LVector3.Dot(point, normal);
}
/// <summary>
/// 根据点和法线,设置平面信息
/// </summary>
/// <param name="pointX"></param>
/// <param name="pointY"></param>
/// <param name="pointZ"></param>
/// <param name="norX"></param>
/// <param name="norY"></param>
/// <param name="norZ"></param>
public void Set(LFloat pointX, LFloat pointY, LFloat pointZ, LFloat norX, LFloat norY, LFloat norZ)
{
this.normal = new LVector3(norX, norY, norZ).Normalized;
distFromOrigin = -(pointX * norX + pointY * norY + pointZ * norZ);
}
/// <summary>
/// 计算平面到点的距离
/// </summary>
/// <param name="point"></param>
/// <returns></returns>
public LFloat Distance(in LVector3 point)
{
// 点在normal方向的投影距离 + 面到原点的距离。根据符号,可判断点在面的哪一侧
return LVector3.Dot(normal, point) + distFromOrigin;
}
/// <summary>
/// 测试点位于平面的哪一面
/// </summary>
/// <param name="point"></param>
/// <returns></returns>
public EnumPlaneSide TestPoint(in LVector3 point)
{
LFloat dist = Distance(in point);
if (dist == 0)
return EnumPlaneSide.OnPlane;
else if (dist < 0)
return EnumPlaneSide.Back;
else
return EnumPlaneSide.Front;
}
/// <summary>
/// 是否位于正面
/// </summary>
/// <param name="direction"></param>
/// <returns></returns>
public bool IsFrontFacing(in LVector3 direction)
{
LFloat dot = LVector3.Dot(normal, direction);
return dot <= 0;
}
public void Reset()
{
normal = LVector3.Zero;
distFromOrigin = LFloat.Zero;
}
public override string ToString()
{
return normal.ToString() + ", " + distFromOrigin;
}
}
}

View File

@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 555aac364a42943c4993c540358381df
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -0,0 +1,50 @@
using JNGame.Math;
namespace JNGame.PathFinding
{
/// <summary>
/// 射线数据结构
/// </summary>
public class Ray
{
/// <summary>
/// 射线起点
/// </summary>
public LVector3 origin = LVector3.Zero;
/// <summary>
/// 射线方向,单位向量
/// </summary>
public LVector3 direction = LVector3.Zero;
public Ray() { }
public Ray(Ray ray)
{
this.origin = ray.origin;
this.direction = ray.direction;
}
public Ray(in LVector3 origin, in LVector3 direction)
{
this.origin = origin;
this.direction = direction.Normalized;
}
public LVector3 GetEndPoint(LFloat distance)
{
return origin + direction * distance;
}
public void Reset()
{
origin = LVector3.Zero;
direction = LVector3.Zero;
}
/** {@inheritDoc} */
public override string ToString()
{
return "ray [" + origin + ":" + direction + "]";
}
}
}

View File

@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 07d36ae82dda2434da808db57664e965
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -0,0 +1,8 @@
fileFormatVersion: 2
guid: 884e8e78a03eb4ca1bcdcc561b57c387
folderAsset: yes
DefaultImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -0,0 +1,32 @@
using JNGame.Math;
namespace JNGame.PathFinding
{
/// <summary>
/// 节点的边邻接关系接口定义
/// </summary>
/// <typeparam name="TNode">图元节点类型</typeparam>
public interface IConnection<TNode>
{
/// <summary>
/// 邻接关系的移动消耗
/// <para>Returns the non-negative cost of this connection</para>
/// </summary>
/// <returns></returns>
LFloat GetCost();
/// <summary>
/// 邻接关系的起始节点
/// <para>Returns the node that this connection came from</para>
/// </summary>
/// <returns>返回图元节点</returns>
TNode GetFromNode();
/// <summary>
/// 邻接关系指向的节点
/// <para>Returns the node that this connection leads to</para>
/// </summary>
/// <returns>返回图元节点</returns>
TNode GetToNode();
}
}

View File

@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 7fb05d24110ca4f9091409c8ff0b0220
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -0,0 +1,20 @@
using System.Collections.Generic;
namespace JNGame.PathFinding
{
/// <summary>
/// 图接口,图是一系列节点的集合,每个节点有邻接关系的列表
/// <para> A graph is a collection of nodes, each one having a collection of outgoing {@link Connection connections}. </para>
/// </summary>
/// <typeparam name="TNode">图元节点类型</typeparam>
public interface IGraph<TNode>
{
/// <summary>
/// 获取当前节点的邻接关系
/// </summary>
/// <param name="fromNode">the node whose outgoing connections will be returned</param>
/// <returns>the array of connections outgoing from the given node.</returns>
List<IConnection<TNode>> GetConnections(TNode fromNode);
}
}

View File

@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 086d1303386de40d395759867571b2a9
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -0,0 +1,32 @@
namespace JNGame.PathFinding
{
/// <summary>
/// A graph for the {@link IndexedAStarPathFinder}.
/// </summary>
/// <typeparam name="TNode">Type of node</typeparam>
public interface IIndexedGraph<TNode> : IGraph<TNode>
{
/// <summary>
/// 获取节点在图中的索引
/// <para>Returns the unique index of the given node.</para>
/// </summary>
/// <param name="node">the node whose index will be returned</param>
/// <returns>return the unique index of the given node.</returns>
int GetIndex(TNode node);
/// <summary>
/// 获取图中的节点总数
/// <para>Returns the number of nodes in this graph.</para>
/// </summary>
/// <returns></returns>
int GetNodeCount();
/// <summary>
/// 根据节点索引获取图元节点
/// </summary>
/// <param name="index"></param>
/// <returns></returns>
TNode GetNode(int index);
}
}

View File

@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 46150180c48a24416979310fdc726856
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -0,0 +1,161 @@
using System.Collections.Generic;
using JNGame.Math;
namespace JNGame.PathFinding
{
/// <summary>
/// <para> 导航网格数据 </para>
/// <para> 从Unity的NavMesh中导出经过定点库转化 </para>
/// </summary>
[System.Serializable]
public class NavMeshData
{
/// <summary>
/// NavMeshData关联的关卡地图ID
/// </summary>
public int mapID;
/// <summary>
/// 移动单位的包围盒半径,在烘焙导航网格时判断是否可通过
/// <para> 如果有包围盒半径差别特别大的,需要生成多份的导航网格数据供不同体型的可移动单位使用 </para>
/// </summary>
public LFloat radius = LFloat.Half;
/// <summary>
/// X轴方向最小位置坐标
/// </summary>
public LFloat xMin;
/// <summary>
/// Z轴方向最小位置坐标
/// </summary>
public LFloat zMin;
/// <summary>
/// X轴方向最大位置坐标
/// </summary>
public LFloat xMax;
/// <summary>
/// Z轴方向最大位置坐标
/// </summary>
public LFloat zMax;
/// <summary>
/// 宽X轴方向最小位置到最大位置的距离
/// </summary>
public LFloat Width => LMath.Abs(xMax - xMin);
/// <summary>
/// 高Z轴方向最小位置到最大位置的距离
/// </summary>
public LFloat Height => LMath.Abs(zMax - zMin);
/// <summary>
/// 行走区域的顶点坐标
/// </summary>
public LVector3[] pathVertices;
/// <summary>
/// 构成行走区域的三角形索引
/// </summary>
public int[] pathTriangles;
/// <summary>
/// 数据检测,客户端的顶点坐标和三角形数据有可能是重复的
/// <para>TODO 小三角形合并成大三角形或多边形;判断顶点是否在寻路层中,寻路层中的顶点不能作为路径点;两点所连线段是否穿过阻挡区,不穿过,直接获取坐标点</para>
/// </summary>
/// <param name="scale">缩放比例</param>
public void Check(int scale)
{
FixSameVertex(pathTriangles, pathVertices);
ScaleVertex(pathVertices, scale);
}
/// <summary>
/// 缩放顶点坐标,进行地图的缩放
/// </summary>
/// <param name="vertices">顶点集合</param>
/// <param name="scale">缩放比例</param>
private void ScaleVertex(LVector3[] vertices, int scale)
{
if (vertices == null || scale == 1)
{
return;
}
var lscale = scale.ToLFloat();
for (int i = 0; i < vertices.Length; i++)
{
vertices[i].x += (-this.xMin); // 缩放移动
vertices[i].z += (-this.zMin);
vertices[i] = vertices[i] * lscale;
}
}
/// <summary>
/// 重复顶点坐标,坐标相同的下标修改为一致
/// <para>unity的NavMeshData有一些共边的三角形共边的三角形其实不是连通关系共边的三角形只是他们共同构成一个凸多边形并且这种共边的三角形全部都是扇形排列。</para>
/// </summary>
/// <param name="indexs">索引数据</param>
/// <param name="vertices">顶点数据</param>
private void FixSameVertex(int[] indexs, LVector3[] vertices)
{
if (indexs == null || vertices == null)
{
return;
}
// 缓存顶点-索引映射
Dictionary<LVector3, int> cacheVertices = new Dictionary<LVector3, int>();
// 检测路径重复点
for (int i = 0; i < vertices.Length; ++i)
{
// 重复出现的坐标
if (cacheVertices.ContainsKey(vertices[i]))
{
for (int j = 0; j < indexs.Length; j++)
{
if (indexs[j] == i)
{ // 修正重复的坐标
indexs[j] = cacheVertices[vertices[i]];
}
}
}
else
{
cacheVertices.Add(vertices[i], i);
}
}
}
public int[] GetPathTriangles()
{
return pathTriangles;
}
public void SetPathTriangles(int[] pathTriangles)
{
this.pathTriangles = pathTriangles;
}
public LVector3[] GetPathVertices()
{
return pathVertices;
}
public void SetPathVertices(LVector3[] pathVertices)
{
this.pathVertices = pathVertices;
}
public int GetMapID()
{
return mapID;
}
public void SetMapID(int mapID)
{
this.mapID = mapID;
}
}
}

View File

@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: d4d0cc4c1d59a45f488d5ab7693a8572
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -0,0 +1,23 @@
using JNGame.Math;
namespace JNGame.PathFinding
{
public static class NavTriangleExtension
{
/// <summary>
/// 基于rf的值获取三角形内的一点
/// </summary>
/// <param name="triangle"></param>
/// <param name="r">点在三角形AB边的比值[0, 1]之间</param>
/// <param name="f">点在三角形BC边的比值[0, 1]之间</param>
/// <returns></returns>
public static LVector3 GetVertex(this NavTriangle triangle, LFloat r, LFloat f)
{
LVector3 vecAB = triangle.vertexB - triangle.vertexA;
LVector3 vecBC = triangle.vertexC - triangle.vertexB;
LFloat x = vecAB.x * r + vecBC.x * r * f + triangle.vertexA.x;
LFloat z = vecAB.z * r + vecBC.z * r * f + triangle.vertexA.z;
return new LVector3(x, LFloat.Zero, z);
}
}
}

View File

@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 49e7da5e90cc551448aa1c8cdedb2148
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -0,0 +1,127 @@
using System.Collections.Generic;
using JNGame.Math;
namespace JNGame.PathFinding
{
/// <summary>
/// 导航网格中的三角形数据结构
/// <para>三个顶点ABC,按照A->B->C->A的绕序。是顺时针绕序</para>
/// </summary>
public class NavTriangle
{
/// <summary>
/// 三角形序号
/// </summary>
public int index;
/// <summary>
/// 三角形顶点A
/// </summary>
public LVector3 vertexA;
/// <summary>
/// 三角形顶点B
/// </summary>
public LVector3 vertexB;
/// <summary>
/// 三角形顶点C
/// </summary>
public LVector3 vertexC;
/// <summary>
/// 三角形Y轴高度取三个顶点的平均高度
/// </summary>
public LFloat y;
/// <summary>
/// 三角形中心点
/// </summary>
public LVector3 center;
// /// <summary>
// /// 三角形顶点的索引
// /// </summary>
// public int[] vectorIndices;
/// <summary>
/// 三角形的邻接边关系集合
/// </summary>
public List<IConnection<NavTriangle>> connections;
public NavTriangle(in LVector3 vertexA, in LVector3 vertexB, in LVector3 vertexC, int index)
{
this.vertexA = vertexA;
this.vertexB = vertexB;
this.vertexC = vertexC;
this.y = (vertexA.y + vertexB.y + vertexC.y) / 3;
this.index = index;
this.center = (vertexA + vertexB + vertexC) / 3;
this.connections = new List<IConnection<NavTriangle>>();
// this.vectorIndices = vectorIndex;
}
/// <summary>
/// 判断点是否在三角形内
/// </summary>
/// <param name="point"></param>
/// <returns></returns>
public bool IsInnerPoint(LVector3 point)
{
bool res = PointInLineLeft(vertexA, vertexB, point);
if (res != PointInLineLeft(vertexB, vertexC, point))
{
return false;
}
if (res != PointInLineLeft(vertexC, vertexA, point))
{
return false;
}
if (Cross2D(vertexA, vertexB, vertexC) == 0)
{
//三点共线
return false;
}
return true;
}
public static LFloat Cross2D(LVector3 fromPoint, LVector3 toPoint, LVector3 p)
{
return (toPoint.x - fromPoint.x) * (p.z - fromPoint.z) - (toPoint.z - fromPoint.z) * (p.x - fromPoint.x);
}
public static bool PointInLineLeft(LVector3 fromPoint, LVector3 toPoint, LVector3 p)
{
return Cross2D(fromPoint, toPoint, p) > 0;
}
public override int GetHashCode()
{
int prime = 31;
int result = 1;
result = prime * result + index;
return result;
}
public override bool Equals(object obj)
{
if (this == obj)
return true;
if (obj == null)
return false;
if (GetType() != obj.GetType())
return false;
NavTriangle other = (NavTriangle)obj;
if (index != other.index)
return false;
return true;
}
public override string ToString()
{
return "Triangle [index=" + index + ", vertexA=" + vertexA + ", vertexB=" + vertexB + ", vertexC=" + vertexC + ", center=" + center + ", connections=" + connections.Count + "]";
}
}
}

View File

@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 1b4e6ea59290a47898b80b6d06b31483
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -0,0 +1,111 @@
using System.Text;
using JNGame.Math;
namespace JNGame.PathFinding
{
/// <summary>
/// 三角形节点的共享(邻接)边关系
/// </summary>
public class TriangleEdge : IConnection<NavTriangle>
{
/// <summary>
/// 源三角形
/// </summary>
public NavTriangle fromNode;
/// <summary>
/// 指向的三角形,可为空,为空说明该边没有邻接三角形
/// </summary>
public NavTriangle toNode;
/// <summary>
/// 三角形邻接边的顶点A边起点绕序同三角形的绕序
/// </summary>
public LVector3 vertexA;
/// <summary>
/// 三角形邻接边的顶点B边终点绕序同三角形的绕序
/// </summary>
public LVector3 vertexB;
public TriangleEdge()
{
this.fromNode = null;
this.toNode = null;
this.vertexA = LVector3.Zero;
this.vertexB = LVector3.Zero;
}
/// <summary>
/// 构建三角形节点的邻接边
/// </summary>
/// <param name="fromNode">边起点三角形</param>
/// <param name="toNode">边终点三角形</param>
/// <param name="vertexA">边连接的两个三角形的共享边的顶点</param>
/// <param name="vertexB">边连接的两个三角形的共享边的顶点</param>
public TriangleEdge(NavTriangle fromNode, NavTriangle toNode, in LVector3 vertexA, in LVector3 vertexB)
{
this.fromNode = fromNode;
this.toNode = toNode;
this.vertexA = vertexA;
this.vertexB = vertexB;
}
public LFloat GetCost()
{
// 邻边的消耗固定为1
return LFloat.One;
}
public NavTriangle GetFromNode()
{
return fromNode;
}
public NavTriangle GetToNode()
{
return toNode;
}
/// <summary>
/// 更新成员属性
/// </summary>
/// <param name="fromNode"></param>
/// <param name="toNode"></param>
/// <param name="vertexA"></param>
/// <param name="vertexB"></param>
public void Set(NavTriangle fromNode, NavTriangle toNode, in LVector3 vertexA, in LVector3 vertexB)
{
this.fromNode = fromNode;
this.toNode = toNode;
this.vertexA = vertexA;
this.vertexB = vertexB;
}
/// <summary>
/// 清理成员属性
/// </summary>
public void Clear()
{
this.fromNode = null;
this.toNode = null;
this.vertexA = LVector3.Zero;
this.vertexB = LVector3.Zero;
}
public bool IsValid()
{
return fromNode != null;
}
public override string ToString()
{
StringBuilder sb = new StringBuilder("Edge{");
sb.Append("fromNode=").Append(fromNode.index);
//sb.Append(", toNode=").Append(toNode == null ? "null" : toNode.index);
sb.Append(", VertexA=").Append(vertexA);
sb.Append(", VertexB=").Append(vertexB);
sb.Append('}');
return sb.ToString();
}
}
}

View File

@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 23cebc1b22ecc4befabcaec093f97db5
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -0,0 +1,557 @@
using System.Collections.Generic;
using JNGame.Math;
using Profiler = JNGame.Runtime.Util.Profiler;
namespace JNGame.PathFinding
{
/// <summary>
/// 以三角形为图节点的索引图数据结构
/// </summary>
public partial class TriangleGraph : IIndexedGraph<NavTriangle>
{
/// <summary>
/// 存储共边三角形共边关系
/// <para>Class for storing the edge connection data between two adjacent triangles.</para>
/// </summary>
public class IndexConnection
{
/// <summary>
/// <para>共边顶点索引1绕序的起点</para>
/// The vertex indices which makes up the edge shared between two triangles.
/// </summary>
public int edgeVertexIndex1;
/// <summary>
/// <para>共边顶点索引2绕序的终点</para>
/// The vertex indices which makes up the edge shared between two triangles.
/// </summary>
public int edgeVertexIndex2;
/// <summary>
/// <para>共边的三角形索引1</para>
/// The indices of the two triangles sharing this edge.
/// </summary>
public int fromTriIndex;
/// <summary>
/// <para>共边的三角形索引2</para>
/// The indices of the two triangles sharing this edge.
public int toTriIndex;
public IndexConnection(int sharedEdgeVertex1Index, int sharedEdgeVertex2Index, int fromTriIndex, int toTriIndex)
{
this.edgeVertexIndex1 = sharedEdgeVertex1Index;
this.edgeVertexIndex2 = sharedEdgeVertex2Index;
this.fromTriIndex = fromTriIndex;
this.toTriIndex = toTriIndex;
}
public override string ToString()
{
return "IndexConnection [edgeVertexIndex1=" + edgeVertexIndex1 + ", edgeVertexIndex2=" +
edgeVertexIndex2
+ ", fromTriIndex=" + fromTriIndex + ", toTriIndex=" + toTriIndex + "]";
}
public override int GetHashCode()
{
int prime = 31;
int result = 1;
result = prime * result + edgeVertexIndex1;
result = prime * result + edgeVertexIndex2;
result = prime * result + fromTriIndex;
result = prime * result + toTriIndex;
return result;
}
public override bool Equals(object obj)
{
if (this == obj)
return true;
if (obj == null)
return false;
if (GetType() != obj.GetType())
return false;
IndexConnection other = (IndexConnection)obj;
if (edgeVertexIndex1 != other.edgeVertexIndex1)
return false;
if (edgeVertexIndex2 != other.edgeVertexIndex2)
return false;
if (fromTriIndex != other.fromTriIndex)
return false;
if (toTriIndex != other.toTriIndex)
return false;
return true;
}
}
/// <summary>
/// 导航网格数据,从文件读取
/// </summary>
private readonly NavMeshData m_NavMeshData;
/// <summary>
/// 根据导航网格数据,生成的三角形
/// </summary>
private readonly List<NavTriangle> m_Triangles = new List<NavTriangle>();
public List<NavTriangle> Triangles => m_Triangles;
/// <summary>
/// 三角形的邻接边关系映射表 [Key, Value] = [三角形对象,共边的连接关系]
/// <para>与m_IsolatedEdgesMap应该是严格互补的</para>
/// </summary>
private readonly Dictionary<NavTriangle, List<IConnection<NavTriangle>>> m_SharedEdges;
/// <summary>
/// 三角形的独边关系映射表 [Key, Value] = [三角形对象,无邻边的连接关系]
/// <para>与m_SharedEdges应该是严格互补的</para>
/// </summary>
private readonly Dictionary<NavTriangle, List<IConnection<NavTriangle>>> m_IsolatedEdgesMap;
/// <summary>
/// BSP树通过空间分割快速查找点所在的三角形
/// </summary>
private readonly BspTree m_BspTree;
/// <summary>
/// 导航三角形中独边总数
/// </summary>
private readonly int m_NumDisconnectedEdges;
/// <summary>
/// 导航三角形中共享边总数
/// </summary>
private readonly int m_NumConnectedEdges;
/// <summary>
/// 导航三角形中所有的边总数
/// </summary>
private readonly int m_NumTotalEdges;
/// <summary>
/// 构造以三角形为节点的图数据结构
/// </summary>
/// <param name="navMeshData">导航网格数据</param>
/// <param name="scale">缩放比例默认1</param>
public TriangleGraph(NavMeshData navMeshData, int scale = 1)
{
m_NavMeshData = navMeshData;
// 检测导航网格数据中重复的顶点,并按需求对顶点进行缩放
m_NavMeshData.Check(scale);
// 根据导航网格数据构建三角形
var pathTriangles = CreateTriangles();
// 计算所有三角形的共边关系
var pathIndexConnections = GetIndexConnections(navMeshData.pathTriangles);
// 创建三角形邻边数据表
m_SharedEdges = CreateSharedEdgesMap(pathIndexConnections, pathTriangles, navMeshData.pathVertices);
// 创建三角形独边数据表
m_IsolatedEdgesMap = CreateIsolatedEdgesMap(m_SharedEdges);
// 创建BSP树耗时
m_BspTree = new BspTree();
m_BspTree.Init(pathTriangles);
// 统计独边的总数
foreach (var edges in m_IsolatedEdgesMap.Values)
{
m_NumDisconnectedEdges += edges.Count;
}
// 统计共享边的总数
foreach (var edges in m_SharedEdges.Values)
{
m_NumConnectedEdges += edges.Count;
}
m_NumConnectedEdges /= 2; // 相邻的三角形各有一条相互的共享边连接数据,因此需要/2
// 导航三角形中所有边的总数
m_NumTotalEdges = m_NumConnectedEdges + m_NumDisconnectedEdges;
// Debug.Log("CreateTriangleGraph Success! mapId{0} triangles{1} totalEdges{2} connEdges{3} disConnEdges{4}", navMeshData.mapID, m_Triangles.Count
// , m_NumTotalEdges, m_NumConnectedEdges, m_NumDisconnectedEdges);
}
/// <summary>
/// 获取当前三角形的邻接边数据
/// </summary>
/// <param name="fromNode">查询的三角形节点</param>
/// <returns></returns>
public List<IConnection<NavTriangle>> GetConnections(NavTriangle fromNode)
{
return m_SharedEdges.TryGetValue(fromNode, out var val) ? val : null;
}
public int GetIndex(NavTriangle node)
{
return node.index;
}
public int GetNodeCount()
{
return m_SharedEdges.Count;
}
public NavTriangle GetNode(int index)
{
if (index < 0 || index >= m_Triangles.Count)
{
return null;
}
return m_Triangles[index];
}
/// <summary>
/// 遍历NavMeshData的顶点数组和索引数组构建三角形
/// </summary>
/// <returns></returns>
private List<NavTriangle> CreateTriangles()
{
var vertexIndexs = m_NavMeshData.GetPathTriangles();
var vertices = m_NavMeshData.GetPathVertices();
int triangleIndex = 0; // 三角形下标
int length = vertexIndexs.Length - 3;
for (int i = 0; i <= length;)
{
int aIndex = vertexIndexs[i++];
int bIndex = vertexIndexs[i++];
int cIndex = vertexIndexs[i++];
NavTriangle triangle = new NavTriangle(vertices[aIndex], vertices[bIndex], vertices[cIndex], triangleIndex++);
m_Triangles.Add(triangle);
}
return m_Triangles;
}
/// <summary>
/// 获取所有三角形的共享边关系
/// </summary>
/// <param name="indices">顶点索引</param>
/// <returns></returns>
private static HashSet<IndexConnection> GetIndexConnections(int[] indices)
{
var indexConnections = new HashSet<IndexConnection>();
int[] edge = { -1, -1 }; // 隐含三角形顶点绕序0是绕序的起点1是绕序的终点
int i = 0;
int j, a0, a1, a2, b0, b1, b2, triAIndex, triBIndex;
while (i < indices.Length)
{
triAIndex = i / 3; // A三角形索引
a0 = indices[i++]; // 构成A三角形的三个顶点索引
a1 = indices[i++];
a2 = indices[i++];
j = i; // 第二个三角形从A三角形之后开始计算A三角形之前的连接关系已经在之前的循环中计算过了
while (j < indices.Length)
{
triBIndex = j / 3; // B三角形索引
b0 = indices[j++]; // 构成B三角形的三个顶点索引
b1 = indices[j++];
b2 = indices[j++];
if (HasSharedEdgeIndices(a0, a1, a2, b0, b1, b2, edge))
{
// 两个有共享边的三角形三角形A和三角形B共享边的绕序必定是相反的
var indexConnection1 = new IndexConnection(edge[0], edge[1], triAIndex, triBIndex);
var indexConnection2 = new IndexConnection(edge[1], edge[0], triBIndex, triAIndex);
indexConnections.Add(indexConnection1);
indexConnections.Add(indexConnection2);
edge[0] = -1;
edge[1] = -1;
}
}
}
// Debug.Log("连接个数:{0}", indexConnections.Count);
return indexConnections;
}
/// <summary>
/// 检测两个三角形是否有共享边,有两个共享索引,说明两个三角形存在共边
/// <para> Checks if the two triangles have shared vertex indices.</para>
/// <para>The edge will always follow the vertex winding order of the triangle A. Since all
/// triangles have the same winding order, triangle A should have the opposite
/// edge direction to triangle B.</para>
/// </summary>
/// <param name="a0">三角形A顶点0索引</param>
/// <param name="a1">三角形A顶点1索引</param>
/// <param name="a2">三角形A顶点2索引</param>
/// <param name="b0">三角形B顶点0索引</param>
/// <param name="b1">三角形B顶点1索引</param>
/// <param name="b2">三角形B顶点2索引</param>
/// <param name="edge">输出:[共边顶点索引1, 共边顶点索引2]</param>
/// <returns>bool: 是否存在共边</returns>
private static bool HasSharedEdgeIndices(int a0, int a1, int a2, int b0, int b1, int b2, int[] edge)
{
bool match0 = (a0 == b0 || a0 == b1 || a0 == b2);
bool match1 = (a1 == b0 || a1 == b1 || a1 == b2);
if (!match0 && !match1)
{ // 无两个共享点
return false;
}
else if (match0 && match1)
{
edge[0] = a0; // 保证三角形绕序一致0->1->2
edge[1] = a1;
return true;
}
bool match2 = (a2 == b0 || a2 == b1 || a2 == b2);
if (match0 && match2)
{
edge[0] = a2; // 保证三角形绕序一致0->1->2
edge[1] = a0;
return true;
}
else if (match1 && match2)
{
edge[0] = a1; // 保证三角形绕序一致0->1->2
edge[1] = a2;
return true;
}
return false;
}
/// <summary>
/// 创建三角形共享边映射表 [Key, Value] = [三角形对象,共边的连接关系]
/// </summary>
/// <param name="indexConnections">共享边关系集合</param>
/// <param name="triangles">三角形集合</param>
/// <param name="vertices">顶点集合</param>
/// <returns></returns>
private static Dictionary<NavTriangle, List<IConnection<NavTriangle>>> CreateSharedEdgesMap(
HashSet<IndexConnection> indexConnections, List<NavTriangle> triangles, LVector3[] vertices)
{
var connectionMap = new Dictionary<NavTriangle, List<IConnection<NavTriangle>>>();
foreach (NavTriangle triangle in triangles)
{
connectionMap.Add(triangle, new List<IConnection<NavTriangle>>());
}
foreach (IndexConnection indexConnection in indexConnections)
{
var fromNode = triangles[indexConnection.fromTriIndex];
var toNode = triangles[indexConnection.toTriIndex];
var edgeVertexA = vertices[indexConnection.edgeVertexIndex1];
var edgeVertexB = vertices[indexConnection.edgeVertexIndex2];
var edge = new TriangleEdge(fromNode, toNode, edgeVertexA, edgeVertexB);
connectionMap[fromNode].Add(edge);
fromNode.connections.Add(edge);
//Debug.Log("Triangle{0}-->{1} {2}-->{3}", fromNode.index, toNode.index, fromNode, toNode);
}
return connectionMap;
}
/// <summary>
/// 创建三角形的没有邻接边的关系映射表
/// </summary>
/// <param name="connectionMap">邻边关系集合</param>
/// <returns>无邻边关系映射表</returns>
private static Dictionary<NavTriangle, List<IConnection<NavTriangle>>> CreateIsolatedEdgesMap(
Dictionary<NavTriangle, List<IConnection<NavTriangle>>> connectionMap)
{
var disconnectionMap = new Dictionary<NavTriangle, List<IConnection<NavTriangle>>>();
foreach (NavTriangle tri in connectionMap.Keys)
{
var connectedEdges = connectionMap[tri];
// 没有邻接边的关系集合
var disconnectedEdges = new List<IConnection<NavTriangle>>();
disconnectionMap.Add(tri, disconnectedEdges);
if (connectedEdges.Count < 3)
{ // This triangle does not have all edges connected to other triangles
bool ab = true; // 三角形顶点AB边没有邻接边
bool bc = true; // 三角形顶点BC边没有邻接边
bool ca = true; // 三角形顶点CA边没有邻接边
foreach (var item in connectedEdges)
{
var edge = item as TriangleEdge;
if (edge.vertexA == tri.vertexA && edge.vertexB == tri.vertexB)
{
ab = false; // AB边在邻接边关系集合中标记置为false
}
else if (edge.vertexA == tri.vertexB && edge.vertexB == tri.vertexC)
{
bc = false; // BC边在邻接边关系集合中标记置为false
}
else if (edge.vertexA == tri.vertexC && edge.vertexB == tri.vertexA)
{
ca = false; // CA边在邻接边关系集合中标记置为false
}
}
// 没有邻边三角形的,加入没有邻边集合中
if (ab)
{
disconnectedEdges.Add(new TriangleEdge(tri, null, tri.vertexA, tri.vertexB));
}
if (bc)
{
disconnectedEdges.Add(new TriangleEdge(tri, null, tri.vertexB, tri.vertexC));
}
if (ca)
{
disconnectedEdges.Add(new TriangleEdge(tri, null, tri.vertexC, tri.vertexA));
}
}
// 断言所有边关系的总和必定为三角形边数
int totalEdges = connectedEdges.Count + disconnectedEdges.Count;
// Debug.Assert(totalEdges == 3, "Wrong number of edges ({0}) in triangle {1}", totalEdges, tri.index);
}
return disconnectionMap;
}
/// <summary>
/// 根据点位置获取所在的三角形
/// </summary>
/// <param name="point"></param>
/// <returns></returns>
public NavTriangle GetTriangle(in LVector3 point)
{
Profiler.BeginSample("InternalGetTriangle");
var ret = InternalGetTriangle(in point, out _);
Profiler.EndSample();
return ret;
}
/// <summary>
/// 根据点位置获取所在的三角形,如果点不在,则获取邻接的三角形,并输出在三角形上的邻接点
/// </summary>
/// <param name="point"></param>
/// <param name="pointOnTriangle">三角形上邻接point的点</param>
/// <returns></returns>
public NavTriangle GetTriangleOrNeareastTriangle(in LVector3 point, out LVector3 pointOnTriangle)
{
Profiler.BeginSample("GetTriangleOrNearestTriangle");
var ret = InternalGetTriangle(in point, out List<int> nearTriangles);
if (ret == null)
{
// 不在三角形内,找最近的一个三角形
LFloat minDist = LFloat.FLT_MAX;
LVector2 pointInVec2 = point.ToLVector2XZ();
for (int i = 0; i < nearTriangles.Count; ++i)
{
int triangleIndex = nearTriangles[i];
NavTriangle triangle = m_Triangles[triangleIndex];
if (m_IsolatedEdgesMap.TryGetValue(triangle, out List<IConnection<NavTriangle>> isolatedEdges))
{
// 查找独边,找邻接三角形
TriangleEdge edge = InternalGetNearestEdge(pointInVec2, isolatedEdges, out LFloat outSqrDistMin);
if (outSqrDistMin < minDist)
{
ret = edge.fromNode;
minDist = outSqrDistMin;
}
}
}
pointOnTriangle = LVector3.Zero;
if (ret != null)
{
GeometryUtil.GetClosestPointOnTriangle(ret.vertexA, ret.vertexB, ret.vertexC, point, ref pointOnTriangle);
// 稍微向三角形方向内部一点,避免严格在边上,三角形落位判断问题
pointOnTriangle = point + (pointOnTriangle - point) * new LFloat(true, 1010000);
}
}
else
{
pointOnTriangle = point;
}
Profiler.EndSample();
return ret;
}
/// <summary>
/// 获取最邻接目标点的共享边
/// </summary>
/// <param name="navTriangle"></param>
/// <param name="position"></param>
/// <returns></returns>
public TriangleEdge GetNearestSharedEdge(NavTriangle navTriangle, in LVector3 position)
{
if (m_SharedEdges.TryGetValue(navTriangle, out List<IConnection<NavTriangle>> outConnections))
{
return InternalGetNearestEdge(position.ToLVector2XZ(), outConnections, out _);
}
return null;
}
/// <summary>
/// 获取最邻接目标点的阻挡边
/// </summary>
/// <param name="navTriangle"></param>
/// <param name="position"></param>
/// <returns></returns>
public TriangleEdge GetNeareastIsolatedEdge(NavTriangle navTriangle, in LVector3 position)
{
if (m_IsolatedEdgesMap.TryGetValue(navTriangle, out List<IConnection<NavTriangle>> outConnections))
{
return InternalGetNearestEdge(position.ToLVector2XZ(), outConnections, out _);
}
return null;
}
/// <summary>
/// 获取最邻接目标点的三角形边,不考虑阻挡还是共享
/// </summary>
/// <param name="navTriangle"></param>
/// <param name="position"></param>
/// <returns></returns>
public TriangleEdge GetNearestEdge(NavTriangle navTriangle, in LVector3 position)
{
// 最近的共享边
TriangleEdge nearestSharedEdge = null;
LFloat sharedEdgeSqrDist = LFloat.FLT_MAX;
if (m_SharedEdges.TryGetValue(navTriangle, out List<IConnection<NavTriangle>> outSharedConnections))
{
nearestSharedEdge = InternalGetNearestEdge(position.ToLVector2XZ(), outSharedConnections, out sharedEdgeSqrDist);
}
// 最近的阻挡边
TriangleEdge nearestIsolatedEdge = null;
LFloat osolatedEdgeSqrDist = LFloat.FLT_MAX;
if (m_IsolatedEdgesMap.TryGetValue(navTriangle, out List<IConnection<NavTriangle>> outIsolatedConnections))
{
nearestIsolatedEdge = InternalGetNearestEdge(position.ToLVector2XZ(), outIsolatedConnections, out osolatedEdgeSqrDist);
}
// 选取最近的返回
return sharedEdgeSqrDist < osolatedEdgeSqrDist ? nearestSharedEdge : nearestIsolatedEdge;
}
/// <summary>
/// 获取点落位的三角形,若为空,会附带点附近的一些三角形
/// </summary>
/// <param name="point"></param>
/// <param name="nearTriangles"></param>
/// <returns></returns>
private NavTriangle InternalGetTriangle(in LVector3 point, out List<int> nearTriangles)
{
// 通过Bsp空间分割树查找点所在的三角形
var index = m_BspTree.GetTriangle(point, out nearTriangles);
if (index >= 0 && index < m_Triangles.Count)
{
return m_Triangles[index];
}
return null;
}
/// <summary>
/// 获取距离指定位置最近的边
/// </summary>
/// <param name="position">目标位置</param>
/// <param name="edges">边集合</param>
/// <param name="outSqrDistMin">输出最短距离</param>
/// <returns></returns>
private TriangleEdge InternalGetNearestEdge(in LVector2 position, List<IConnection<NavTriangle>> edges, out LFloat outSqrDistMin)
{
outSqrDistMin = LFloat.FLT_MAX;
if (edges == null || edges.Count <= 0) { return null; }
TriangleEdge targetEdge = null;
foreach (IConnection<NavTriangle> connection in edges)
{
if (connection is not TriangleEdge edge) { continue; }
LFloat sprDist = GeometryUtil.SqrDistPointLineSegment(edge.vertexA.ToLVector2XZ(), edge.vertexB.ToLVector2XZ(), position);
if (sprDist < outSqrDistMin)
{
targetEdge = edge;
outSqrDistMin = sprDist;
}
}
return targetEdge;
}
}
}

View File

@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 68a7ecd59eb0548cf9676b742a1855b8
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -0,0 +1,69 @@
#if UNITY_5_3_OR_NEWER
using JNGame.Math;
using UnityEngine;
namespace JNGame.PathFinding
{
public partial class TriangleNavMesh
{
#if UNITY_EDITOR
public void DebugVisualize()
{
m_Graph.DebugVisualize();
}
public void DrawGizmos()
{
m_Graph.DrawGizmos();
}
#endif
}
public partial class TriangleGraph
{
#if UNITY_EDITOR
public void DebugVisualize()
{
m_BspTree.DebugDraw();
}
public void DrawGizmos()
{
if (m_NavMeshData == null || m_NavMeshData.pathTriangles == null) { return; }
int i = 0;
Gizmos.color = Color.green;
while (i < m_NavMeshData.pathTriangles.Length)
{
int aIndex = m_NavMeshData.pathTriangles[i++];
int bIndex = m_NavMeshData.pathTriangles[i++];
int cIndex = m_NavMeshData.pathTriangles[i++];
Vector3 vertexA = m_NavMeshData.pathVertices[aIndex].ToVector3();
Vector3 vertexB = m_NavMeshData.pathVertices[bIndex].ToVector3();
Vector3 vertexC = m_NavMeshData.pathVertices[cIndex].ToVector3();
Gizmos.DrawLine(vertexA, vertexB);
Gizmos.DrawLine(vertexB, vertexC);
Gizmos.DrawLine(vertexC, vertexA);
}
Gizmos.color = Color.blue;
Vector3 bottomLeft = new LVector3(m_NavMeshData.xMin, 0.ToLFloat(), m_NavMeshData.zMin).ToVector3();
Vector3 bottomRight = new LVector3(m_NavMeshData.xMax, 0.ToLFloat(), m_NavMeshData.zMin).ToVector3();
Vector3 topLeft = new LVector3(m_NavMeshData.xMin, 0.ToLFloat(), m_NavMeshData.zMax).ToVector3();
Vector3 topRight = new LVector3(m_NavMeshData.xMax, 0.ToLFloat(), m_NavMeshData.zMax).ToVector3();
Gizmos.DrawLine(bottomLeft, bottomRight);
Gizmos.DrawLine(bottomLeft, topLeft);
Gizmos.DrawLine(bottomRight, topRight);
Gizmos.DrawLine(topLeft, topRight);
}
#endif
}
}
#endif

View File

@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: b08810f156faa684f92fa3cd9cb54c14
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -0,0 +1,205 @@
using System.Collections.Generic;
using JNGame.Math;
using Newtonsoft.Json;
namespace JNGame.PathFinding
{
/// <summary>
/// 边查找过滤枚举
/// </summary>
[System.Flags]
public enum EnumEdgeFilter
{
/// <summary>
/// 共享边
/// </summary>
kFilterShared = 1,
/// <summary>
/// 独边
/// </summary>
kFilterIsolated = 2,
/// <summary>
/// 全部边
/// </summary>
kFilterAll = kFilterShared | kFilterIsolated
}
/// <summary>
/// 以三角形为图元的导航网格
/// </summary>
public partial class TriangleNavMesh
{
/// <summary>
/// 三角形图元节点组成的图
/// </summary>
private readonly TriangleGraph m_Graph;
/// <summary>
/// 路径查找器
/// </summary>
private readonly IndexedAStarPathFinder<NavTriangle> m_PathFinder;
/// <summary>
/// 寻路启发器
/// </summary>
private readonly TriangleHeuristic m_Heuristic;
/// <summary>
/// 寻路坐标描述结果的缓存,每次寻路都会被清理和覆盖,外部不可直接使用本对象
/// </summary>
private readonly TrianglePointPath m_TempPointPath = new TrianglePointPath();
/// <summary>
/// 寻路节点描述结果的缓存,每次寻路都会被清理和覆盖,外部不可直接使用本对象
/// </summary>
private readonly TriangleEdgeGraphPath m_TempGraphPath = new TriangleEdgeGraphPath();
/// <summary>
/// 构造导航网格
/// </summary>
/// <param name="jsonNavMeshData">导航网格数据Json格式字符串</param>
/// <param name="scale"></param>
public TriangleNavMesh(string jsonNavMeshData, int scale = 1)
{
var data = JsonConvert.DeserializeObject<NavMeshData>(jsonNavMeshData);
m_Graph = new TriangleGraph(data, scale);
m_PathFinder = new IndexedAStarPathFinder<NavTriangle>(m_Graph, true);
m_Heuristic = new TriangleHeuristic();
}
/// <summary>
/// 构造导航网格
/// </summary>
/// <param name="navMeshData">导航网格数据</param>
/// <param name="scale"></param>
public TriangleNavMesh(NavMeshData navMeshData, int scale = 1)
{
m_Graph = new TriangleGraph(navMeshData, scale);
m_PathFinder = new IndexedAStarPathFinder<NavTriangle>(m_Graph, true);
m_Heuristic = new TriangleHeuristic();
}
/// <summary>
/// 搜索从起点fromPoint到终点toPoint的路径
/// </summary>
/// <param name="fromPoint">起点位置</param>
/// <param name="toPoint">终点位置</param>
/// <param name="allowFixToPoint">是否运行修正终点坐标,如果终点在阻挡内,使用阻挡边的邻接点作为终点</param>
/// <returns>路径经过的坐标点列表</returns>
public List<LVector3> FindPath(in LVector3 fromPoint, in LVector3 toPoint, bool allowFixToPoint = false)
{
// 清理结果缓存
m_TempPointPath.Clear();
m_TempGraphPath.Clear();
// 查找目标路径
bool find = FindPath(fromPoint, toPoint, m_TempGraphPath, allowFixToPoint);
if (!find)
{
return null;
}
// 节点路径描述转化为坐标路径描述
// m_TempPointPath.CalculateForGraphPath(m_TempGraphPath, false);
// Profiler.BeginSample("PathTranslate");
m_TempPointPath.CalculateForGraphPath(m_TempGraphPath, false);
// Profiler.EndSample();
return m_TempPointPath.GetVectors();
}
/// <summary>
/// 搜索从fromPoint到toPoint的路径路径结果用路径经过的图上的边或结点表示
/// </summary>
/// <param name="fromPoint">起点位置</param>
/// <param name="toPoint">终点位置</param>
/// <param name="allowFixToPoint">是否运行修正终点坐标,如果终点在阻挡内,使用阻挡边的邻接点作为终点</param>
/// <param name="path">结果路径</param>
/// <returns>是否成功找出路径</returns>
private bool FindPath(in LVector3 fromPoint, in LVector3 toPoint, TriangleEdgeGraphPath path, bool allowFixToPoint)
{
// 获取起始点和目标点所在的三角形节点
NavTriangle fromTriangle = m_Graph.GetTriangle(in fromPoint);
LVector3 toPointFixed = toPoint;
NavTriangle toTriangle = allowFixToPoint ? m_Graph.GetTriangleOrNeareastTriangle(in toPoint, out toPointFixed)
: m_Graph.GetTriangle(in toPoint);
// 验证输入合法性
if (fromTriangle == null)
{
//Debug.LogWarning("TriangleNavMesh.FindPath Failed! fromPoint = {0} valid!", fromPoint);
return false;
}
if (toTriangle == null)
{
//Debug.LogWarning("TriangleNavMesh.FindPath Failed! toPoint = {0} valid!", toPoint);
return false;
}
// Profiler.BeginSample("AStarPathFinder");
// 搜索路径
bool result = m_PathFinder.SearchPath(fromTriangle, toTriangle, m_Heuristic, path);
if (result)
{
path.startPoint = fromPoint;
path.endPoint = toPointFixed;
path.startTriangle = fromTriangle;
}
// Profiler.EndSample();
return result;
}
/// <summary>
/// 查找坐标落位的三角形,空说明在阻挡区内
/// </summary>
/// <param name="position"></param>
/// <returns></returns>
public NavTriangle GetTriangle(in LVector3 position)
{
return m_Graph.GetTriangle(position);
}
/// <summary>
/// 查找坐标落位的三角形或是距离最近的三角形,返回最近三角形上的点
/// </summary>
/// <param name="position"></param>
/// <param name="outNearestPosition"></param>
/// <returns></returns>
public NavTriangle GetTriangleOrNearestTriangle(in LVector3 position, out LVector3 outNearestPosition)
{
return m_Graph.GetTriangleOrNeareastTriangle(position, out outNearestPosition);
}
/// <summary>
/// 查询距离指定位置最近的三角形网格边
/// </summary>
/// <param name="navTriangle">三角形</param>
/// <param name="position">位置</param>
/// <param name="filter">边查找过滤</param>
/// <returns></returns>
public TriangleEdge GetNearestEdge(NavTriangle navTriangle, in LVector3 position, EnumEdgeFilter filter)
{
return filter switch
{
EnumEdgeFilter.kFilterShared => m_Graph.GetNearestSharedEdge(navTriangle, in position),
EnumEdgeFilter.kFilterIsolated => m_Graph.GetNeareastIsolatedEdge(navTriangle, in position),
EnumEdgeFilter.kFilterAll => m_Graph.GetNearestEdge(navTriangle, in position),
_ => null,
};
}
/// <summary>
/// 根据索引获取三角形网格
/// </summary>
/// <param name="index"></param>
/// <returns></returns>
public NavTriangle GetTriangle(int index)
{
return m_Graph.GetNode(index);
}
/// <summary>
/// 获取全部的导航三角形网格数据
/// </summary>
/// <returns></returns>
public List<NavTriangle> GetNavTriangles()
{
return m_Graph.Triangles;
}
}
}

View File

@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 0de2ec35d68a342c292a00bb7f43e327
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -0,0 +1,8 @@
fileFormatVersion: 2
guid: b8bb5a4c219ae4dcbbd5a7b6ff3b6b85
folderAsset: yes
DefaultImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -0,0 +1,62 @@
using System.Collections.Generic;
namespace JNGame.PathFinding
{
/// <summary>
/// 描述图数据结构上的一条路径
/// </summary>
/// <typeparam name="TEdge">路径节点描述:可以是图数据结构中邻接边或节点</typeparam>
public class DefaultGraphPath<TEdge> : IGraphPath<TEdge>
{
/// <summary>
/// 路径节点的集合,节点可以用图的邻接边或节点描述
/// </summary>
public List<TEdge> nodes;
/// <summary>
/// Creates a {@code DefaultGraphPath} with no nodes.
/// </summary>
public DefaultGraphPath() : this(new List<TEdge>()) { }
/// <summary>
/// Creates a {@code DefaultGraphPath} with the given capacity and no nodes.
/// </summary>
/// <param name="capacity"></param>
public DefaultGraphPath(int capacity) : this(new List<TEdge>(capacity)) { }
/// <summary>
/// Creates a {@code DefaultGraphPath} with the given nodes.
/// </summary>
/// <param name="nodes"></param>
public DefaultGraphPath(List<TEdge> nodes)
{
this.nodes = nodes;
}
public void Clear()
{
nodes.Clear();
}
public int GetCount()
{
return nodes.Count;
}
public void Add(TEdge node)
{
nodes.Add(node);
}
public TEdge Get(int index)
{
return nodes[index];
}
public void Reverse()
{
nodes.Reverse();
}
}
}

View File

@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 2c513670f2d7d435ab65402918015090
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -0,0 +1,48 @@
using System.Collections.Generic;
using JNGame.Math;
namespace JNGame.PathFinding
{
public class EdgePoint
{
/**
* Triangle which must be crossed to reach the next path point.
*/
public NavTriangle toNode;
/**
* Triangle which was crossed to reach this point.
*/
public NavTriangle fromNode;
/**
* Path edges connected to this point. Can be used for spline generation at some
* point perhaps...
*/
public List<TriangleEdge> connectingEdges = new List<TriangleEdge>(8);
/**
* The point where the path crosses an edge.
*/
public LVector3 point;
public EdgePoint()
{
point = LVector3.Zero;
toNode = null;
}
public void Set(in LVector3 point, NavTriangle toNode)
{
this.point = point;
this.toNode = toNode;
}
public EdgePoint(in LVector3 point, NavTriangle toNode)
{
this.point = point;
this.toNode = toNode;
}
}
}

View File

@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: a8be0d16ec4044fbc8b80bb7a236024b
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -0,0 +1,75 @@
using JNGame.Math;
namespace JNGame.PathFinding
{
/// <summary>
/// 漏斗
/// </summary>
public class Funnel
{
/// <summary>
/// 漏斗点,路径的起点或拐点
/// </summary>
public LVector3 pivot = LVector3.Zero;
/// <summary>
/// 漏斗的边界面A
/// </summary>
public Plane leftPlane = new Plane(); // 左平面高度为y轴
/// <summary>
/// 漏斗的边界面B
/// </summary>
public Plane rightPlane = new Plane();
/// <summary>
/// 漏斗的边界面A与三角形平面的交点
/// </summary>
public LVector3 leftPortal = LVector3.Zero; // 路径左顶点,
/// <summary>
/// 漏斗的边界面B与三角形平面的交点
/// </summary>
public LVector3 rightPortal = LVector3.Zero; // 路径右顶点
public void SetLeftPlane(in LVector3 pivot, in LVector3 leftEdgeVertex)
{
leftPlane.Set(pivot, pivot + LVector3.Up, leftEdgeVertex);
leftPortal = leftEdgeVertex;
}
public void SetRightPlane(in LVector3 pivot, in LVector3 rightEdgeVertex)
{
rightPlane.Set(pivot, pivot + LVector3.Up, rightEdgeVertex); // 高度
rightPlane.normal = -rightPlane.normal; // 平面方向取反
rightPlane.distFromOrigin = -rightPlane.distFromOrigin;
rightPortal = rightEdgeVertex;
}
/// <summary>
/// 设置漏斗的两个边面
/// </summary>
/// <param name="pivot">漏斗点</param>
/// <param name="edge">三角形的共享边</param>
public void SetPlanes(in LVector3 pivot, TriangleEdge edge)
{
SetLeftPlane(pivot, edge.vertexB);
SetRightPlane(pivot, edge.vertexA);
}
public EnumPlaneSide SideLeftPlane(in LVector3 point)
{
return leftPlane.TestPoint(point);
}
public EnumPlaneSide SideRightPlane(in LVector3 point)
{
return rightPlane.TestPoint(point);
}
public void Reset()
{
pivot = LVector3.Zero;
leftPlane.Reset();
rightPlane.Reset();
leftPortal = LVector3.Zero;
rightPortal = LVector3.Zero;
}
}
}

View File

@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 55922bc76371f4895b885c6663d8d181
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -0,0 +1,44 @@
namespace JNGame.PathFinding
{
/// <summary>
/// 描述一条路径的接口
/// </summary>
/// <typeparam name="TEdge">路径节点描述:可以是图数据结构中邻接边或节点</typeparam>
public interface IGraphPath<TEdge>
{
/// <summary>
/// 返回路径节点总数
/// <para> Returns the number of items of this path. </para>
/// </summary>
/// <returns></returns>
int GetCount();
/// <summary>
/// 获取指定索引路径节点
/// <para> Returns the item of this path at the given index. </para>
/// </summary>
/// <param name="index"></param>
/// <returns></returns>
TEdge Get(int index);
/// <summary>
/// 添加一个路径节点到列表尾部
/// <para> Adds an item at the end of this path. </para>
/// </summary>
/// <param name="node"></param>
void Add(TEdge node);
/// <summary>
/// 清理所有的路径节点
/// <para> Clears this path. </para>
/// </summary>
void Clear();
/// <summary>
/// 翻转路径节点(节点A->节点B 变为 节点B->节点A)
/// <para> Reverses this path. </para>
/// </summary>
void Reverse();
}
}

View File

@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 3b81b57e2d2d1466eadfe89f96171b11
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -0,0 +1,25 @@
using JNGame.Math;
namespace JNGame.PathFinding
{
/// <summary>
/// <para> A {@code Heuristic} generates estimates of the cost to move from a given node to the goal. </para>
/// With a heuristic function pathfinding algorithms can choose the node that is most likely to lead to the optimal path. The
/// notion of "most likely" is controlled by a heuristic. If the heuristic is accurate, then the algorithm will be efficient. If
/// the heuristic is terrible, then it can perform even worse than other algorithms that don't use any heuristic function such as
/// Dijkstra.
/// </summary>
/// <typeparam name="TNode">Type of node</typeparam>
public interface IHeuristic<TNode>
{
/// <summary>
/// 启发式算法估价函数,计算从当前点到目标点的估价
/// <para>A*启发函数的g(n)</para>
/// <para>Calculates an estimated cost to reach the goal node from the given node.</para>
/// </summary>
/// <param name="node">the start node</param>
/// <param name="endNode">the end node</param>
/// <returns>the estimated cost</returns>
LFloat Estimate(TNode node, TNode endNode);
}
}

View File

@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 736f27d5a74ce43f8923b8bc96b225b3
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -0,0 +1,30 @@
namespace JNGame.PathFinding
{
/// <summary>
/// 路径搜索器的接口定义
/// </summary>
/// <typeparam name="TNode">图数据结构的节点</typeparam>
public interface IPathFinder<TNode>
{
/// <summary>
/// 搜索从startNode到endNode的一条路径
/// </summary>
/// <param name="startNode">起始节点</param>
/// <param name="endNode">目标节点</param>
/// <param name="heuristic">启发函数</param>
/// <param name="outPath">输出结果路径:路径用图的邻接边描述</param>
/// <returns>是否成功搜索到一条路径</returns>
bool SearchPath(TNode startNode, TNode endNode, IHeuristic<TNode> heuristic, IGraphPath<IConnection<TNode>> outPath);
/// <summary>
/// 搜索从startNode到endNode的一条路径
/// </summary>
/// <param name="startNode">图中的起始节点</param>
/// <param name="endNode">图中的目标节点</param>
/// <param name="heuristic">启发函数</param>
/// <param name="outPath">输出结果路径:路径用图的节点描述</param>
/// <returns>是否成功搜索到一条路径</returns>
bool SearchNodePath(TNode startNode, TNode endNode, IHeuristic<TNode> heuristic, IGraphPath<TNode> outPath);
}
}

View File

@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 09b9a4d779c64410b8720c88bd5168e5
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -0,0 +1,366 @@
using System.Collections.Generic;
using JNGame.Math;
namespace JNGame.PathFinding
{
/// <summary>
/// 基于A*算法的路径搜索器
/// </summary>
/// <typeparam name="TNode">图的节点类型</typeparam>
public class IndexedAStarPathFinder<TNode> : IPathFinder<TNode>
{
/// <summary>
/// 节点搜索状态 - 未遍历
/// </summary>
public const int UNVISITED = 0;
/// <summary>
/// 节点搜索状态 - 处理OpenList中
/// </summary>
public const int OPEN = 1;
/// <summary>
/// 节点搜索状态 - 处理完成
/// </summary>
public const int CLOSED = 2;
/// <summary>
/// 寻路使用的数据源
/// <para>以TNode为节点构成的图数据结构包含TNode节点集合以及TNode的邻接关系集合</para>
/// </summary>
private readonly IIndexedGraph<TNode> m_Graph;
/// <summary>
/// 节点的搜索记录集合,数组长度为图的节点总数
/// </summary>
private readonly NodeRecord<TNode>[] m_NodeRecords;
/// <summary>
/// OpenList用二叉堆数据结构描述
/// </summary>
private readonly GenericBinaryHeap<NodeRecord<TNode>> m_OpenList;
/// <summary>
/// 搜索ID每次搜索自增用于标记节点搜索记录
/// The unique ID for each search run. Used to mark nodes.
/// </summary>
private int m_SearchId = 0;
/// <summary>
/// 当前搜索的节点
/// </summary>
private NodeRecord<TNode> m_Current;
/// <summary>
/// 性能评估指标与搜索过程无关记录A*的搜索次数,以此评估性能
/// </summary>
public Metrics metrics;
/// <summary>
/// 基于A*算法的路径搜索器构造
/// </summary>
/// <param name="graph">路径搜索器使用的数据源</param>
/// <param name="calculateMetrics">是否开启性能评估,默认:false</param>
public IndexedAStarPathFinder(IIndexedGraph<TNode> graph, bool calculateMetrics = false)
{
m_Graph = graph;
m_NodeRecords = new NodeRecord<TNode>[graph.GetNodeCount()];
m_OpenList = new GenericBinaryHeap<NodeRecord<TNode>>();
if (calculateMetrics)
{
metrics = new Metrics();
}
}
public bool SearchPath(TNode startNode, TNode endNode, IHeuristic<TNode> heuristic, IGraphPath<IConnection<TNode>> outPath)
{
// Perform AStar
bool found = Search(startNode, endNode, heuristic);
if (found)
{
// Create a path made of connections
GeneratePath(startNode, outPath);
}
return found;
}
public bool SearchNodePath(TNode startNode, TNode endNode, IHeuristic<TNode> heuristic, IGraphPath<TNode> outPath)
{
// Perform AStar
bool found = Search(startNode, endNode, heuristic);
if (found)
{
// Create a path made of nodes
GenerateNodePath(startNode, outPath);
}
return found;
}
/// <summary>
/// 开始执行路径搜索
/// </summary>
/// <param name="startNode">图上的起始节点</param>
/// <param name="endNode">图上的目标节点</param>
/// <param name="heuristic">启发函数</param>
/// <returns>是否存在路径</returns>
protected bool Search(TNode startNode, TNode endNode, IHeuristic<TNode> heuristic)
{
InitSearch(startNode, endNode, heuristic);
// Iterate through processing each node
do
{
// Retrieve the node with smallest estimated total cost from the open list
m_Current = m_OpenList.Pop();
m_Current.category = CLOSED;
// Terminate if we reached the goal node
if (m_Current.node.Equals(endNode)) { return true; }
// 遍历当前节点的邻接节点,选取评估值
VisitChildren(endNode, heuristic);
}
while (m_OpenList.size > 0);
// We've run out of nodes without finding the goal, so there's no solution
return false;
}
/// <summary>
/// 初始化路径搜索
/// </summary>
/// <param name="startNode">图上的起始节点</param>
/// <param name="endNode">图上的目标节点</param>
/// <param name="heuristic">启发函数</param>
protected void InitSearch(TNode startNode, TNode endNode, IHeuristic<TNode> heuristic)
{
// 清空性能指标
metrics?.Reset();
// 搜索ID自增
if (++m_SearchId < 0) { m_SearchId = 1; }
// 初始化OpenList
m_OpenList.Clear();
// Initialize the record for the start node and add it to the open list
NodeRecord<TNode> startRecord = GetNodeRecord(startNode);
startRecord.node = startNode;
startRecord.connection = null; // 起点的连接口设置为null
startRecord.costSoFar = 0.ToLFloat();
// 起始节点添加到OpenList中
AddToOpenList(startRecord, heuristic.Estimate(startNode, endNode));
m_Current = null;
}
/// <summary>
/// 遍历子节点
/// </summary>
/// <param name="endNode">路径的目标节点</param>
/// <param name="heuristic">启发函数</param>
protected void VisitChildren(TNode endNode, IHeuristic<TNode> heuristic)
{
// Get current node's outgoing connections
List<IConnection<TNode>> connections = m_Graph.GetConnections(m_Current.node);
// Loop through each connection in turn
for (int i = 0; i < connections.Count; i++)
{
if (metrics != null) { ++metrics.VisitedNodes; }
IConnection<TNode> connection = connections[i];
// Get the cost estimate for the node
TNode node = connection.GetToNode(); // 邻接边连接的目标节点
LFloat nodeCost = m_Current.costSoFar + connection.GetCost(); // 节点到起点的消耗(g(n))
LFloat nodeHeuristic;
NodeRecord<TNode> nodeRecord = GetNodeRecord(node);
if (nodeRecord.category == CLOSED)
{ // The node is closed
// If we didn't find a shorter route, skip 已经是消耗最小的目标点
if (nodeRecord.costSoFar <= nodeCost) { continue; }
// We can use the node's old cost values to calculate its heuristic
// without calling the possibly expensive heuristic function
nodeHeuristic = nodeRecord.GetEstimatedTotalCost() - nodeRecord.costSoFar;
}
else if (nodeRecord.category == OPEN)
{ // The node is open
// If our route is no better, then skip
if (nodeRecord.costSoFar <= nodeCost) { continue; }
// Remove it from the open list (it will be re-added with the new cost)
m_OpenList.Remove(nodeRecord);
// We can use the node's old cost values to calculate its heuristic
// without calling the possibly expensive heuristic function
nodeHeuristic = nodeRecord.GetEstimatedTotalCost() - nodeRecord.costSoFar;
}
else
{ // the node is unvisited
// We'll need to calculate the heuristic value using the function,
// since we don't have a node record with a previously calculated value
nodeHeuristic = heuristic.Estimate(node, endNode);
}
// Update node record's cost and connection
nodeRecord.costSoFar = nodeCost;
nodeRecord.connection = connection;
// Add it to the open list with the estimated total cost
AddToOpenList(nodeRecord, nodeCost + nodeHeuristic);
}
}
/// <summary>
/// 生成用图的邻接边描述的路径
/// </summary>
/// <param name="startNode">图起点</param>
/// <param name="outPath">输出的路径</param>
protected void GeneratePath(TNode startNode, IGraphPath<IConnection<TNode>> outPath)
{
// outPath.clear();
// Work back along the path, accumulating connections
while (!m_Current.node.Equals(startNode))
{ // 沿着变成搜索的连接口,搜索到起点
outPath.Add(m_Current.connection);
m_Current = m_NodeRecords[m_Graph.GetIndex(m_Current.connection.GetFromNode())];
}
// Reverse the path
outPath.Reverse();
}
/// <summary>
/// 生成用图的节点描述的路径
/// </summary>
/// <param name="startNode">图起点</param>
/// <param name="outPath">输出的路径</param>
protected void GenerateNodePath(TNode startNode, IGraphPath<TNode> outPath)
{
// outPath.clear();
// Work back along the path, accumulating nodes
while (m_Current.connection != null)
{ // 沿着变成搜索的连接口,搜索到空连接
outPath.Add(m_Current.node);
m_Current = m_NodeRecords[m_Graph.GetIndex(m_Current.connection.GetFromNode())];
}
// 额外添加起点
outPath.Add(startNode);
// Reverse the path
outPath.Reverse();
}
/// <summary>
/// 添加节点到OpenList节点搜索记录状态设置为Open
/// </summary>
/// <param name="nodeRecord">节点关联的搜索记录</param>
/// <param name="estimatedTotalCost">当前节点的到路径终点的总消耗(g(n)+h(n))</param>
protected void AddToOpenList(NodeRecord<TNode> nodeRecord, LFloat estimatedTotalCost)
{
m_OpenList.Add(nodeRecord, estimatedTotalCost);
// 节点搜索状态更新为OPEN
nodeRecord.category = OPEN;
if (metrics != null)
{
// 更新性能指标
++metrics.OpenListAdditions;
if (m_OpenList.size > metrics.OpenListPeak)
{
metrics.OpenListPeak = m_OpenList.size;
}
}
}
/// <summary>
/// 获取指定图节点的搜索记录
/// </summary>
/// <param name="node">图节点</param>
/// <returns>图节点关联的搜索记录</returns>
protected NodeRecord<TNode> GetNodeRecord(TNode node)
{
int index = m_Graph.GetIndex(node);
NodeRecord<TNode> nr = m_NodeRecords[index];
if (nr != null)
{
// 不同批次的搜索重置节点状态和关联的搜索ID
if (nr.searchId != m_SearchId)
{
nr.category = UNVISITED;
nr.searchId = m_SearchId;
}
return nr;
}
// 从未搜索过,创建新的节点搜索记录
nr = m_NodeRecords[index] = new NodeRecord<TNode>();
nr.node = node;
nr.searchId = m_SearchId;
return nr;
}
#region
/// <summary>
/// 图节点搜索记录
/// </summary>
/// <typeparam name="TN"></typeparam>
public class NodeRecord<TN> : BinaryHeapNode
{
/// <summary>
/// 关联的图节点
/// <para>The reference to the node.</para>
/// </summary>
public TN node;
/// <summary>
/// 本次搜索走的邻接口
/// <para>The incoming connection to the node.</para>
/// </summary>
public IConnection<TN> connection;
/// <summary>
/// 当前节点到起点的实际消耗A*启发函数的g(n)
/// <para>The actual cost from the start node.</para>
/// </summary>
public LFloat costSoFar;
/// <summary>
/// 节点的状态UNVISITED / OPEN / OPEN 三者之一
/// <para>The node category: {@link #UNVISITED}, {@link #OPEN} or {@link #OPEN}.</para>
/// </summary>
public int category;
/// <summary>
/// 关联的搜索ID
/// <para>ID of the current search.</para>
/// </summary>
public int searchId;
/// <summary>
/// Creates a {@code NodeRecord}.
/// </summary>
public NodeRecord() : base(LFloat.Zero) { }
/// <summary>
/// 获取当前节点总的消耗f(n) = g(n) + h(n)
/// </summary>
/// <returns>Returns the estimated total cost.</returns>
public LFloat GetEstimatedTotalCost()
{
return value;
}
}
#endregion
#region
/// <summary>
/// 寻路指标,评测寻路性能使用
/// </summary>
public class Metrics
{
/// <summary>
/// 遍历节点的次数
/// </summary>
public int VisitedNodes;
/// <summary>
/// OpenList添加的次数
/// </summary>
public int OpenListAdditions;
/// <summary>
/// OpenList中最大的待处理节点数
/// </summary>
public int OpenListPeak;
public Metrics() { }
public void Reset()
{
VisitedNodes = 0;
OpenListAdditions = 0;
OpenListPeak = 0;
}
}
#endregion
}
}

View File

@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 5243578b0e4974e358512ec1e0c2ae05
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -0,0 +1,317 @@
using System;
using System.Text;
using JNGame.Math;
namespace JNGame.PathFinding
{
/// <summary>
/// 二叉堆的节点基类
/// </summary>
public class BinaryHeapNode
{
/// <summary>
/// 节点排序比较值
/// </summary>
public LFloat value;
/// <summary>
/// 节点索引
/// </summary>
public int index;
public BinaryHeapNode(LFloat value)
{
this.value = value;
}
public override string ToString()
{
return value.ToString();
}
}
/// <summary>
/// 二叉堆数据结构
/// <para>常规二叉堆不从索引0开始而是从索引1开始</para>
/// <para>索引为n的左孩子索引是2n索引为n的右孩子索引是2n+1索引为n的父节点索引是n/2</para>
/// <para>本实现从索引0开始</para>
/// <para>本实现索引为n的左孩子索引是2n+1索引为n的右孩子索引是2(n+1)索引为n的父节点索引是(n-1)/2</para>
/// </summary>
/// <typeparam name="T">二叉堆中的数据节点</typeparam>
public class GenericBinaryHeap<T> where T : BinaryHeapNode
{
/// <summary>
/// 二叉堆中的有效节点总数,同时是当前的可插入位置
/// </summary>
public int size;
/// <summary>
/// 二叉堆节点集合,用数组存储
/// </summary>
private BinaryHeapNode[] m_Nodes;
/// <summary>
/// 是否是最大堆
/// <para>最大堆:任意节点权值不小于其子节点</para>
/// <para>最小堆:任意节点权值不大于于其子节点</para>
/// </summary>
private readonly bool m_IsMaxHeap;
/// <summary>
/// 二叉堆构造器
/// </summary>
public GenericBinaryHeap() : this(16, false) { }
/// <summary>
/// 二叉堆构造器
/// </summary>
/// <param name="capacity">初始容量</param>
/// <param name="isMaxHeap">是否是最大堆</param>
public GenericBinaryHeap(int capacity, bool isMaxHeap)
{
m_IsMaxHeap = isMaxHeap;
m_Nodes = new BinaryHeapNode[capacity];
}
/// <summary>
/// 添加节点到二叉堆
/// </summary>
/// <param name="node"></param>
/// <returns></returns>
public T Add(T node)
{
// Expand if necessary.
if (size == m_Nodes.Length)
{
BinaryHeapNode[] newNodes = new BinaryHeapNode[size << 1];
Array.Copy(m_Nodes, 0, newNodes, 0, size);
m_Nodes = newNodes;
}
// Insert at end.
node.index = size;
m_Nodes[size] = node;
// bubble up
Up(size++);
return node;
}
/// <summary>
/// 添加节点到二叉堆并设置节点的排序权重为value
/// </summary>
/// <param name="node">添加到二叉堆的节点</param>
/// <param name="value">节点的排序权重</param>
/// <returns></returns>
public T Add(T node, LFloat value)
{
node.value = value;
return Add(node);
}
/// <summary>
/// 获取二叉堆顶的节点
/// </summary>
/// <returns></returns>
/// <exception cref="Exception"></exception>
public T Peek()
{
if (size == 0)
throw new Exception("The heap is empty.");
return (T)m_Nodes[0];
}
/// <summary>
/// 弹出二叉堆顶的节点
/// </summary>
/// <returns></returns>
public T Pop()
{
return Remove(0);
}
/// <summary>
/// 弹出指定的二叉堆中节点
/// </summary>
/// <param name="node">移除的节点</param>
/// <returns></returns>
public T Remove(T node)
{
return Remove(node.index);
}
/// <summary>
/// 移除二叉堆指定索引位置的节点
/// </summary>
/// <param name="index">移除位置索引</param>
/// <returns></returns>
private T Remove(int index)
{
BinaryHeapNode[] nodes = m_Nodes;
BinaryHeapNode removed = nodes[index];
// 用最后一个位置的节点填充移除的位置
nodes[index] = nodes[--size];
nodes[size] = null;
if (size > 0 && index < size)
{
Down(index);
}
return (T)removed;
}
/// <summary>
/// 清空二叉堆
/// TODO 优化可以不用对nodes进行清零
/// </summary>
public void Clear()
{
for (int i = 0, n = size; i < n; ++i)
{
m_Nodes[i] = null;
}
size = 0;
}
/// <summary>
/// 修改二叉堆中一个节点的排序权重
/// </summary>
/// <param name="node">二叉堆中的一个节点</param>
/// <param name="value">新的排序值</param>
public void SetValue(T node, LFloat value)
{
LFloat oldValue = node.value;
node.value = value;
if ((value < oldValue) ^ m_IsMaxHeap)
{
Up(node.index);
}
else
{
Down(node.index);
}
}
/// <summary>
/// 上浮索引位置的节点,直至满足二叉堆性质
/// </summary>
/// <param name="index">上浮的节点索引</param>
private void Up(int index)
{
BinaryHeapNode[] nodes = this.m_Nodes;
BinaryHeapNode node = nodes[index];
LFloat value = node.value;
while (index > 0)
{
// 获取父节点
int parentIndex = (index - 1) >> 1;
BinaryHeapNode parent = nodes[parentIndex];
// 当前节点的权值与父节点比较,是否满足二叉堆性质(最大堆或最小堆)
if ((value < parent.value) ^ m_IsMaxHeap)
{ // 不满足,则将父节点的值写入到当前节点的位置,当前节点索引该为父节点索引
nodes[index] = parent;
parent.index = index;
index = parentIndex;
}
else
{
// 满足,退出上浮
break;
}
}
// 当前节点写入正确位置,更新节点的索引
nodes[index] = node;
node.index = index;
}
/// <summary>
/// 节点下沉,寻找符合二叉堆性质的
/// </summary>
/// <param name="index"></param>
private void Down(int index)
{
BinaryHeapNode[] nodes = m_Nodes;
int size = this.size;
BinaryHeapNode node = nodes[index];
LFloat value = node.value;
// 当前节点循环下沉,寻找符合二叉堆性质的索引位置
while (true)
{
// no child, finished
int leftIndex = 1 + (index << 1);
if (leftIndex >= size)
{
break;
}
// Always have a left child.
BinaryHeapNode leftNode = nodes[leftIndex];
LFloat leftValue = leftNode.value;
// May have a right child.
int rightIndex = leftIndex + 1;
BinaryHeapNode rightNode;
LFloat rightValue;
if (rightIndex >= size)
{
rightNode = null;
rightValue = m_IsMaxHeap ? LFloat.FLT_MIN : LFloat.FLT_MAX;
}
else
{
rightNode = nodes[rightIndex];
rightValue = rightNode.value;
}
// The smallest of the three values is the parent.
if ((leftValue < rightValue) ^ m_IsMaxHeap)
{ // 最大堆,左节点为两个子节点的大值 || 最小堆,左节点为两个子节点的小值
if (leftValue == value || ((leftValue > value) ^ m_IsMaxHeap))
{ // 左节点与当前节点(父节点)权值相同,或 最大堆:左节点为权值小的 | 最小堆:左节点为权值大的。退出,下沉完成
break;
}
// 最小堆:左节点为权值最小的 || 最大堆:左节点为权值最大的,交换当前(父)节点与左节点的位置
nodes[index] = leftNode;
leftNode.index = index;
index = leftIndex;
}
else
{ // 最大堆,右节点为两个子节点的大值 || 最小堆,右节点为两个子节点的小值
if (rightValue == value || ((rightValue > value) ^ m_IsMaxHeap))
{ // 右节点与当前节点(父节点)权值相同,或 最大堆右节点为权值小的 | 最小堆:右节点为权值大的。退出,下沉完成
break;
}
// 最小堆:右节点为权值最小的 || 最大堆:右节点为权值最大的,交换当前(父)节点与右节点的位置
nodes[index] = rightNode;
rightNode.index = index;
index = rightIndex;
}
}
// 更新当前节点到最终的索引位置
nodes[index] = node;
node.index = index;
}
public override int GetHashCode()
{
long h = 1;
for (int i = 0, n = size; i < n; i++)
{
h = h * 31 + m_Nodes[i].value.rawValue;
}
return (int)h;
}
public override string ToString()
{
if (size == 0)
{
return "[]";
}
BinaryHeapNode[] nodes = this.m_Nodes;
StringBuilder buffer = new StringBuilder(32);
buffer.Append('[');
buffer.Append(nodes[0].value);
for (int i = 1; i < size; i++)
{
buffer.Append(", ");
buffer.Append(nodes[i].value);
}
buffer.Append(']');
return buffer.ToString();
}
}
}

View File

@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 024aed145f1c9421faa2dcf00949e00d
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -0,0 +1,38 @@
using JNGame.Math;
namespace JNGame.PathFinding
{
/// <summary>
/// 描述以三角形为图元节点的图上,从起点三角形到终点三角形的一条图上的路径。
/// <para>路径描节点用图的邻接边描述</para>
/// </summary>
public class TriangleEdgeGraphPath : DefaultGraphPath<IConnection<NavTriangle>>
{
/// <summary>
/// 路径的起点
/// <para>The start point when generating a point path for this triangle path</para>
/// </summary>
public LVector3 startPoint;
/// <summary>
/// 路径的终点
/// <para>The end point when generating a point path for this triangle path</para>
/// </summary>
public LVector3 endPoint;
/// <summary>
/// 路径的起始三角形
/// </summary>
public NavTriangle startTriangle;
/// <summary>
/// 获取路径上的最后一个三角形
/// </summary>
/// <returns>Last triangle in the path.</returns>
public NavTriangle GetEndTriangle()
{
// If the triangle path is empty, the point path will span this triangle
return (GetCount() > 0) ? Get(GetCount() - 1).GetToNode() : startTriangle;
}
}
}

View File

@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 274f209b145dc49a0b89a286ea968a06
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -0,0 +1,97 @@
using JNGame.Math;
namespace JNGame.PathFinding
{
/// <summary>
/// 三角形节点寻路启发式算法
/// </summary>
public class TriangleHeuristic : IHeuristic<NavTriangle>
{
/// <summary>
/// 三角形A的AB边中点
/// </summary>
private LVector3 m_MidPointAB_A = LVector3.Zero;
/// <summary>
/// 三角形A的BC边中点
/// </summary>
private LVector3 m_MidPointBC_A = LVector3.Zero;
/// <summary>
/// 三角形A的CA边中点
/// </summary>
private LVector3 m_MidPointCA_A = LVector3.Zero;
/// <summary>
/// 三角形B的AB边中点
/// </summary>
private LVector3 m_MidPointAB_B = LVector3.Zero;
/// <summary>
/// 三角形B的BC边中点
/// </summary>
private LVector3 m_MidPointBC_B = LVector3.Zero;
/// <summary>
/// 三角形B的CA边中点
/// </summary>
private LVector3 m_MidPointCA_B = LVector3.Zero;
/// <summary>
/// 启发式算法估价函数:两个三角形节点三边中点的距离,获取最短的距离
/// <para>A*启发函数的g(n)</para>
/// </summary>
/// <param name="node">当前节点</param>
/// <param name="endNode">目标节点</param>
/// <returns></returns>
public LFloat Estimate(NavTriangle node, NavTriangle endNode)
{
LFloat minDst2 = LFloat.FLT_MAX;
// 三角形A的三边中点
m_MidPointAB_A = (node.vertexA + node.vertexB) * LFloat.Half;
m_MidPointBC_A = (node.vertexB + node.vertexC) * LFloat.Half;
m_MidPointCA_A = (node.vertexC + node.vertexA) * LFloat.Half;
// 三角形B的三边中点
m_MidPointAB_B = (endNode.vertexA + endNode.vertexB) * LFloat.Half;
m_MidPointBC_B = (endNode.vertexB + endNode.vertexC) * LFloat.Half;
m_MidPointCA_B = (endNode.vertexC + endNode.vertexA) * LFloat.Half;
LFloat dst2;
// 三角形A的边AB中点到三角形B的三边中点的距离平方
if ((dst2 = LVector3.Distance2(m_MidPointAB_A, m_MidPointAB_B)) < minDst2)
{
minDst2 = dst2;
}
if ((dst2 = LVector3.Distance2(m_MidPointAB_A, m_MidPointBC_B)) < minDst2)
{
minDst2 = dst2;
}
if ((dst2 = LVector3.Distance2(m_MidPointAB_A, m_MidPointCA_B)) < minDst2)
{
minDst2 = dst2;
}
// 三角形A的边BC中点到三角形B的三边中点的距离平方
if ((dst2 = LVector3.Distance2(m_MidPointBC_A, m_MidPointAB_B)) < minDst2)
{
minDst2 = dst2;
}
if ((dst2 = LVector3.Distance2(m_MidPointBC_A, m_MidPointBC_B)) < minDst2)
{
minDst2 = dst2;
}
if ((dst2 = LVector3.Distance2(m_MidPointBC_A, m_MidPointCA_B)) < minDst2)
{
minDst2 = dst2;
}
// 三角形A的边CA中点到三角形B的三边中点的距离平方
if ((dst2 = LVector3.Distance2(m_MidPointCA_A, m_MidPointAB_B)) < minDst2)
{
minDst2 = dst2;
}
if ((dst2 = LVector3.Distance2(m_MidPointCA_A, m_MidPointBC_B)) < minDst2)
{
minDst2 = dst2;
}
if ((dst2 = LVector3.Distance2(m_MidPointCA_A, m_MidPointCA_B)) < minDst2)
{
minDst2 = dst2;
}
// 返回最短距离
return LMath.Sqrt(minDst2);
}
}
}

View File

@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 29ee51a11a00042b6bcd827f0d2d634f
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -0,0 +1,476 @@
using System.Collections.Generic;
using JNGame.Math;
namespace JNGame.PathFinding
{
/// <summary>
/// 位置平面关系枚举
/// </summary>
public enum EnumPlaneSide
{
/// <summary>
/// 点在平面上
/// </summary>
OnPlane,
/// <summary>
/// 点在平面背面
/// </summary>
Back,
/// <summary>
/// 点在平面正面
/// </summary>
Front
}
/// <summary>
/// 将图节点路径转换为一条最短路径
/// <para>漏斗算法</para>
/// </summary>
public class TrianglePointPath
{
private readonly Plane m_CrossingPlane = new Plane(); // 横跨平面
/// <summary>
/// 用图边描述的路径
/// </summary>
private List<IConnection<NavTriangle>> m_EdgePaths; // 路径连接点
/// <summary>
/// 路径起点
/// </summary>
private LVector3 m_Start; // 起点
/// <summary>
/// 路径终点
/// </summary>
private LVector3 m_End; // 终点
/// <summary>
/// 起始三角形
/// </summary>
private NavTriangle m_StartTri; // 起始三角形
/// <summary>
/// 上次添加的边点
/// </summary>
private EdgePoint m_LastPointAdded;
/// <summary>
/// 最终结果路径坐标点
/// </summary>
private readonly List<LVector3> m_Vectors = new List<LVector3>();
/// <summary>
/// 最终结果边点描述
/// </summary>
private readonly List<EdgePoint> m_PathPoints = new List<EdgePoint>();
/// <summary>
/// 路径计算的中间射线对象,每次计算清理并复用
/// </summary>
private readonly Ray m_TempRay = new Ray();
/// <summary>
/// 路径计算的中间漏斗对象,每次计算清理并复用
/// </summary>
private readonly Funnel m_TempFunnel = new Funnel();
/// <summary>
/// 路径计算的最后一个边,每次计算清理并复用
/// </summary>
private TriangleEdge m_TempLastEdge = new TriangleEdge();
/// <summary>
/// EdgePoint临时对象队列的初始数量
/// </summary>
private const int kDefaultEdgePointCount = 16;
/// <summary>
/// EdgePoint的临时对象缓存队列作为对象池复用
/// </summary>
private readonly Queue<EdgePoint> m_TempEdgePointQueue = new Queue<EdgePoint>(kDefaultEdgePointCount);
public TrianglePointPath()
{
// 初始化一些EdgePoint的临时对象
for (int i = 0; i < kDefaultEdgePointCount; ++i)
{
m_TempEdgePointQueue.Enqueue(new EdgePoint());
}
}
public void CalculateForGraphPath(TriangleEdgeGraphPath trianglePath)
{
Clear();
m_EdgePaths = trianglePath.nodes;
m_Start = trianglePath.startPoint;
m_End = trianglePath.endPoint;
m_StartTri = trianglePath.startTriangle;
for (int i = 0; i < m_EdgePaths.Count; ++i)
{
IConnection<NavTriangle> connection = m_EdgePaths[i];
NavTriangle to = connection.GetToNode();
m_Vectors.Add(to.center);
}
m_Vectors.Add(m_End);
}
/// <summary>
/// 通过图元中间路径转化为点路径
/// </summary>
/// <param name="trianglePath"></param>
/// <param name="calculateCrossPoint"></param>
public void CalculateForGraphPath(TriangleEdgeGraphPath trianglePath, bool calculateCrossPoint)
{
Clear();
m_EdgePaths = trianglePath.nodes;
m_Start = trianglePath.startPoint;
m_End = trianglePath.endPoint;
m_StartTri = trianglePath.startTriangle;
// Check that the start point is actually inside the start triangle,
// if not, project it to the closest triangle edge.
// Otherwise the funnel calculation might generate spurious path segments.
LVector3 rayOrigin = m_Start + LVector3.Up * 1000;
// Ray ray = new Ray(rayOrigin, LVector3.Down);
// 起始坐标从上向下的射线
m_TempRay.origin = rayOrigin;
m_TempRay.direction = LVector3.Down;
if (!GeometryUtil.IntersectRayTriangle(m_TempRay, m_StartTri.vertexA, m_StartTri.vertexB, m_StartTri.vertexC, out var ss))
{
LFloat minDst = LFloat.FLT_MAX;
LVector3 projection = new LVector3(); // 规划坐标
LVector3 newStart = new LVector3(); // 新坐标
LFloat dst;
// A-B
if ((dst = GeometryUtil.NearestSegmentPointSquareDistance(ref projection, m_StartTri.vertexA, m_StartTri.vertexB,
m_Start)) < minDst)
{
minDst = dst;
newStart = projection;
}
// B-C
if ((dst = GeometryUtil.NearestSegmentPointSquareDistance(ref projection, m_StartTri.vertexB, m_StartTri.vertexC,
m_Start)) < minDst)
{
minDst = dst;
newStart = projection;
}
// C-A
if ((dst = GeometryUtil.NearestSegmentPointSquareDistance(ref projection, m_StartTri.vertexC, m_StartTri.vertexA,
m_Start)) < minDst)
{
minDst = dst;
newStart = projection;
}
m_Start = newStart;
}
if (m_EdgePaths.Count == 0)
{ // 起点终点在同一三角形中
AddPoint(m_Start, m_StartTri);
AddPoint(m_End, m_StartTri);
}
else
{
m_TempLastEdge.Set(m_EdgePaths[^1].GetToNode(), m_EdgePaths[^1].GetToNode(), m_End, m_End);
CalculateEdgePoints(calculateCrossPoint);
}
}
public void Clear()
{
m_TempFunnel.Reset();
m_TempRay.Reset();
m_Vectors.Clear();
// Debug.Log("RemainCount: {0}", m_TempEdgePointQueue.Count);
for (int i = m_PathPoints.Count - 1; i >= 0; --i)
{
RecycleEdgePoint(m_PathPoints[i]);
}
// Debug.Log("TotalCount: {0}", m_TempEdgePointQueue.Count);
m_PathPoints.Clear();
m_StartTri = null;
m_LastPointAdded = null;
m_TempLastEdge.Clear();
}
private TriangleEdge GetEdge(int index)
{
return (TriangleEdge)((index == m_EdgePaths.Count) ? m_TempLastEdge : m_EdgePaths[index]);
}
private int NumEdges()
{
return m_EdgePaths.Count + 1;
}
public LVector3 GetVector(int index)
{
return m_Vectors[index];
}
public int GetSize()
{
return m_Vectors.Count;
}
/** All vectors in the path */
public List<LVector3> GetVectors()
{
return m_Vectors;
}
/** The triangle which must be crossed to reach the next path point.*/
public NavTriangle GetToTriangle(int index)
{
return m_PathPoints[index].toNode;
}
/** The triangle from which must be crossed to reach this point. */
public NavTriangle GetFromTriangle(int index)
{
return m_PathPoints[index].fromNode;
}
/** The navmesh edge(s) crossed at this path point. */
public List<TriangleEdge> GetCrossedEdges(int index)
{
return m_PathPoints[index].connectingEdges;
}
private void AddPoint(in LVector3 point, NavTriangle toNode)
{
// AddPoint(new EdgePoint(point, toNode));
AddPoint(GetFreeEdgePoint(in point, toNode));
}
private void AddPoint(EdgePoint edgePoint)
{
m_Vectors.Add(edgePoint.point);
m_PathPoints.Add(edgePoint);
m_LastPointAdded = edgePoint;
}
/// <summary>
/// 基于漏斗算法,将图的边路径转换为最短点路径
/// <para>point path through the path triangles, using the Simple Stupid Funnel Algorithm.</para>
/// </summary>
/// <param name="calculateCrossPoint"></param>
private void CalculateEdgePoints(bool calculateCrossPoint)
{
TriangleEdge edge = GetEdge(0);
AddPoint(m_Start, edge.fromNode);
m_LastPointAdded.fromNode = edge.fromNode;
// 初始化第一个漏斗
m_TempFunnel.pivot = m_Start;
m_TempFunnel.SetPlanes(m_TempFunnel.pivot, edge);// 路径起点为漏斗点,设置共享边的两个端点为漏斗边界平面
int leftIndex = 0; // 左顶点索引
int rightIndex = 0; // 右顶点索引
int lastRestart = 0;
for (int i = 1; i < NumEdges(); ++i)
{
edge = GetEdge(i); // 下一条边
// 边的两个端点与漏斗边平面A的位置关系
var leftPlaneLeftDP = m_TempFunnel.SideLeftPlane(edge.vertexB);
var leftPlaneRightDP = m_TempFunnel.SideLeftPlane(edge.vertexA);
// 边的两个端点与漏斗边平面B的位置关系
var rightPlaneLeftDP = m_TempFunnel.SideRightPlane(edge.vertexB);
var rightPlaneRightDP = m_TempFunnel.SideRightPlane(edge.vertexA);
// 右顶点在右平面里面
if (rightPlaneRightDP != EnumPlaneSide.Front)
{
// 右顶点在左平面里面
if (leftPlaneRightDP != EnumPlaneSide.Front)
{
// Tighten the funnel. 缩小漏斗
m_TempFunnel.SetRightPlane(m_TempFunnel.pivot, edge.vertexA);
rightIndex = i;
}
else
{
// Right over left, insert left to path and restart scan from portal left point.
// 右顶点在左平面外面,设置左顶点为漏斗顶点和路径点,从新已该漏斗开始扫描
if (calculateCrossPoint)
{
CalculateEdgeCrossings(lastRestart, leftIndex, m_TempFunnel.pivot, m_TempFunnel.leftPortal);
}
else
{
m_Vectors.Add(m_TempFunnel.leftPortal);
}
m_TempFunnel.pivot = m_TempFunnel.leftPortal;
i = leftIndex;
rightIndex = i;
if (i < NumEdges() - 1)
{
lastRestart = i;
m_TempFunnel.SetPlanes(m_TempFunnel.pivot, GetEdge(i + 1));
continue;
}
break;
}
}
// 左顶点在左平面里面
if (leftPlaneLeftDP != EnumPlaneSide.Front)
{
// 左顶点在右平面里面
if (rightPlaneLeftDP != EnumPlaneSide.Front)
{
// Tighten the funnel.
m_TempFunnel.SetLeftPlane(m_TempFunnel.pivot, edge.vertexB);
leftIndex = i;
}
else
{
// Left over right, insert right to path and restart scan from portal right point.
if (calculateCrossPoint)
{
CalculateEdgeCrossings(lastRestart, rightIndex, m_TempFunnel.pivot, m_TempFunnel.rightPortal);
}
else
{
m_Vectors.Add(m_TempFunnel.rightPortal);
}
m_TempFunnel.pivot = m_TempFunnel.rightPortal;
i = rightIndex;
leftIndex = i;
if (i < NumEdges() - 1)
{
lastRestart = i;
m_TempFunnel.SetPlanes(m_TempFunnel.pivot, GetEdge(i + 1));
continue;
}
break;
}
}
}
if (calculateCrossPoint)
{
CalculateEdgeCrossings(lastRestart, NumEdges() - 1, m_TempFunnel.pivot, m_End);
}
else
{
m_Vectors.Add(m_End);
}
for (int i = 1; i < m_PathPoints.Count; i++)
{
EdgePoint p = m_PathPoints[i];
p.fromNode = m_PathPoints[i - 1].toNode;
}
return;
}
/**
* Store all edge crossing points between the start and end indices. If the path
* crosses exactly the start or end points (which is quite likely), store the
* edges in order of crossing in the EdgePoint data structure.
* <p/>
* Edge crossings are calculated as intersections with the plane from the start,
* end and up vectors.
*/
private void CalculateEdgeCrossings(int startIndex, int endIndex, LVector3 startPoint, LVector3 endPoint)
{
if (startIndex >= NumEdges() || endIndex >= NumEdges())
{
return;
}
m_CrossingPlane.Set(startPoint, startPoint + LVector3.Up, endPoint);
EdgePoint previousLast = m_LastPointAdded;
var edge = GetEdge(endIndex);
// EdgePoint end = new EdgePoint(endPoint, edge.toNode);
EdgePoint end = GetFreeEdgePoint(in endPoint, edge.toNode);
for (int i = startIndex; i < endIndex; i++)
{
edge = GetEdge(i);
if (edge.vertexA.Equals(startPoint) || edge.vertexB.Equals(startPoint))
{
previousLast.toNode = edge.toNode;
if (!previousLast.connectingEdges.Contains(edge))
{
previousLast.connectingEdges.Add(edge);
}
}
else if (edge.vertexB.Equals(endPoint) || edge.vertexA.Equals(endPoint))
{
if (!end.connectingEdges.Contains(edge))
{
end.connectingEdges.Add(edge);
}
}
else if (IntersectSegmentPlane(edge.vertexB, edge.vertexA, m_CrossingPlane, out LVector3 intersection))
{
if (i != startIndex || i == 0)
{
m_LastPointAdded.toNode = edge.fromNode;
// EdgePoint crossing = new EdgePoint(intersection, edge.toNode);
EdgePoint crossing = GetFreeEdgePoint(in intersection, edge.toNode);
crossing.connectingEdges.Add(edge);
AddPoint(crossing);
}
}
}
if (endIndex < NumEdges() - 1)
{
end.connectingEdges.Add(GetEdge(endIndex));
}
if (!m_LastPointAdded.Equals(end))
{
AddPoint(end);
}
}
public static bool IntersectSegmentPlane(LVector3 start, LVector3 end, Plane plane, out LVector3 intersection)
{
intersection = LVector3.Zero;
LVector3 dir = end - start;
LFloat denom = LVector3.Dot(dir, plane.normal);
LFloat t = -(LVector3.Dot(start, plane.normal) + plane.distFromOrigin) / denom;
if (t < 0 || t > 1)
{
return false;
}
intersection = start + dir * t;
return true;
}
/// <summary>
/// 从临时队列中获取缓存的EdgePoint对象并且初始化
/// </summary>
/// <param name="point"></param>
/// <param name="toNode"></param>
/// <returns></returns>
private EdgePoint GetFreeEdgePoint(in LVector3 point, NavTriangle toNode)
{
if (m_TempEdgePointQueue.Count == 0)
{
return new EdgePoint(in point, toNode);
}
EdgePoint edgePoint = m_TempEdgePointQueue.Dequeue();
edgePoint.Set(in point, toNode);
return edgePoint;
}
/// <summary>
/// 回收edgePoint临时对象以备复用
/// </summary>
/// <param name="edgePoint"></param>
private void RecycleEdgePoint(EdgePoint edgePoint)
{
if (edgePoint == null)
{
return;
}
m_TempEdgePointQueue.Enqueue(edgePoint);
}
}
}

View File

@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 8f226701410f4424d8ac316660c1ccb0
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -0,0 +1,3 @@
fileFormatVersion: 2
guid: 8405f7862a7145bc96b9cbc73bf9853c
timeCreated: 1715335241

View File

@@ -0,0 +1,214 @@
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using DotRecast.Core.Numerics;
using DotRecast.Detour;
using DotRecast.Recast.Geom;
using DotRecast.Recast.Toolset;
using DotRecast.Recast.Toolset.Builder;
using JNGame.Map.DotRecast.Util;
using JNGame.Math;
using UnityEngine;
using UnityEngine.AI;
namespace JNGame.Map.DotRecast
{
public class DotRecastController : MonoBehaviour
{
public LayerMask layerMask;
private Mesh debugMesh;
private DtNavMesh dtNavMesh;
private List<Mesh> debugPathMeshs;
public void Save()
{
if (debugMesh is null)
{
Debug.Log("[DotRecast] 未生成地形 请生成地形.");
return;
}
var data = new MeshData(debugMesh);
File.WriteAllText(UnityEngine.Application.dataPath + "/Resources/map1.json", JsonUtility.ToJson(data));
Debug.Log("[DotRecast] 保存成功.");
}
public void Load()
{
debugMesh = this.Merge();
var data = new MeshData(debugMesh);
var vertices = new LFloat[data.vertices.Length * 3];
for (int i = 0; i < data.vertices.Length; ++i)
{
vertices[i * 3 + 0] = (LFloat)data.vertices[i].x;
vertices[i * 3 + 1] = (LFloat)data.vertices[i].y;
vertices[i * 3 + 2] = (LFloat)data.vertices[i].z;
}
var bmin = RcVecUtils.Create(vertices);
var bmax = RcVecUtils.Create(vertices);
for (int i = 1; i < vertices.Length / 3; i++)
{
bmin = RcVecUtils.Min(bmin, vertices, i * 3);
bmax = RcVecUtils.Max(bmax, vertices, i * 3);
}
var geom = new DefaultInputGeomProvider(vertices,data.triangles);
var builder = new TileNavMeshBuilder();
var settings = new RcNavMeshBuildSettings();
settings.cellSize = (LFloat)1f;
var result = builder.Build(geom, settings);
dtNavMesh = result.NavMesh;
debugPathMeshs = new List<Mesh>();
for (int i = 0; i < dtNavMesh.GetMaxTiles(); ++i)
{
DtMeshTile tile = dtNavMesh.GetTile(i);
if (tile != null && tile.data != null)
{
DrawMeshTile(tile);
}
}
Debug.Log("[DotRecast] 加载完成.");
}
private void DrawMeshTile(DtMeshTile tile)
{
List<Vector3> vertices = new List<Vector3>();
//Begin(DebugDrawPrimitives.TRIS);
for (int i = 0; i < tile.data.header.polyCount; ++i)
{
DtPoly p = tile.data.polys[i];
if (p.GetPolyType() == DtPolyTypes.DT_POLYTYPE_OFFMESH_CONNECTION)
{
continue;
}
if (tile.data.detailMeshes != null)
{
DtPolyDetail pd = tile.data.detailMeshes[i];
for (int j = 0; j < pd.triCount; ++j)
{
int t = (pd.triBase + j) * 4;
for (int k = 0; k < 3; ++k)
{
int v = tile.data.detailTris[t + k];
if (v < p.vertCount)
{
vertices.Add(new Vector3(tile.data.verts[p.verts[v] * 3], tile.data.verts[p.verts[v] * 3 + 1] + 0.1f,
tile.data.verts[p.verts[v] * 3 + 2]));
}
else
{
vertices.Add(new Vector3(tile.data.detailVerts[(pd.vertBase + v - p.vertCount) * 3],
tile.data.detailVerts[(pd.vertBase + v - p.vertCount) * 3 + 1],
tile.data.detailVerts[(pd.vertBase + v - p.vertCount) * 3 + 2]));
}
}
}
}
}
List<Vector3[]> vertices3 = new List<Vector3[]>();
for (int i = 0; i < vertices.Count; i += 3)
{
vertices3.Add(new []{vertices[i], vertices[i + 1], vertices[i + 2]});
}
int triCount = vertices3.Count;
Vector3[] verts = new Vector3[3 * triCount];
int[] tris = new int[3* triCount];
Color[] colors = new Color[3*triCount];
for (int i=0;i<triCount;++i){
int v = i*3;
for (int j=0;j<3;++j){
verts[v+j] = vertices3[i][j];
tris[v+j] = v+j;
colors[v+j] = Color.white;
}
}
Mesh mesh1 = new Mesh();
mesh1.vertices = verts;
mesh1.triangles = tris;
mesh1.colors = colors;
mesh1.RecalculateNormals();
debugPathMeshs.Add(mesh1);
}
private Mesh Merge()
{
//获取所有子物体的网格过滤器组件
MeshFilter[] meshFilters = FindObjectsOfType<MeshFilter>(true).Where(mf => ((1 << mf.gameObject.layer) & layerMask) > 0).ToArray();
//创建一个新的合并后的网格
Mesh mergedMesh = new Mesh();
//创建一个合并用的网格数组
CombineInstance[] combine = new CombineInstance[meshFilters.Length+1];
for (int i = 0; i < meshFilters.Length; i++)
{
//设置合并用的网格数组
combine[i].mesh = meshFilters[i].sharedMesh;
combine[i].transform = meshFilters[i].transform.localToWorldMatrix;
}
var triangulation = NavMesh.CalculateTriangulation();
combine[^1].mesh = new Mesh()
{
vertices = triangulation.vertices,
triangles = triangulation.indices,
};
combine[^1].transform = Matrix4x4.identity;
//合并网格
mergedMesh.CombineMeshes(combine);
mergedMesh.RecalculateNormals();
mergedMesh.RecalculateBounds();
debugPathMeshs = null;
return mergedMesh;
}
public void OnDrawGizmos()
{
// if (debugMesh is not null)
// {
// Gizmos.color = new Color(0f, 1f, 1f, 0.5f);
// Gizmos.DrawMesh(debugMesh);
// Gizmos.color = new Color(0f, 0f, 0f, 0.3f);
// Gizmos.DrawWireMesh(debugMesh);
// }
//
// return;
if (debugPathMeshs is not null)
{
foreach (var mesh in debugPathMeshs)
{
Gizmos.color = new Color(0f, 1f, 1f, 0.5f);
Gizmos.DrawMesh(mesh);
}
}
}
}
}

View File

@@ -0,0 +1,3 @@
fileFormatVersion: 2
guid: f83204b2c7bd45b086634b5819450152
timeCreated: 1715336022

View File

@@ -0,0 +1,191 @@
using System.Collections.Generic;
using DotRecast.Core.Numerics;
using DotRecast.Detour;
using DotRecast.Detour.Crowd;
using DotRecast.Recast.Geom;
using DotRecast.Recast.Toolset;
using DotRecast.Recast.Toolset.Builder;
using JNGame.Map.DotRecast.Util;
using JNGame.Math;
namespace JNGame.Map.DotRecast
{
public class DotRecastRoot
{
//导航数据
public DtNavMesh NavMesh;
//导航查询
public DtNavMeshQuery Query { private set; get; }
//智能体避障
public DtCrowd Crowd { private set; get; }
private Dictionary<ulong, DtCrowdAgent> agents;
public DotRecastRoot(MeshData data)
{
agents = new Dictionary<ulong, DtCrowdAgent>();
//初始化导航数据
InitNavMesh(data);
//初始化避障数据
InitCrowd(data);
}
//更新
public void Update(LFloat dt)
{
Crowd.Update(dt,null);
}
public DtCrowdAgentParams GetAgentParams()
{
int updateFlags = DtCrowdAgentUpdateFlags.DT_CROWD_ANTICIPATE_TURNS |
DtCrowdAgentUpdateFlags.DT_CROWD_OPTIMIZE_VIS |
DtCrowdAgentUpdateFlags.DT_CROWD_OPTIMIZE_TOPO |
DtCrowdAgentUpdateFlags.DT_CROWD_OBSTACLE_AVOIDANCE;
DtCrowdAgentParams ap = new DtCrowdAgentParams();
ap.radius = (LFloat)0.6f;
ap.height = (LFloat)2f;
ap.maxAcceleration = (LFloat)8.0f;
ap.maxSpeed = (LFloat)6f;
ap.collisionQueryRange = ap.radius * (LFloat)12f;
ap.pathOptimizationRange = ap.radius * (LFloat)30f;
ap.updateFlags = updateFlags;
ap.obstacleAvoidanceType = 0;
ap.separationWeight = (LFloat)2f;
return ap;
}
//初始化导航
private void InitNavMesh(MeshData data)
{
var vertices = new LFloat[data.vertices.Length * 3];
for (int i = 0; i < data.vertices.Length; ++i)
{
vertices[i * 3 + 0] = data.vertices[i].x;
vertices[i * 3 + 1] = data.vertices[i].y;
vertices[i * 3 + 2] = data.vertices[i].z;
}
var bmin = RcVecUtils.Create(vertices);
var bmax = RcVecUtils.Create(vertices);
for (int i = 1; i < vertices.Length / 3; i++)
{
bmin = RcVecUtils.Min(bmin, vertices, i * 3);
bmax = RcVecUtils.Max(bmax, vertices, i * 3);
}
var geom = new DefaultInputGeomProvider(vertices,data.triangles);
var builder = new TileNavMeshBuilder();
var settings = new RcNavMeshBuildSettings();
settings.cellSize = (LFloat)1f;
var result = builder.Build(geom, settings);
NavMesh = result.NavMesh;
Query = new DtNavMeshQuery(NavMesh);
}
//初始化避障
private void InitCrowd(MeshData data)
{
DtCrowdConfig config = new DtCrowdConfig((LFloat)0.6f);
Crowd = new DtCrowd(config, NavMesh);
DtObstacleAvoidanceParams option = new DtObstacleAvoidanceParams();
option.velBias = (LFloat)0.5f;
option.adaptiveDivs = 5;
option.adaptiveRings = 2;
option.adaptiveDepth = 1;
Crowd.SetObstacleAvoidanceParams(0, option);
}
//添加避障
public void AddAgent(ulong id,RcVec3f start)
{
DelAgent(id);
agents.Add(id,Crowd.AddAgent(start, GetAgentParams()));
}
public void AddAgent(ulong id,RcVec3f start,DtCrowdAgentParams agentParams)
{
DelAgent(id);
agents.Add(id,Crowd.AddAgent(start, agentParams));
}
//添加避障
public void DelAgent(ulong id)
{
if (agents.ContainsKey(id))
{
Crowd.RemoveAgent(agents[id]);
agents.Remove(id);
}
}
//移动避障
public void MoveAgent(ulong id,RcVec3f move)
{
DtCrowdAgent agent = agents[id];
Query.FindNearestPoly(move, Crowd.GetQueryExtents(), Crowd.GetFilter(0), out var nearestRef, out var nearestPt, out var _);
Crowd.RequestMoveTarget(agent, nearestRef, nearestPt);
}
//向量移动避障
public void VectorMoveAgent(ulong id,RcVec3f vector)
{
DtCrowdAgent agent = agents[id];
Crowd.RequestMoveVelocity(agent,vector);
}
//获取避障
public DtCrowdAgent GetAgent(ulong id)
{
agents.TryGetValue(id, out var value);
return value;
}
/// <summary>
/// 查询导航
/// </summary>
/// <param name="pos"></param>
/// <returns></returns>
public List<DtStraightPath> FindPath(LVector3 start,LVector3 end)
{
var start1 = new RcVec3f(start.x,start.y,start.z);
var end1 = new RcVec3f(end.x,end.y,end.z);
var half = new RcVec3f(1000, 1000, 1000);
var filter = new DtQueryDefaultFilter();
Query.FindNearestPoly(start1, half, filter, out var startRef, out var startPt, out var _);
Query.FindNearestPoly(end1, half, filter, out var endRef, out var endPt, out var _);
var path = new List<long>();
DtStatus status = Query.FindPath(startRef,endRef,startPt, endPt, filter,ref path, DtFindPathOption.AnyAngle);
var paths = new List<DtStraightPath>();
Query.FindStraightPath(startPt, endPt, path, path.Count, ref paths, 256, 0);
return paths;
}
/// <summary>
/// 返回最近点
/// </summary>
/// <param name="pos"></param>
/// <returns></returns>
public LVector3 GetNearbyPoints(LVector3 pos)
{
DtStatus status = Query.FindNearestPoly(
new RcVec3f(pos.x, pos.y, pos.z),
new RcVec3f(1000, 1000, 1000),
new DtQueryDefaultFilter(),
out var refs,
out var nearestPt,
out var _);
return new LVector3((LFloat)nearestPt.X, (LFloat)nearestPt.Y, (LFloat)nearestPt.Z);
}
}
}

View File

@@ -0,0 +1,3 @@
fileFormatVersion: 2
guid: dfd8873ca1a84305b83bc38ebe5eb1f0
timeCreated: 1715335565

View File

@@ -0,0 +1,8 @@
fileFormatVersion: 2
guid: 62ef7a1f4db447008e8974609a74ce24
folderAsset: yes
DefaultImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -0,0 +1,3 @@
fileFormatVersion: 2
guid: e0546f91227845db8711cbd928005e6b
timeCreated: 1715335323

View File

@@ -0,0 +1,3 @@
fileFormatVersion: 2
guid: 109919afddd1401d9087b46e76532e90
timeCreated: 1715335324

View File

@@ -0,0 +1,274 @@
using System;
using System.Collections;
using System.Collections.Generic;
using System.Net.Security;
namespace DotRecast.Core.Buffers
{
// https://github.com/joaoportela/CircularBuffer-CSharp/blob/master/CircularBuffer/CircularBuffer.cs
public class RcCyclicBuffer<T> : IEnumerable<T>
{
public struct Enumerator : IEnumerator<T>
{
private readonly RcCyclicBuffer<T> _cb;
private int _index;
private readonly int _size;
internal Enumerator(RcCyclicBuffer<T> cb)
{
_cb = cb;
_size = _cb._size;
_index = default;
Reset();
}
public bool MoveNext()
{
return ++_index < _size;
}
public void Reset()
{
_index = -1;
}
public T Current => _cb[_index];
object IEnumerator.Current => Current;
public void Dispose()
{
// This could be used to unlock write access to collection
}
}
private readonly T[] _buffer;
private int _start;
private int _end;
private int _size;
public RcCyclicBuffer(int capacity)
: this(capacity, new T[] { })
{
}
public RcCyclicBuffer(int capacity, T[] items)
{
if (capacity < 1)
{
throw new ArgumentException("RcCyclicBuffer cannot have negative or zero capacity.", nameof(capacity));
}
if (items == null)
{
throw new ArgumentNullException(nameof(items));
}
if (items.Length > capacity)
{
throw new ArgumentException("Too many items to fit RcCyclicBuffer", nameof(items));
}
_buffer = new T[capacity];
Array.Copy(items, _buffer, items.Length);
_size = items.Length;
_start = 0;
_end = _size == capacity ? 0 : _size;
}
public int Capacity => _buffer.Length;
public bool IsFull => Size == Capacity;
public bool IsEmpty => Size == 0;
public int Size => _size;
public T Front()
{
ThrowIfEmpty();
return _buffer[_start];
}
public T Back()
{
ThrowIfEmpty();
return _buffer[(_end != 0 ? _end : Capacity) - 1];
}
public T this[int index]
{
get
{
if (IsEmpty)
{
throw new IndexOutOfRangeException($"Cannot access index {index}. Buffer is empty");
}
if (index >= _size)
{
throw new IndexOutOfRangeException($"Cannot access index {index}. Buffer size is {_size}");
}
int actualIndex = InternalIndex(index);
return _buffer[actualIndex];
}
set
{
if (IsEmpty)
{
throw new IndexOutOfRangeException($"Cannot access index {index}. Buffer is empty");
}
if (index >= _size)
{
throw new IndexOutOfRangeException($"Cannot access index {index}. Buffer size is {_size}");
}
int actualIndex = InternalIndex(index);
_buffer[actualIndex] = value;
}
}
public void PushBack(T item)
{
if (IsFull)
{
_buffer[_end] = item;
Increment(ref _end);
_start = _end;
}
else
{
_buffer[_end] = item;
Increment(ref _end);
++_size;
}
}
public void PushFront(T item)
{
if (IsFull)
{
Decrement(ref _start);
_end = _start;
_buffer[_start] = item;
}
else
{
Decrement(ref _start);
_buffer[_start] = item;
++_size;
}
}
public void PopBack()
{
ThrowIfEmpty("Cannot take elements from an empty buffer.");
Decrement(ref _end);
_buffer[_end] = default(T);
--_size;
}
public void PopFront()
{
ThrowIfEmpty("Cannot take elements from an empty buffer.");
_buffer[_start] = default(T);
Increment(ref _start);
--_size;
}
public void Clear()
{
// to clear we just reset everything.
_start = 0;
_end = 0;
_size = 0;
Array.Clear(_buffer, 0, _buffer.Length);
}
public T[] ToArray()
{
T[] newArray = new T[Size];
CopyTo(newArray);
return newArray;
}
public void CopyTo(Span<T> destination)
{
var span1 = ArrayOne();
span1.CopyTo(destination);
ArrayTwo().CopyTo(destination[span1.Length..]);
}
private void ThrowIfEmpty(string message = "Cannot access an empty buffer.")
{
if (IsEmpty)
{
throw new InvalidOperationException(message);
}
}
private void Increment(ref int index)
{
if (++index == Capacity)
{
index = 0;
}
}
private void Decrement(ref int index)
{
if (index == 0)
{
index = Capacity;
}
index--;
}
private int InternalIndex(int index)
{
return _start + (index < (Capacity - _start)
? index
: index - Capacity);
}
internal Span<T> ArrayOne()
{
if (IsEmpty)
{
return new Span<T>(Array.Empty<T>());
}
if (_start < _end)
{
return new Span<T>(_buffer, _start, _end - _start);
}
return new Span<T>(_buffer, _start, _buffer.Length - _start);
}
internal Span<T> ArrayTwo()
{
if (IsEmpty)
{
return new Span<T>(Array.Empty<T>());
}
if (_start < _end)
{
return new Span<T>(_buffer, _end, 0);
}
return new Span<T>(_buffer, 0, _end);
}
public Enumerator GetEnumerator() => new Enumerator(this);
IEnumerator<T> IEnumerable<T>.GetEnumerator() => GetEnumerator();
IEnumerator IEnumerable.GetEnumerator() => GetEnumerator();
}
}

View File

@@ -0,0 +1,3 @@
fileFormatVersion: 2
guid: 1e9219e3d1874d829d9d3ed342f36981
timeCreated: 1715335324

View File

@@ -0,0 +1,120 @@
using System;
using System.Numerics;
using System.Runtime.InteropServices;
using JNGame.Math;
namespace DotRecast.Core.Buffers
{
public static class RcCyclicBuffers
{
public static long Sum(this ReadOnlySpan<long> source)
{
var buffer = source;
var result = 0L;
if (Vector.IsHardwareAccelerated)
{
var vectors = MemoryMarshal.Cast<long, Vector<long>>(buffer);
var vecSum = Vector<long>.Zero;
foreach (var vec in vectors)
vecSum += vec;
result = Vector.Dot(vecSum, Vector<long>.One);
var remainder = source.Length % Vector<long>.Count;
buffer = buffer[^remainder..];
}
foreach (var val in buffer)
result += val;
return result;
}
public static LFloat Average(this ReadOnlySpan<long> source)
{
if (0 >= source.Length)
return 0;
return source.Sum() / (LFloat)source.Length;
}
private static long Min(this ReadOnlySpan<long> source)
{
var buffer = source;
var result = long.MaxValue;
if (Vector.IsHardwareAccelerated)
{
var vectors = MemoryMarshal.Cast<long, Vector<long>>(buffer);
var vecMin = Vector<long>.One * result;
foreach (var vec in vectors)
vecMin = Vector.Min(vecMin, vec);
for (int i = 0; i < Vector<long>.Count; i++)
result = LMath.Min(result, vecMin[i]);
var remainder = source.Length % Vector<long>.Count;
buffer = buffer[^remainder..];
}
foreach (var val in buffer)
result = LMath.Min(result, val);
return result;
}
private static long Max(this ReadOnlySpan<long> source)
{
var buffer = source;
var result = long.MinValue;
if (Vector.IsHardwareAccelerated)
{
var vectors = MemoryMarshal.Cast<long, Vector<long>>(buffer);
var vecMax = Vector<long>.One * result;
foreach (var vec in vectors)
vecMax = Vector.Max(vecMax, vec);
for (int i = 0; i < Vector<long>.Count; i++)
result = LMath.Max(result, vecMax[i]);
var remainder = source.Length % Vector<long>.Count;
buffer = buffer[^remainder..];
}
foreach (var val in buffer)
result = LMath.Max(result, val);
return result;
}
public static long Sum(this RcCyclicBuffer<long> source)
{
return Sum(source.ArrayOne()) + Sum(source.ArrayTwo());
}
public static LFloat Average(this RcCyclicBuffer<long> source)
{
return Sum(source) / (LFloat)source.Size;
}
public static long Min(this RcCyclicBuffer<long> source)
{
var firstHalf = source.ArrayOne();
var secondHalf = source.ArrayTwo();
var a = firstHalf.Length > 0 ? Min(firstHalf) : long.MaxValue;
var b = secondHalf.Length > 0 ? Min(secondHalf) : long.MaxValue;
return LMath.Min(a, b);
}
public static long Max(this RcCyclicBuffer<long> source)
{
var firstHalf = source.ArrayOne();
var secondHalf = source.ArrayTwo();
var a = firstHalf.Length > 0 ? Max(firstHalf) : long.MinValue;
var b = secondHalf.Length > 0 ? Max(secondHalf) : long.MinValue;
return LMath.Max(a, b);
}
}
}

View File

@@ -0,0 +1,3 @@
fileFormatVersion: 2
guid: 2feb2734ce084ed78c3ed26b729eaddb
timeCreated: 1715335324

View File

@@ -0,0 +1,57 @@
using System;
using System.Buffers;
using System.Runtime.CompilerServices;
namespace DotRecast.Core.Buffers
{
public static class RcRentedArray
{
public static RcRentedArray<T> Rent<T>(int minimumLength)
{
var array = ArrayPool<T>.Shared.Rent(minimumLength);
return new RcRentedArray<T>(ArrayPool<T>.Shared, array, minimumLength);
}
}
public class RcRentedArray<T> : IDisposable
{
private ArrayPool<T> _owner;
private T[] _array;
public int Length { get; }
public bool IsDisposed => null == _owner || null == _array;
internal RcRentedArray(ArrayPool<T> owner, T[] array, int length)
{
_owner = owner;
_array = array;
Length = length;
}
public ref T this[int index]
{
[MethodImpl(MethodImplOptions.AggressiveInlining)]
get
{
RcThrowHelper.ThrowExceptionIfIndexOutOfRange(index, Length);
return ref _array[index];
}
}
public T[] AsArray()
{
return _array;
}
public void Dispose()
{
if (null != _owner && null != _array)
{
_owner.Return(_array, true);
_owner = null;
_array = null;
}
}
}
}

View File

@@ -0,0 +1,3 @@
fileFormatVersion: 2
guid: 1d2bbd30d7a14dedb1d02ec3dfc84549
timeCreated: 1715335324

View File

@@ -0,0 +1,3 @@
fileFormatVersion: 2
guid: d39a1676557d4a07b4132cfda6597a2f
timeCreated: 1715335324

View File

@@ -0,0 +1,49 @@
using System;
using System.Collections.Generic;
namespace DotRecast.Core.Collections
{
public static class CollectionExtensions
{
/// Sorts the given data in-place using insertion sort.
///
/// @param data The data to sort
/// @param dataLength The number of elements in @p data
public static void InsertSort(this int[] data)
{
for (int valueIndex = 1; valueIndex < data.Length; valueIndex++)
{
int value = data[valueIndex];
int insertionIndex;
for (insertionIndex = valueIndex - 1; insertionIndex >= 0 && data[insertionIndex] > value; insertionIndex--)
{
// Shift over values
data[insertionIndex + 1] = data[insertionIndex];
}
// Insert the value in sorted order.
data[insertionIndex + 1] = value;
}
}
public static void ForEach<T>(this IEnumerable<T> collection, Action<T> action)
{
foreach (var item in collection)
{
action.Invoke(item);
}
}
public static void Shuffle<T>(this IList<T> list)
{
JNGame.Math.Random random = new JNGame.Math.Random();
int n = list.Count;
while (n > 1)
{
n--;
int k = random.Next(n + 1);
(list[k], list[n]) = (list[n], list[k]);
}
}
}
}

View File

@@ -0,0 +1,3 @@
fileFormatVersion: 2
guid: 20942d7eee5249cf846b3249f705095b
timeCreated: 1715335324

View File

@@ -0,0 +1,83 @@
using System;
using System.Collections;
using System.Collections.Generic;
namespace DotRecast.Core.Collections
{
public readonly partial struct RcImmutableArray<T>
{
public IEnumerator<T> GetEnumerator()
{
return EnumeratorObject.Create(_array);
}
IEnumerator IEnumerable.GetEnumerator()
{
return EnumeratorObject.Create(_array);
}
private sealed class EnumeratorObject : IEnumerator<T>
{
private static readonly IEnumerator<T> EmptyEnumerator = new EnumeratorObject(Empty._array!);
private readonly T[] _array;
private int _index;
internal static IEnumerator<T> Create(T[] array)
{
if (array.Length != 0)
{
return new EnumeratorObject(array);
}
else
{
return EmptyEnumerator;
}
}
private EnumeratorObject(T[] array)
{
_index = -1;
_array = array;
}
public T Current
{
get
{
if (unchecked((uint)_index) < (uint)_array.Length)
{
return _array[_index];
}
throw new InvalidOperationException();
}
}
object IEnumerator.Current => this.Current;
public void Dispose()
{
}
public bool MoveNext()
{
int newIndex = _index + 1;
int length = _array.Length;
if ((uint)newIndex <= (uint)length)
{
_index = newIndex;
return (uint)newIndex < (uint)length;
}
return false;
}
void IEnumerator.Reset()
{
_index = -1;
}
}
}
}

View File

@@ -0,0 +1,3 @@
fileFormatVersion: 2
guid: a8ad3ba1cbae49ff97ae32d0a104b439
timeCreated: 1715335324

View File

@@ -0,0 +1,69 @@
using System;
using System.Collections.Generic;
namespace DotRecast.Core.Collections
{
public readonly partial struct RcImmutableArray<T> : IList<T>
{
public int Count => Length;
public bool IsReadOnly => true;
T IList<T>.this[int index]
{
get
{
var self = this;
return self[index];
}
set => throw new NotSupportedException();
}
public int IndexOf(T item)
{
for (int i = 0; i < Count; ++i)
{
if (_array![i].Equals(item))
return i;
}
return -1;
}
public bool Contains(T item)
{
return IndexOf(item) >= 0;
}
public void CopyTo(T[] array, int arrayIndex)
{
var self = this;
RcArrays.Copy(self._array!, 0, array, arrayIndex, self.Length);
}
public void Add(T item)
{
throw new NotSupportedException();
}
public void Clear()
{
throw new NotSupportedException();
}
public bool Remove(T item)
{
throw new NotSupportedException();
}
public void Insert(int index, T item)
{
throw new NotSupportedException();
}
public void RemoveAt(int index)
{
throw new NotSupportedException();
}
}
}

View File

@@ -0,0 +1,3 @@
fileFormatVersion: 2
guid: 10969565c461452babaab57192ab7b27
timeCreated: 1715335324

View File

@@ -0,0 +1,19 @@
namespace DotRecast.Core.Collections
{
public readonly partial struct RcImmutableArray<T>
{
#pragma warning disable CA1825
public static readonly RcImmutableArray<T> Empty = new RcImmutableArray<T>(new T[0]);
#pragma warning restore CA1825
private readonly T[] _array;
internal RcImmutableArray(T[] items)
{
_array = items;
}
public T this[int index] => _array![index];
public int Length => _array!.Length;
}
}

View File

@@ -0,0 +1,3 @@
fileFormatVersion: 2
guid: 9da9862cffc84b6ea948ddd33a266734
timeCreated: 1715335324

View File

@@ -0,0 +1,48 @@
using System;
namespace DotRecast.Core.Collections
{
public static class RcImmutableArray
{
public static RcImmutableArray<T> Create<T>()
{
return RcImmutableArray<T>.Empty;
}
public static RcImmutableArray<T> Create<T>(T item1)
{
T[] array = new[] { item1 };
return new RcImmutableArray<T>(array);
}
public static RcImmutableArray<T> Create<T>(T item1, T item2)
{
T[] array = new[] { item1, item2 };
return new RcImmutableArray<T>(array);
}
public static RcImmutableArray<T> Create<T>(T item1, T item2, T item3)
{
T[] array = new[] { item1, item2, item3 };
return new RcImmutableArray<T>(array);
}
public static RcImmutableArray<T> Create<T>(T item1, T item2, T item3, T item4)
{
T[] array = new[] { item1, item2, item3, item4 };
return new RcImmutableArray<T>(array);
}
public static RcImmutableArray<T> Create<T>(params T[] items)
{
if (items == null || items.Length == 0)
{
return RcImmutableArray<T>.Empty;
}
var tmp = new T[items.Length];
RcArrays.Copy(items, tmp, items.Length);
return new RcImmutableArray<T>(tmp);
}
}
}

View File

@@ -0,0 +1,3 @@
fileFormatVersion: 2
guid: 791dfec48cc5499488a03342e775c818
timeCreated: 1715335324

View File

@@ -0,0 +1,108 @@
/*
Copyright (c) 2009-2010 Mikko Mononen memon@inside.org
recast4j copyright (c) 2015-2019 Piotr Piastucki piotr@jtilia.org
DotRecast Copyright (c) 2023 Choi Ikpil ikpil@naver.com
This software is provided 'as-is', without any express or implied
warranty. In no event will the authors be held liable for any damages
arising from the use of this software.
Permission is granted to anyone to use this software for any purpose,
including commercial applications, and to alter it and redistribute it
freely, subject to the following restrictions:
1. The origin of this software must not be misrepresented; you must not
claim that you wrote the original software. If you use this software
in a product, an acknowledgment in the product documentation would be
appreciated but is not required.
2. Altered source versions must be plainly marked as such, and must not be
misrepresented as being the original software.
3. This notice may not be removed or altered from any source distribution.
*/
using System;
using System.Collections.Generic;
namespace DotRecast.Core.Collections
{
public class RcSortedQueue<T>
{
private bool _dirty;
private readonly List<T> _items;
private readonly Comparer<T> _comparer;
public RcSortedQueue(Comparison<T> comp)
{
_items = new List<T>();
_comparer = Comparer<T>.Create((x, y) => comp.Invoke(x, y) * -1);
}
public int Count()
{
return _items.Count;
}
public bool IsEmpty()
{
return 0 == _items.Count;
}
public void Clear()
{
_items.Clear();
_dirty = false;
}
private void Balance()
{
if (_dirty)
{
_items.Sort(_comparer); // reverse
_dirty = false;
}
}
public T Peek()
{
Balance();
return _items[^1];
}
public T Dequeue()
{
var node = Peek();
_items.RemoveAt(_items.Count - 1);
return node;
}
public void Enqueue(T item)
{
if (null == item)
return;
_items.Add(item);
_dirty = true;
}
public bool Remove(T item)
{
if (null == item)
return false;
//int idx = _items.BinarySearch(item, _comparer); // don't use this! Because reference types can be reused externally.
int idx = _items.FindLastIndex(x => item.Equals(x));
if (0 > idx)
return false;
_items.RemoveAt(idx);
return true;
}
public List<T> ToList()
{
Balance();
var temp = new List<T>(_items);
temp.Reverse();
return temp;
}
}
}

View File

@@ -0,0 +1,3 @@
fileFormatVersion: 2
guid: 6099a4d887984cdcbdea9ac5381be246
timeCreated: 1715335324

Some files were not shown because too many files have changed in this diff Show More