提交Unity 联机Pro

This commit is contained in:
PC-20230316NUNE\Administrator
2024-08-17 14:27:18 +08:00
parent f00193b000
commit 894100ae37
7448 changed files with 854473 additions and 0 deletions

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: