Files

440 lines
20 KiB
C#
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

using System.Collections.Generic;
using UnityEngine;
using UnityEngine.UI;
using static UI.UILineRenderer; // 允许LineCapType直接访问
// 确保在UI命名空间内
namespace UI
{
/// <summary>
/// UILineRenderer 是一个用于在UI中绘制自定义线条的Graphic组件。
/// 它支持设置线条宽度、渐变颜色以及不同类型的线头(方形或圆形)。
/// 同时支持线段之间的斜切连接,以确保转角平滑。
/// </summary>
[RequireComponent(typeof(CanvasRenderer))]
public class UILineRenderer : Graphic
{
/// <summary>
/// 存储构成线条的顶点列表。
/// </summary>
public List<Vector2> points = new List<Vector2>();
/// <summary>
/// 线的宽度。
/// </summary>
[SerializeField] private float lineWidth = 5f;
/// <summary>
/// 获取或设置线的宽度。
/// 设置新值时如果宽度发生改变将触发UI网格的重新绘制。
/// </summary>
public float LineWidth
{
get => lineWidth;
set
{
// 仅当值发生显著变化时才更新,以避免不必要的重绘。
// Mathf.Abs(lineWidth - value) > float.Epsilon 是一个更健壮的浮点数比较方式。
if (Mathf.Abs(lineWidth - value) > float.Epsilon)
{
lineWidth = value;
SetVerticesDirty(); // 标记UI网格需要重新生成。
}
}
}
/// <summary>
/// 用于线条的渐变颜色。
/// </summary>
public Gradient lineGradient = new Gradient();
/// <summary>
/// 定义线条两端的线头类型。
/// </summary>
public enum LineCapType
{
/// <summary>
/// 无线头。
/// </summary>
None,
/// <summary>
/// 方形线头。
/// </summary>
Square,
/// <summary>
/// 圆形线头。
/// </summary>
Round
}
/// <summary>
/// 线的起始端线头类型。
/// </summary>
public LineCapType startCap = LineCapType.None;
/// <summary>
/// 线的结束端线头类型。
/// </summary>
public LineCapType endCap = LineCapType.None;
/// <summary>
/// 获取用于UI渲染的相机。
/// 如果Canvas的渲染模式是Screen Space - Overlay则返回null此时不需要相机。
/// </summary>
/// <returns>用于UI渲染的Camera实例或在Overlay模式下返回null。</returns>
private Camera GetCanvasRenderCamera()
{
Canvas _canvas = GetComponentInParent<Canvas>();
// 作为Graphic组件总会有一个Canvas作为父级。
if (_canvas == null) return null;
// 对于ScreenSpaceOverlay模式不需要相机来将屏幕点转换为世界点。
return _canvas.renderMode == RenderMode.ScreenSpaceOverlay ? null : _canvas.worldCamera;
}
/// <summary>
/// 在UI需要被重新绘制时调用用于生成网格顶点数据。
/// </summary>
/// <param name="vh">VertexHelper用于构建UI元素的网格。</param>
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<float> segmentLengths = new List<float>();
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<UIVertex> vertices = new List<UIVertex>();
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));
}
}
/// <summary>
/// 绘制指定类型的线头。
/// </summary>
/// <param name="vh">VertexHelper用于构建线头网格。</param>
/// <param name="center">线头的中心(即线条的端点)。</param>
/// <param name="direction">线头延伸的方向。</param>
/// <param name="capType">线头类型(无、方形或圆形)。</param>
/// <param name="capColor">线头的颜色。</param>
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<int> arcVerticesIndices = new List<int>();
// 遍历并添加半圆弧上的点。
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]);
}
}
}
/// <summary>
/// 将一个UI元素的世界坐标转换为此UILineRenderer的局部坐标并将其作为线条上的一个点添加。
/// </summary>
/// <param name="uiElement">要作为线条点的RectTransform UI元素。</param>
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网格需要重新生成。
}
/// <summary>
/// 将鼠标的当前位置作为线条的末端点。
/// 该方法支持对已有折线的末端点进行修改,也可用于绘制“橡皮筋”效果。
/// </summary>
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网格需要重新生成。
}
/// <summary>
/// 设置线的宽度。
/// </summary>
/// <param name="width">线条的新宽度。</param>
public void SetWidth(float width)
{
LineWidth = width; // 调用公共属性,确保 SetVerticesDirty() 被调用。
}
/// <summary>
/// 设置线条的渐变颜色。
/// </summary>
/// <param name="newGradient">新的渐变色对象。</param>
public void SetGradient(Gradient newGradient)
{
// 仅当新的渐变色不同于当前渐变色时才进行更新。
if (newGradient != null && !newGradient.Equals(lineGradient))
{
lineGradient = newGradient;
SetVerticesDirty(); // 标记UI网格需要重新生成。
}
}
/// <summary>
/// 设置线条的起始和结束线头类型。
/// </summary>
/// <param name="startCapType">新的起始线头类型。</param>
/// <param name="endCapType">新的结束线头类型。</param>
public void SetCaps(LineCapType startCapType, LineCapType endCapType)
{
// 仅当线头类型发生改变时才更新。
if (startCap != startCapType || endCap != endCapType)
{
startCap = startCapType;
endCap = endCapType;
SetVerticesDirty(); // 标记UI网格需要重新生成。
}
}
/// <summary>
/// 重置此UILineRenderer组件的状态包括清空所有点、重置宽度、渐变色和线头类型。
/// </summary>
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网格需要重新生成。
}
}
}