using System.Collections.Generic; using UnityEngine; using UnityEngine.UI; namespace LineUI { // 定义 ArrowInfo 类 public class ArrowInfo { public Vector2 Position { get; set; } public Vector2 Direction { get; set; } public ArrowInfo(Vector2 position, Vector2 direction) { Position = position; Direction = direction; } } [RequireComponent(typeof(CanvasRenderer))] [RequireComponent(typeof(RectTransform))] public class Line : Graphic { [Header("绘制线段")] [SerializeField] private bool loop = false; [SerializeField] private float thickness = 1f; [SerializeField] private int roundCount = 0; [SerializeField] private List screenPositions = new List(); //获取当前的points [HideInInspector] public List ScreenPositions => screenPositions; [Header("绘制内四边形")] //是否绘制内四边形 [SerializeField] bool bDrawQuad = true; [SerializeField] private Vector2 quadrilateralSize = new Vector2(100, 100); [SerializeField] private Color quadColor = Color.red; [Header("绘制外围蒙板")] //是否绘制外围蒙板 [SerializeField] bool bDrawMask = false; [SerializeField] private Color maskColor = Color.red; //控制扇形角绘制 [Header("扇形角绘制")] [SerializeField] private bool bDrawFan = false; [SerializeField] private Color fanColor = Color.white; [SerializeField] private int fanSegments = 20; // 扇形的平滑度 [SerializeField] private float fanOuterRadius = 150f; // 扇形的半径 private float fanInnerRadius = 0f; [Tooltip("扇形指向箭头")] [SerializeField] private bool bDrawArrow = false; [SerializeField] private Color arrowColor = Color.white; [SerializeField] float arrowLength = 50f; [SerializeField] float arrowWidth = 50f; [SerializeField] float arrowHeadHeight = 30f; [SerializeField] float arrowDis = 100f; private List arrowInfos = new List(); //获取当前的points [HideInInspector] public List ArrowInfos => arrowInfos; //[SerializeField] private Color quadTextColor = Color.black; //[SerializeField] private int quadFontSize = 14; //private List quadsToDraw = new List(); //private List quadTextToDraw = new List(); //private List quadObjects = new List(); //private Stack objectPool = new Stack(); [Tooltip("抗锯齿处理部分")] [SerializeField, Range(0f, 5f)] private float antiAliasValue = 2f; // 抗锯齿羽化宽度 private const int ObjectPoolLimit = 4; public RectTransform RectTr => rectTransform; public float MyThickness { get => thickness; set { if (value >= 1) { thickness = value; } } } public float MyFanWidth { get => fanOuterRadius; set { if (value >= 1) { fanOuterRadius = value; } } } public void SetLine(List screenPositions) { this.screenPositions = screenPositions; SetVerticesDirty(); } public void SetDrawQuad(bool value) { bDrawQuad = value; SetVerticesDirty(); } public void SetDrawMask(bool value) { bDrawMask = value; SetVerticesDirty(); } public void SetDrawFan(bool value) { bDrawFan = value; SetVerticesDirty(); } protected override void OnPopulateMesh(VertexHelper vh) { vh.Clear(); arrowInfos.Clear(); //quadsToDraw.Clear(); //quadTextToDraw.Clear(); if (screenPositions.Count < 2) return; SetLineVertices(vh); SetLineTriangles(vh); if(bDrawQuad) SetQuadrilateralVertices(vh); if(bDrawMask) DrawMask(vh); // 绘制蒙版 if (bDrawFan) DrawFansAtCorners(vh); // 在转折角绘制扇形 } private void SetLineVertices(VertexHelper vh) { UIVertex vert = UIVertex.simpleVert; vert.color = color; List _screenPositions = new List(screenPositions); if (loop) { _screenPositions.Add(screenPositions[0]); _screenPositions.Add(screenPositions[1]); } float lastAngle = 0; for (int i = 1; i < _screenPositions.Count; i++) { Vector2 previousPos = _screenPositions[i - 1]; Vector2 currentPos = _screenPositions[i]; Vector2 dif = currentPos - previousPos; float angle = Vector2.SignedAngle(Vector2.right, dif); Vector2 offset = Quaternion.Euler(0, 0, angle) * Vector3.up * thickness; if (i > 1) { float anglePerRound = (angle - lastAngle) / roundCount; for (int j = 0; j <= roundCount; j++) { float ang = lastAngle + anglePerRound * j; Vector2 roundOffset = Quaternion.Euler(0, 0, ang) * Vector2.up * thickness; vert.position = previousPos - roundOffset; vh.AddVert(vert); vert.position = previousPos + roundOffset; vh.AddVert(vert); } } lastAngle = angle; vert.position = previousPos - offset; vh.AddVert(vert); vert.position = previousPos + offset; vh.AddVert(vert); vert.position = currentPos - offset; vh.AddVert(vert); vert.position = currentPos + offset; vh.AddVert(vert); // 🔹 在每段线条两侧增加抗锯齿羽化 if (antiAliasValue > 0.01f) { AddAntiAliasForLine(vh, previousPos, currentPos, offset, antiAliasValue); } } } private void SetQuadrilateralVertices(VertexHelper vh) { UIVertex vert = UIVertex.simpleVert; vert.color = quadColor; List _screenPositions = new List(screenPositions); if (loop) { _screenPositions.Add(screenPositions[0]); _screenPositions.Add(screenPositions[1]); } for (int i = 1; i < _screenPositions.Count; i++) { Vector2 previousPos = _screenPositions[i - 1]; Vector2 currentPos = _screenPositions[i]; if (i > 1) { Vector2 prevDir = (_screenPositions[i - 1] - _screenPositions[i - 2]).normalized; Vector2 currentDir = (currentPos - previousPos).normalized; int index = i == 4 ? 4 : i % 4; if (index == 4 || index == 2) { DrawSquareAtCorner(vh, previousPos, prevDir, currentDir, quadrilateralSize.x, quadrilateralSize.y, index); } else { DrawSquareAtCorner(vh, previousPos, prevDir, currentDir, quadrilateralSize.y, quadrilateralSize.x, index); } } } } private void DrawSquareAtCorner(VertexHelper vh, Vector2 corner, Vector2 prevDir, Vector2 currentDir, float width, float height, int index) { UIVertex vert = UIVertex.simpleVert; vert.color = quadColor; Vector2[] corners = new Vector2[4]; corners[0] = corner; corners[1] = corner + prevDir * -width; corners[2] = corner + prevDir * -width + currentDir * height; corners[3] = corner + currentDir * height; foreach (Vector2 pos in corners) { vert.position = pos; vh.AddVert(vert); } int startIndex = vh.currentVertCount - 4; vh.AddTriangle(startIndex, startIndex + 1, startIndex + 2); vh.AddTriangle(startIndex + 2, startIndex + 3, startIndex); // Store the quad's center position and text for later use //Vector2 quadCenter = corner + (prevDir * -width + currentDir * height) / 2; //quadsToDraw.Add(quadCenter); //quadTextToDraw.Add(index.ToString()); } private void SetLineTriangles(VertexHelper vh) { for (int i = 0; i < vh.currentVertCount - 2; i += 2) { int index = i; vh.AddTriangle(index, index + 1, index + 3); vh.AddTriangle(index + 3, index + 2, index); } } private void DrawMask(VertexHelper vh) { UIVertex vert = UIVertex.simpleVert; vert.color = maskColor; //// 屏幕四个角的坐标 //Vector2[] screenCorners = new Vector2[] //{ // new Vector2(-Screen.width / 2, -Screen.height / 2), // 左下角 // new Vector2(Screen.width / 2, -Screen.height / 2), // 右下角 // new Vector2(Screen.width / 2, Screen.height / 2), // 右上角 // new Vector2(-Screen.width / 2, Screen.height / 2), // 左上角 //}; // 获取 RectTransform 的实际四个角坐标 Rect rect = rectTransform.rect; Vector2[] screenCorners = new Vector2[] { new Vector2(rect.xMin, rect.yMin), // 左下角 new Vector2(rect.xMax, rect.yMin), // 右下角 new Vector2(rect.xMax, rect.yMax), // 右上角 new Vector2(rect.xMin, rect.yMax), // 左上角 }; // 添加四个点作为内框(中间区域的四个顶点) Vector2[] innerCorners = screenPositions.ToArray(); // 分别绘制四个蒙版区域 // 1. 左上区域:围绕左上角和内框左上、右上 AddQuad(vh, screenCorners[3], innerCorners[3], innerCorners[2], screenCorners[2]); // 2. 右上区域:围绕右上角和内框右上、右下 AddQuad(vh, innerCorners[2], screenCorners[2], screenCorners[1], innerCorners[1]); // 3. 右下区域:围绕右下角和内框右下、左下 AddQuad(vh, innerCorners[0], innerCorners[1], screenCorners[1], screenCorners[0]); // 4. 左下区域:围绕左下角和内框左下、左上 AddQuad(vh, screenCorners[3], screenCorners[0], innerCorners[0], innerCorners[3]); } private void AddQuad(VertexHelper vh, Vector2 corner1, Vector2 corner2, Vector2 corner3, Vector2 corner4) { UIVertex vert = UIVertex.simpleVert; vert.color = maskColor; vert.position = corner1; vh.AddVert(vert); vert.position = corner2; vh.AddVert(vert); vert.position = corner3; vh.AddVert(vert); vert.position = corner4; vh.AddVert(vert); //Debug.Log("vh.currentVertCount:"+ vh.currentVertCount); int startIndex = vh.currentVertCount - 4; vh.AddTriangle(startIndex, startIndex + 1, startIndex + 2); vh.AddTriangle(startIndex + 2, startIndex + 3, startIndex); } /// /// 1. 计算角度方向: /// 使用 Vector2.Angle 可以得到两个向量之间的夹角大小,但它不能确定角度的方向。为此,我们需要使用 Mathf.Atan2 来计算相对坐标的角度。Atan2 可以返回一个在 -π 到 π 之间的角度值,并且考虑了顺时针和逆时针的方向。 /// 2. 从 X 轴正方向为 0 计算角度: /// 将角度计算为相对于 x 轴正方向的角度,并根据 Atan2 结果进行调整。 /// /// private void DrawFansAtCorners(VertexHelper vh) { List _screenPositions = new List(screenPositions); if (loop) { _screenPositions.Add(screenPositions[0]); _screenPositions.Add(screenPositions[1]); } //比如现在是6个点,实际从第二个点开始绘制 for (int i = 1; i < _screenPositions.Count - 1; i++) { Vector2 previousPos = _screenPositions[i - 1]; Vector2 currentPos = _screenPositions[i]; Vector2 nextPos = _screenPositions[i + 1]; // 计算当前点到前一点的方向 Vector2 prevDir = (currentPos - previousPos).normalized; // 计算当前点到下一点的方向 Vector2 currentDir = (nextPos - currentPos).normalized; // 使用 Atan2 计算方向相对于 x 轴的角度 float prevAngle = Mathf.Atan2(prevDir.y, prevDir.x) * Mathf.Rad2Deg; // 前一个方向相对于 x 轴的角度 float currentAngle = Mathf.Atan2(currentDir.y, currentDir.x) * Mathf.Rad2Deg; // 当前方向相对于 x 轴的角度 // 计算两个方向之间的角度差 float angleBetween = Vector2.Angle(prevDir, currentDir); // 判断旋转方向 float cross = prevDir.x * currentDir.y - prevDir.y * currentDir.x; if (cross < 0) { // 逆时针旋转 angleBetween = 360f - angleBetween; } // 内角的计算(补充180度) float innerAngle = 180f - angleBetween; // 计算开始角度,这里用的是 x 轴为起始点来计算 float startAngle = currentAngle; //Debug.Log($"Index {i}: startAngle = {startAngle}, innerAngle = {innerAngle}"); // 根据当前角的位置调整内角和起始角 DrawFanAtCorner(vh, currentPos, startAngle, innerAngle, fanInnerRadius, fanOuterRadius, fanSegments, fanColor); // 计算箭头的中心角度,确保角度在 0-360° 范围内 float centerAngle = (startAngle + innerAngle / 2) % 360f; float radians = Mathf.Deg2Rad * centerAngle; // 计算箭头位置:在扇形外半径的基础上延长一定距离(箭头偏移量) float arrowOffset = fanOuterRadius + arrowDis; Vector2 arrowPosition = currentPos + new Vector2(Mathf.Cos(radians), Mathf.Sin(radians)) * arrowOffset; // 箭头方向 Vector2 arrowDirection = new Vector2(Mathf.Cos(radians), Mathf.Sin(radians)).normalized; // 将箭头信息保存到列表中 arrowInfos.Add(new ArrowInfo(arrowPosition, arrowDirection)); // 在所有扇形处绘制箭头 // 在扇形中心绘制箭头 if (bDrawArrow) { // 调试信息 //Debug.Log($"Index {i}: arrowPosition = {arrowPosition}, startAngle = {startAngle}, centerAngle = {centerAngle}, direction = {arrowDirection}"); // 绘制箭头 DrawArrow(vh, arrowPosition, arrowDirection, arrowLength, arrowWidth, arrowHeadHeight); } } } private void DrawFanAtCorner(VertexHelper vh, Vector2 center, float startAngle, float angleDegree, float innerRadius, float outerRadius, int segments ,Color color) { UIVertex vert = UIVertex.simpleVert; vert.color = color; float angleRad = Mathf.Deg2Rad * angleDegree; // 将绘制角度转换为弧度 float startAngleRad = Mathf.Deg2Rad * startAngle; // 将起始角度转换为弧度 float angleStep = angleRad / segments; // 每个扇形段的角度步长 int initialVertCount = vh.currentVertCount; // 记录当前顶点数量 // 添加中心点顶点 vert.position = center; vert.uv0 = new Vector2(0.5f, 0.5f); // 中心UV值 vh.AddVert(vert); // 绘制外圈和内圈的顶点 for (int i = 0; i <= segments; i++) { float currentAngle = startAngleRad + i * angleStep; float cosA = Mathf.Cos(currentAngle); float sinA = Mathf.Sin(currentAngle); // 外圈顶点 vert.position = center + new Vector2(cosA * outerRadius, sinA * outerRadius); vert.uv0 = new Vector2(0.5f + cosA * 0.5f, 0.5f + sinA * 0.5f); vh.AddVert(vert); // 内圈顶点 vert.position = center + new Vector2(cosA * innerRadius, sinA * innerRadius); vert.uv0 = new Vector2(0.5f + cosA * innerRadius / outerRadius * 0.5f, 0.5f + sinA * innerRadius / outerRadius * 0.5f); vh.AddVert(vert); } // 创建三角形索引来绘制扇形 for (int i = 1; i <= segments; i++) { int baseIndex = initialVertCount + (i - 1) * 2; vh.AddTriangle(initialVertCount, baseIndex + 1, baseIndex + 3); // 中心点连接外圈 vh.AddTriangle(baseIndex + 1, baseIndex + 2, baseIndex + 3); // 内圈连接外圈 } } // 新增箭头绘制方法 private void DrawArrow(VertexHelper vh, Vector2 position, Vector2 direction, float arrowLength, float arrowWidth, float arrowHeadHeight) { // 标准化方向向量 direction.Normalize(); // 反向箭头的方向(箭头指向扇形方向) Vector2 reversedDirection = -direction; // 箭头尾部位置 Vector2 basePosition = position - reversedDirection * arrowLength; // 计算垂直向量用于箭头宽度 Vector2 perpendicular = new Vector2(-reversedDirection.y, reversedDirection.x); // 让箭头矩形部分的宽度比三角形的底边窄 float adjustedArrowWidth = arrowWidth * 0.6f; // 调整宽度,使矩形比三角形窄 // 箭头矩形部分的四个顶点 Vector2 baseLeft = basePosition + perpendicular * (adjustedArrowWidth / 2); Vector2 baseRight = basePosition - perpendicular * (adjustedArrowWidth / 2); Vector2 headLeft = position + perpendicular * (adjustedArrowWidth / 2); Vector2 headRight = position - perpendicular * (adjustedArrowWidth / 2); Vector2 triangleHeadLeft = position + perpendicular * (arrowHeadHeight / 2); Vector2 triangleHeadRight = position - perpendicular * (arrowHeadHeight / 2); // 顶点颜色 UIVertex vert = UIVertex.simpleVert; vert.color = arrowColor; // 先添加矩形部分的顶点 int rectStartIndex = vh.currentVertCount; vert.position = baseLeft; vh.AddVert(vert); vert.position = baseRight; vh.AddVert(vert); vert.position = headLeft; vh.AddVert(vert); vert.position = headRight; vh.AddVert(vert); // 添加矩形部分的两条三角形索引 vh.AddTriangle(rectStartIndex, rectStartIndex + 1, rectStartIndex + 2); vh.AddTriangle(rectStartIndex + 1, rectStartIndex + 3, rectStartIndex + 2); // 然后添加三角形头部的顶点 Vector2 headPosition = position + reversedDirection * arrowHeadHeight; int headStartIndex = vh.currentVertCount; vert.position = triangleHeadLeft; vh.AddVert(vert); vert.position = triangleHeadRight; vh.AddVert(vert); vert.position = headPosition; // 添加箭头尖端 vh.AddVert(vert); // 添加三角形的索引 int triangleStartIndex = vh.currentVertCount - 3; // 三角形部分的开始索引 vh.AddTriangle(triangleStartIndex, triangleStartIndex + 1, triangleStartIndex + 2); // 三角形索引 } /// /// 抗锯齿羽化边缘 /// private void AddAntiAliasForLine(VertexHelper vh, Vector2 start, Vector2 end, Vector2 offset, float feather) { UIVertex vert = UIVertex.simpleVert; Color featherColor = color; featherColor.a = 0; // 透明收尾 // 上边缘 vert.color = color; vert.position = start + offset; vh.AddVert(vert); vert.position = end + offset; vh.AddVert(vert); vert.color = featherColor; vert.position = start + offset + (offset.normalized * feather); vh.AddVert(vert); vert.position = end + offset + (offset.normalized * feather); vh.AddVert(vert); int idx = vh.currentVertCount - 4; vh.AddTriangle(idx, idx + 1, idx + 2); vh.AddTriangle(idx + 1, idx + 3, idx + 2); // 下边缘 vert.color = color; vert.position = start - offset; vh.AddVert(vert); vert.position = end - offset; vh.AddVert(vert); vert.color = featherColor; vert.position = start - offset - (offset.normalized * feather); vh.AddVert(vert); vert.position = end - offset - (offset.normalized * feather); vh.AddVert(vert); idx = vh.currentVertCount - 4; vh.AddTriangle(idx, idx + 1, idx + 2); vh.AddTriangle(idx + 1, idx + 3, idx + 2); } //private void LateUpdate() //{ // foreach (var quadObj in quadObjects) // { // if (quadObj != null) // { // objectPool.Push(quadObj); // } // } // quadObjects.Clear(); // for (int i = 0; i < quadsToDraw.Count; i++) // { // GameObject quadGO = GetQuadObject(); // if (quadGO != null) // { // quadGO.SetActive(true); // quadGO.GetComponent().anchoredPosition = quadsToDraw[i]; // Text quadTextComponent = quadGO.GetComponent(); // quadTextComponent.text = quadTextToDraw[i]; // quadTextComponent.fontSize = quadFontSize; // quadTextComponent.color = quadTextColor; // quadTextComponent.alignment = TextAnchor.MiddleCenter; // } // } // quadsToDraw.Clear(); // quadTextToDraw.Clear(); //} //private GameObject GetQuadObject() //{ // if (objectPool.Count > 0) // { // GameObject quadGO = objectPool.Pop(); // if (quadGO != null) // { // quadObjects.Add(quadGO); // return quadGO; // } // } // if (quadObjects.Count < ObjectPoolLimit) // { // GameObject newQuadGO = new GameObject("QuadText", typeof(RectTransform), typeof(Text)); // newQuadGO.transform.SetParent(this.transform); // Text quadTextComponent = newQuadGO.GetComponent(); // quadTextComponent.alignment = TextAnchor.MiddleCenter; // quadTextComponent.rectTransform.sizeDelta = new Vector2(quadrilateralSize.x, quadrilateralSize.y); // quadObjects.Add(newQuadGO); // return newQuadGO; // } // return null; //} } }