原理:
利用 Graphic 类重写 OnPopulateMesh 方法类绘制自定义顶点的面片从而组成一条线。
MaskableGraphic 类继承自 Graphic,并且可以实现“可遮罩图形”,方便在列表中使用。
绘制图形API:
// 添加顶点,第一个添加的顶点索引为0,第二个添加的顶点为1,依次.....
AddVert
// 绘制三角形,GPU绘制时会按照输入的顶点下标的顺序绘制一个三角形
AddTriangle
// 添加一个矩形
AddUIVertexQuad
// 批量添加顶点
AddUIVertexStream
// 批量添加三角形顶点,长度必须是3的倍数
AddUIVertexTriangleStream
组件功能说明:
1、设置线段宽度
2、切换曲线和直线绘制模式
3、在曲线模式下,可以控制曲线的片段数量
4、实时刷新,根据目标点的位置实时更新线条
具体实现可直接复制代码使用,具体实现已添加备注。
完整代码:
using System;
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.UI;
namespace XLine
{
/// <summary>
/// VertexHelper 扩展类型
/// </summary>
static class VertexHelperEx
{
#region Bezier
class Bezier
{
private Vector2 _point0, _point1, _point2, _point3;
/// <summary>
///
/// </summary>
/// <param name="p"></param>
/// <exception cref="ArgumentException">x0,y0,x1,y1,x2,y2,x3,y3</exception>
public Bezier(params float[] p)
{
if (p.Length < 8)
{
throw new ArgumentException("参数数量错误,需要8个参数(4个Vector2)");
}
_point0 = new Vector2(p[0], p[1]);
_point1 = new Vector2(p[2], p[3]);
_point2 = new Vector2(p[4], p[5]);
_point3 = new Vector2(p[6], p[7]);
}
public Bezier(Vector2 p0, Vector2 p1, Vector2 p2, Vector2 p3)
{
_point0 = p0;
_point1 = p1;
_point2 = p2;
_point3 = p3;
}
public float X(float t)
{
var it = 1 - t;
return it * it * it * _point0.x +
3 * it * it * t * _point1.x +
3 * it * t * t * _point2.x +
t * t * t * _point3.x;
}
public float Y(float t)
{
var it = 1 - t;
return it * it * it * _point0.y +
3 * it * it * t * _point1.y +
3 * it * t * t * _point2.y +
t * t * t * _point3.y;
}
public float SpeedX(float t)
{
var it = 1 - t;
return -3 * _point0.x * it * it +
3 * _point1.x * it * it -
6 * _point1.x * it * t +
6 * _point2.x * it * t -
3 * _point2.x * t * t +
3 * _point3.x * t * t;
}
public float SpeedY(float t)
{
var it = 1 - t;
return -3 * _point0.y * it * it +
3 * _point1.y * it * it -
6 * _point1.y * it * t +
6 * _point2.y * it * t -
3 * _point2.y * t * t +
3 * _point3.y * t * t;
}
private float SpeedXY(float t)
{
return Mathf.Sqrt(Mathf.Pow(SpeedX(t), 2) + Mathf.Pow(SpeedY(t), 2));
}
}
#endregion
#region Tools
private static Vector2 Direction(this Vector2 v, Vector2 pos)
{
return (pos - v).normalized;
}
private static Vector2 Rotate90(this Vector2 v, int dir = 1)
{
var x = v.x;
if (dir >= 0)
{
v.x = -v.y;
v.y = x;
}
else
{
v.x = v.y;
v.y = -x;
}
return v;
}
private static Vector2 Copy(this Vector2 v)
{
return new Vector2(v.x, v.y);
}
#endregion
/// <summary>
/// 生成顶点
/// </summary>
/// <param name="lineColor"></param>
/// <param name="vertPos"></param>
/// <returns></returns>
private static UIVertex[] UIVertexQuad(Color lineColor, params Vector2[] vertPos)
{
var vs = new UIVertex[4];
var uv = new Vector2[4];
uv[0] = new Vector2(0, 0);
uv[1] = new Vector2(0, 1);
uv[2] = new Vector2(1, 0);
uv[3] = new Vector2(1, 1);
for (var i = 0; i < 4; i++)
{
var v = UIVertex.simpleVert;
v.color = lineColor;
v.position = vertPos[i];
v.uv0 = uv[i];
vs[i] = v;
}
return vs;
}
/// <summary>
/// 绘制多点直线
/// </summary>
/// <param name="vh"></param>
/// <param name="points"></param>
/// <param name="width"></param>
/// <param name="lineColor"></param>
public static void DrawLines(this VertexHelper vh, List<Vector2> points, float width, Color lineColor)
{
for (int i = 0; i < points.Count - 1; i++)
{
DrawLine(vh, points[i], points[i + 1], width, lineColor);
}
}
// 直接绘制两点之间的线段
public static void DrawLine(VertexHelper vh, Vector2 start, Vector2 end, float width, Color lineColor)
{
var direction = end.Direction(start);
var normal = direction.Rotate90() * (width * 0.5f);
UIVertex[] quadVerts = UIVertexQuad(lineColor, start + normal, end + normal, end - normal, start - normal);
vh.AddUIVertexQuad(quadVerts);
}
#region 绘制贝塞尔曲线
private const float InterpolationV2 = 0.6f;
private const float Interpolation = 0.5f;
private static readonly List<Bezier> Beziers = new List<Bezier>();
/// <summary>
/// 中心点
/// </summary>
private static readonly List<Vector2> MidPoints = new List<Vector2>();
/// <summary>
/// 控制点
/// </summary>
private static readonly List<Vector2> CtrlPoints = new List<Vector2>();
/// <summary>
/// 绘制贝塞尔曲线
/// </summary>
/// <param name="vh"></param>
/// <param name="points"></param>
/// <param name="segment"></param>
/// <param name="width"></param>
/// <param name="lineColor"></param>
public static void DrawBeziers(this VertexHelper vh, List<Vector2> points, float segment, float width, Color lineColor)
{
ConvertBeziers(points);
if (Beziers.Count <= 0) return;
foreach (var bezier in Beziers)
{
DrawBezier(vh, bezier, segment, width, lineColor);
}
}
private static void DrawBezier(VertexHelper vh, Bezier bezier, float segment, float width, Color lineColor)
{
var leftPos = new List<Vector2>();
var rightPos = new List<Vector2>();
for (var i = 0; i <= segment; i++)
{
var t = i / segment;
var t2 = Interpolation * width;
var bezierPos = new Vector2(bezier.X(t), bezier.Y(t));
var bezierSpeed = new Vector2(bezier.SpeedX(t), bezier.SpeedY(t));
var offsetA = bezierSpeed.normalized.Rotate90() * t2;
var offsetB = bezierSpeed.normalized.Rotate90(-1) * t2;
leftPos.Add(bezierPos.Copy() + offsetA);
rightPos.Add(bezierPos.Copy() + offsetB);
}
for (var j = 0; j < segment; j++)
{
vh.AddUIVertexQuad(UIVertexQuad(lineColor, leftPos[j], leftPos[j + 1], rightPos[j + 1], rightPos[j]));
}
}
/// <summary>
/// 通过点绘制贝塞尔曲线
/// </summary>
/// <param name="points"></param>
/// <returns></returns>
private static void ConvertBeziers(List<Vector2> points)
{
Beziers.Clear();
var originCnt = points.Count - 1;
MidPoints.Clear();
for (var i = 0; i < originCnt; i++)
{
var x = Mathf.Lerp(points[i].x, points[i + 1].x, Interpolation);
var y = Mathf.Lerp(points[i].y, points[i + 1].y, Interpolation);
MidPoints.Add(new Vector2(x, y));
}
CtrlPoints.Clear();
CtrlPoints.Add(points[0]);
for (var i = 0; i < originCnt - 1; i++)
{
var midX = Mathf.Lerp(MidPoints[i].x, MidPoints[i + 1].x, Interpolation);
var midY = Mathf.Lerp(MidPoints[i].y, MidPoints[i + 1].y, Interpolation);
var originPoint = points[i + 1];
var offsetX = originPoint.x - midX;
var offsetY = originPoint.y - midY;
CtrlPoints.Add(new Vector2(MidPoints[i].x + offsetX, MidPoints[i].y + offsetY));
CtrlPoints.Add(new Vector2(MidPoints[i + 1].x + offsetX, MidPoints[i + 1].y + offsetY));
CtrlPoints[i * 2 + 1] = Vector2.Lerp(originPoint, CtrlPoints[i * 2 + 1], InterpolationV2);
CtrlPoints[i * 2 + 2] = Vector2.Lerp(originPoint, CtrlPoints[i * 2 + 2], InterpolationV2);
}
CtrlPoints.Add(points[^1]);
for (var i = 0; i < originCnt; i++)
{
var bezier = new Bezier(points[i],
CtrlPoints[i * 2],
CtrlPoints[i * 2 + 1],
points[i + 1]);
Beziers.Add(bezier);
}
}
#endregion
}
[RequireComponent(typeof(CanvasRenderer))]
public class UILineDraw : MaskableGraphic
{
/// <summary>
/// 目标位置
/// </summary>
public List<RectTransform> targets = new List<RectTransform>();
/// <summary>
/// 线宽
/// </summary>
public float lineWidth = 10f;
/// <summary>
/// 片段数量,仅用于曲线
/// </summary>
public int segments = 10;
/// <summary>
/// UI转屏幕坐标
/// </summary>
public bool isScreen = false;
/// <summary>
/// UI相机,用于屏幕坐标转化
/// </summary>
public Camera uiCamera;
/// <summary>
/// 开启贝塞尔曲线
/// </summary>
public bool openBezier = false;
/// <summary>
/// 实时刷新
/// </summary>
public bool realTimeUpdate = true;
private readonly List<Vector2> _points = new List<Vector2>();
private void Update()
{
if (!realTimeUpdate)
{
return;
}
_points.Clear();
foreach (var item in targets)
{
_points.Add(isScreen ? RectTransformUtility.WorldToScreenPoint(uiCamera, item.anchoredPosition) : item.anchoredPosition);
}
UpdateGeometry();
}
protected override void OnPopulateMesh(VertexHelper vh)
{
// 先行清除
vh.Clear();
if (_points.Count <= 0) return;
if (openBezier)
{
vh.DrawBeziers(_points, segments, lineWidth, color);
}
else
{
vh.DrawLines(_points, lineWidth, color);
}
}
/// <summary>
/// 获取目标数量
/// </summary>
public int Count => targets.Count;
/// <summary>
/// 添加目标
/// </summary>
/// <param name="target"></param>
public void AddTarget(RectTransform target)
{
targets.Add(target);
}
/// <summary>
/// 添加目标集合
/// </summary>
/// <param name="transforms"></param>
public void AddTargets(IEnumerable<RectTransform> transforms)
{
targets.AddRange(transforms);
}
/// <summary>
/// 移除目标
/// </summary>
/// <param name="target"></param>
public void RemoveTarget(RectTransform target)
{
targets.Remove(target);
}
/// <summary>
/// 清理目标
/// </summary>
public void ClearTargets()
{
targets.Clear();
}
}
}
面板扩展:
using UnityEditor;
using UnityEditor.UI;
using UnityEngine;
namespace XLine
{
[CustomEditor(typeof(UILineDraw), true)]
[CanEditMultipleObjects]
public class UILineDrawEditor : GraphicEditor
{
private SerializedProperty _targetRectTransform;
private SerializedProperty _lineWidth;
private SerializedProperty _segments;
private SerializedProperty _isScreen;
private SerializedProperty _uiCamera;
private SerializedProperty _openBezier;
private SerializedProperty _realTimeUpdate;
private bool _fade = false;
protected override void OnEnable()
{
base.OnEnable();
_fade = GetEditorPrefsBool("_fadeLines", _fade);
_targetRectTransform = serializedObject.FindProperty("targets");
_lineWidth = serializedObject.FindProperty("lineWidth");
_segments = serializedObject.FindProperty("segments");
_isScreen = serializedObject.FindProperty("isScreen");
_uiCamera = serializedObject.FindProperty("uiCamera");
_openBezier = serializedObject.FindProperty("openBezier");
_realTimeUpdate = serializedObject.FindProperty("realTimeUpdate");
}
public override void OnInspectorGUI()
{
base.OnInspectorGUI();
serializedObject.Update();
EditorGUILayout.Space();
// 线条功能折叠或展开
EditorGUI.BeginChangeCheck();
_fade = EditorGUILayout.Foldout(_fade,"Lines");
if (EditorGUI.EndChangeCheck())
SetEditorPrefsBool("_fadeLines", _fade);
if (_fade)
{
EditorGUI.indentLevel++;
EditorGUILayout.PropertyField(_targetRectTransform,new GUIContent("目标点"));
EditorGUILayout.PropertyField(_lineWidth, new GUIContent("线宽"));
EditorGUILayout.PropertyField(_openBezier, new GUIContent("开启贝塞尔曲线"));
if (_openBezier.boolValue)
{
EditorGUI.indentLevel++;
EditorGUILayout.PropertyField(_segments, new GUIContent("线条面片数量"));
EditorGUI.indentLevel--;
}
EditorGUILayout.PropertyField(_isScreen, new GUIContent("UI坐标转屏幕坐标"));
if (_isScreen.boolValue)
{
EditorGUI.indentLevel++;
EditorGUILayout.PropertyField(_uiCamera, new GUIContent("UI相机"));
EditorGUI.indentLevel--;
}
EditorGUILayout.PropertyField(_realTimeUpdate, new GUIContent("实时刷新"));
EditorGUI.indentLevel--;
}
serializedObject.ApplyModifiedProperties();
}
/// <summary>
/// 获取编辑器指定配置
/// </summary>
/// <param name="key"></param>
/// <param name="defaultValue"></param>
/// <returns></returns>
private bool GetEditorPrefsBool(string key, bool defaultValue = false)
{
return EditorPrefs.GetBool($"{PlayerSettings.companyName}{key}", defaultValue);
}
/// <summary>
/// 设置编辑器指定配置
/// </summary>
/// <param name="key"></param>
/// <param name="defaultValue"></param>
private void SetEditorPrefsBool(string key, bool defaultValue = false)
{
EditorPrefs.SetBool($"{PlayerSettings.companyName}{key}", defaultValue);
}
}
}
演示示例:
Unity UGUI扩展 —— XLine组件
版权归原作者 Long Xiao 所有, 如有侵权,请联系我们删除。