using System.Collections.Generic; using UnityEngine; using UnityEngine.UI; using static UI.UILineRenderer; // 允许LineCapType直接访问 // 确保在UI命名空间内 namespace UI { /// /// UILineRenderer 是一个用于在UI中绘制自定义线条的Graphic组件。 /// 它支持设置线条宽度、渐变颜色以及不同类型的线头(方形或圆形)。 /// 同时支持线段之间的斜切连接,以确保转角平滑。 /// [RequireComponent(typeof(CanvasRenderer))] public class UILineRenderer : Graphic { /// /// 存储构成线条的顶点列表。 /// public List points = new List(); /// /// 线的宽度。 /// [SerializeField] private float lineWidth = 5f; /// /// 获取或设置线的宽度。 /// 设置新值时,如果宽度发生改变,将触发UI网格的重新绘制。 /// public float LineWidth { get => lineWidth; set { // 仅当值发生显著变化时才更新,以避免不必要的重绘。 // Mathf.Abs(lineWidth - value) > float.Epsilon 是一个更健壮的浮点数比较方式。 if (Mathf.Abs(lineWidth - value) > float.Epsilon) { lineWidth = value; SetVerticesDirty(); // 标记UI网格需要重新生成。 } } } /// /// 用于线条的渐变颜色。 /// public Gradient lineGradient = new Gradient(); /// /// 定义线条两端的线头类型。 /// public enum LineCapType { /// /// 无线头。 /// None, /// /// 方形线头。 /// Square, /// /// 圆形线头。 /// Round } /// /// 线的起始端线头类型。 /// public LineCapType startCap = LineCapType.None; /// /// 线的结束端线头类型。 /// public LineCapType endCap = LineCapType.None; /// /// 获取用于UI渲染的相机。 /// 如果Canvas的渲染模式是Screen Space - Overlay,则返回null,此时不需要相机。 /// /// 用于UI渲染的Camera实例,或在Overlay模式下返回null。 private Camera GetCanvasRenderCamera() { Canvas _canvas = GetComponentInParent(); // 作为Graphic组件,总会有一个Canvas作为父级。 if (_canvas == null) return null; // 对于ScreenSpaceOverlay模式,不需要相机来将屏幕点转换为世界点。 return _canvas.renderMode == RenderMode.ScreenSpaceOverlay ? null : _canvas.worldCamera; } /// /// 在UI需要被重新绘制时调用,用于生成网格顶点数据。 /// /// VertexHelper,用于构建UI元素的网格。 protected override void OnPopulateMesh(VertexHelper vh) { vh.Clear(); // 如果点列表为空或点的数量不足以构成线段,则不绘制任何东西。 if (points == null || points.Count < 2) return; // 如果渐变色对象未设置或颜色键为空,则默认创建一个纯白色渐变。 if (lineGradient == null || lineGradient.colorKeys.Length == 0) { lineGradient = new Gradient(); lineGradient.SetKeys( new[] { new GradientColorKey(Color.white, 0f), new GradientColorKey(Color.white, 1f) }, new[] { new GradientAlphaKey(1f, 0f), new GradientAlphaKey(1f, 1f) } ); } List segmentLengths = new List(); float totalLength = 0f; // 预计算线条总长度和每段长度,用于渐变色和线头定位 for (int i = 0; i < points.Count - 1; i++) { float len = Vector2.Distance(points[i], points[i + 1]); segmentLengths.Add(len); totalLength += len; } // 存储计算出的所有顶点,包括内外侧及颜色信息 List vertices = new List(); float currentRenderedLength = 0f; float halfWidth = lineWidth / 2f; const float miterLimit = 4f; // Miter limit to prevent excessively long miters for (int i = 0; i < points.Count; i++) { Vector2 p = points[i]; Vector2 currentPerp = Vector2.zero; // 垂直于期望线段方向的偏移向量 // 计算当前点的渐变颜色 float normalizedDistance = (totalLength > 0 && points.Count > 1) ? (currentRenderedLength / totalLength) : 0f; Color pointColor = lineGradient.Evaluate(normalizedDistance); if (i == 0) // 第一个点 (起点) { // 使用第一段的法线方向 Vector2 segmentDir = (points[1] - p).normalized; currentPerp = new Vector2(-segmentDir.y, segmentDir.x) * halfWidth; } else if (i == points.Count - 1) // 最后一个点 (终点) { // 使用最后一段的法线方向 Vector2 segmentDir = (p - points[points.Count - 2]).normalized; currentPerp = new Vector2(-segmentDir.y, segmentDir.x) * halfWidth; } else // 中间点 (转角) { Vector2 prevDir = (p - points[i - 1]).normalized; Vector2 nextDir = (points[i + 1] - p).normalized; // 计算两条线段的平均法线 (角平分线方向) Vector2 angleBisector = (prevDir + nextDir).normalized; // 计算垂直于角平分线的向量 Vector2 bisectorPerp = new Vector2(-angleBisector.y, angleBisector.x); // 确定 bisectorPerp 的方向,使其始终指向“外侧” // 通过检查 prevDir 和 bisectorPerp 的点积来判断方向 // 如果点积为负,表示 prevDir 的“向左”方向(perp)与 bisectorPerp 方向相反,需要翻转 // (prevDir.x * bisectorPerp.y - prevDir.y * bisectorPerp.x) > 0 表示 bisectorPerp 在 prevDir 的左侧 // (prevDir.x * bisectorPerp.y - prevDir.y * bisectorPerp.x) < 0 表示 bisectorPerp 在 prevDir 的右侧 // 更直观的判断是看prevDir的右向量和bisectorPerp是否同向 Vector2 prevDirPerp = new Vector2(-prevDir.y, prevDir.x); // prevDir的左侧法线 if (Vector2.Dot(prevDirPerp, bisectorPerp) < 0) // 如果bisectorPerp不是指向prevDir左侧,则翻转 { bisectorPerp *= -1; } // 计算斜切的延伸长度 // 角度越尖锐,cos(angle/2) 越小,miterFactor 越大 float angleRad = Vector2.Angle(prevDir, nextDir) * Mathf.Deg2Rad; float miterLengthFactor = halfWidth / Mathf.Max(0.001f, Mathf.Cos(angleRad / 2f)); // 防止除以0 // 应用miter limit,避免极端尖锐角造成过长斜切 miterLengthFactor = Mathf.Min(miterLengthFactor, lineWidth * miterLimit); currentPerp = bisectorPerp * miterLengthFactor; } // 添加内外侧顶点 // Outer point vertices.Add(new UIVertex { position = p + currentPerp, color = pointColor, uv0 = new Vector2(0, normalizedDistance) // Simple UV mapping for gradient, 0 for outer }); // Inner point vertices.Add(new UIVertex { position = p - currentPerp, color = pointColor, uv0 = new Vector2(1, normalizedDistance) // Simple UV mapping for gradient, 1 for inner }); // 更新已渲染长度,用于下一个点的渐变计算 if (i < segmentLengths.Count) { currentRenderedLength += segmentLengths[i]; } } // --- 绘制线段网格 --- for (int i = 0; i < points.Count - 1; i++) { int prevOuter = i * 2; // 上一个点的外侧顶点索引 int prevInner = i * 2 + 1; // 上一个点的内侧顶点索引 int currentOuter = (i + 1) * 2; // 当前点的外侧顶点索引 int currentInner = (i + 1) * 2 + 1; // 当前点的内侧顶点索引 // 添加顶点到 VertexHelper // 确保vh.AddVert的顺序与vertices列表中的顺序一致 if (i == 0) // 为第一个点添加顶点 { vh.AddVert(vertices[prevOuter]); vh.AddVert(vertices[prevInner]); } vh.AddVert(vertices[currentOuter]); vh.AddVert(vertices[currentInner]); // 连接当前点和上一个点的顶点,形成一个矩形(两个三角形) vh.AddTriangle(prevOuter, currentOuter, currentInner); vh.AddTriangle(prevOuter, currentInner, prevInner); } // --- 绘制线头 --- if (points.Count >= 2) { // 起点线头 Vector2 firstSegmentDirection = (points[1] - points[0]).normalized; DrawCap(vh, points[0], -firstSegmentDirection, startCap, lineGradient.Evaluate(0f)); // 终点线头 Vector2 lastSegmentDirection = (points[points.Count - 1] - points[points.Count - 2]).normalized; DrawCap(vh, points[points.Count - 1], lastSegmentDirection, endCap, lineGradient.Evaluate(1f)); } } /// /// 绘制指定类型的线头。 /// /// VertexHelper,用于构建线头网格。 /// 线头的中心(即线条的端点)。 /// 线头延伸的方向。 /// 线头类型(无、方形或圆形)。 /// 线头的颜色。 private void DrawCap(VertexHelper vh, Vector2 center, Vector2 direction, LineCapType capType, Color capColor) { if (capType == LineCapType.None) return; // 计算线头垂直于方向的向量,用于确定线头的宽度。 var perpendicular = new Vector2(-direction.y, direction.x) * lineWidth / 2f; // 记录当前VertexHelper中的顶点数量。 var currentVertCount = vh.currentVertCount; UIVertex vertexTemplate = UIVertex.simpleVert; vertexTemplate.color = capColor; if (capType == LineCapType.Square) { // 绘制方形线头,通过在端点处添加一个与线段垂直的矩形。 Vector2 p0 = center - perpendicular; // 线段边缘点1 Vector2 p1 = center + perpendicular; // 线段边缘点2 Vector2 p2 = center + perpendicular + (direction * (lineWidth / 2f)); // 延伸点1 Vector2 p3 = center - perpendicular + (direction * (lineWidth / 2f)); // 延伸点2 // 添加方形线头的四个顶点。 // 注意:这里需要确保顶点的顺序正确,形成两个三角形 vertexTemplate.position = p0; vertexTemplate.uv0 = new Vector2(0f, 0f); vh.AddVert(vertexTemplate); // 0 vertexTemplate.position = p1; vertexTemplate.uv0 = new Vector2(1f, 0f); vh.AddVert(vertexTemplate); // 1 vertexTemplate.position = p2; vertexTemplate.uv0 = new Vector2(1f, 1f); vh.AddVert(vertexTemplate); // 2 vertexTemplate.position = p3; vertexTemplate.uv0 = new Vector2(0f, 1f); vh.AddVert(vertexTemplate); // 3 // 添加两个三角形组成方形线头。 vh.AddTriangle(currentVertCount, currentVertCount + 1, currentVertCount + 2); vh.AddTriangle(currentVertCount, currentVertCount + 2, currentVertCount + 3); } else if (capType == LineCapType.Round) { const int segments = 12; // 增加段数使圆形更平滑 float radius = lineWidth / 2f; // 半圆的半径等于线宽的一半。 // 添加扇形中心点。 vertexTemplate.position = center; vertexTemplate.uv0 = new Vector2(0.5f, 0.5f); // 中心UV vh.AddVert(vertexTemplate); var centerVertIndex = currentVertCount; currentVertCount++; // 计算半圆弧的起始角度 (垂直向量的角度) 和每段的扫过角度。 float baseAngle = Mathf.Atan2(perpendicular.y, perpendicular.x); float angleSweepStep = Mathf.PI / segments; // 180度 / 段数。 List arcVerticesIndices = new List(); // 遍历并添加半圆弧上的点。 for (int j = 0; j <= segments; j++) { float angle = baseAngle - (j * angleSweepStep); // 逆时针扫描。 Vector2 pointOnArc = new Vector2(Mathf.Cos(angle), Mathf.Sin(angle)) * radius; vertexTemplate.position = center + pointOnArc; // 计算相对中心的UV,使纹理正确映射到圆形。 vertexTemplate.uv0 = new Vector2(0.5f + pointOnArc.x / (radius * 2), 0.5f + pointOnArc.y / (radius * 2)); vh.AddVert(vertexTemplate); arcVerticesIndices.Add(currentVertCount); currentVertCount++; } // 连接扇形中心和弧上的相邻点形成三角形,构成半圆形线头。 for (int j = 0; j < arcVerticesIndices.Count - 1; j++) { vh.AddTriangle(centerVertIndex, arcVerticesIndices[j], arcVerticesIndices[j + 1]); } } } /// /// 将一个UI元素的世界坐标转换为此UILineRenderer的局部坐标,并将其作为线条上的一个点添加。 /// /// 要作为线条点的RectTransform UI元素。 public void AppendUIElement(RectTransform uiElement) { Vector2 localPoint; // 转换后的局部坐标。 // 将UI元素的世界坐标转换为屏幕坐标,再将屏幕坐标转换为当前UILineRenderer的局部坐标。 RectTransformUtility.ScreenPointToLocalPointInRectangle( rectTransform, // 当前 UILineRenderer 的 RectTransform。 RectTransformUtility.WorldToScreenPoint(GetCanvasRenderCamera(), uiElement.position), // UI 元素的世界坐标转换为屏幕坐标。 GetCanvasRenderCamera(), // 使用正确的相机进行屏幕到局部坐标转换。 out localPoint // 输出的局部坐标。 ); // 添加转换后的局部坐标到点列表中。 points.Add(localPoint); SetVerticesDirty(); // 标记UI网格需要重新生成。 } /// /// 将鼠标的当前位置作为线条的末端点。 /// 该方法支持对已有折线的末端点进行修改,也可用于绘制“橡皮筋”效果。 /// public void SetMouse() { // 如果点列表为空,则无法设置鼠标位置为末端点,直接返回。 if (points.Count == 0) { return; } var mousePosition = Input.mousePosition; // 获取当前的鼠标屏幕坐标。 Vector2 localPoint; // 将鼠标屏幕坐标转换为当前UILineRenderer的局部坐标。 RectTransformUtility.ScreenPointToLocalPointInRectangle(rectTransform, mousePosition, GetCanvasRenderCamera(), out localPoint); if (points.Count == 1) { // 如果只有一个点,则将鼠标位置作为第二点添加到列表,形成第一条线段。 points.Add(localPoint); } else // points.Count >= 2 { // 如果已有多个点,则更新列表中的最后一个点为鼠标位置。 points[points.Count - 1] = localPoint; } SetVerticesDirty(); // 标记UI网格需要重新生成。 } /// /// 设置线的宽度。 /// /// 线条的新宽度。 public void SetWidth(float width) { LineWidth = width; // 调用公共属性,确保 SetVerticesDirty() 被调用。 } /// /// 设置线条的渐变颜色。 /// /// 新的渐变色对象。 public void SetGradient(Gradient newGradient) { // 仅当新的渐变色不同于当前渐变色时才进行更新。 if (newGradient != null && !newGradient.Equals(lineGradient)) { lineGradient = newGradient; SetVerticesDirty(); // 标记UI网格需要重新生成。 } } /// /// 设置线条的起始和结束线头类型。 /// /// 新的起始线头类型。 /// 新的结束线头类型。 public void SetCaps(LineCapType startCapType, LineCapType endCapType) { // 仅当线头类型发生改变时才更新。 if (startCap != startCapType || endCap != endCapType) { startCap = startCapType; endCap = endCapType; SetVerticesDirty(); // 标记UI网格需要重新生成。 } } /// /// 重置此UILineRenderer组件的状态,包括清空所有点、重置宽度、渐变色和线头类型。 /// public void ResetSelf() { points.Clear(); // 清空所有线条点。 // 重置为默认的纯白色渐变。 lineGradient = new Gradient(); lineGradient.SetKeys( new[] { new GradientColorKey(Color.white, 0f), new GradientColorKey(Color.white, 1f) }, new[] { new GradientAlphaKey(1f, 0f), new GradientAlphaKey(1f, 1f) } ); lineWidth = 5f; // 重置线条宽度为默认值。 startCap = LineCapType.None; // 重置线头类型为无。 endCap = LineCapType.None; // 重置线头类型为无。 SetVerticesDirty(); // 标记UI网格需要重新生成。 } } }