Unity扩展 UI线段绘制组件——UI上的LineRenderer

原理:

利用 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组件

相关推荐

  1. Unity扩展 UI线段绘制组件——UILineRenderer

    2024-07-22 21:28:01       14 阅读
  2. Unity判断鼠标是否在UI

    2024-07-22 21:28:01       14 阅读
  3. element-ui传图片组件封装

    2024-07-22 21:28:01       39 阅读

最近更新

  1. docker php8.1+nginx base 镜像 dockerfile 配置

    2024-07-22 21:28:01       52 阅读
  2. Could not load dynamic library ‘cudart64_100.dll‘

    2024-07-22 21:28:01       54 阅读
  3. 在Django里面运行非项目文件

    2024-07-22 21:28:01       45 阅读
  4. Python语言-面向对象

    2024-07-22 21:28:01       55 阅读

热门阅读

  1. IDM破解

    IDM破解

    2024-07-22 21:28:01      11 阅读
  2. 通过Python面向对象编程探索克苏鲁神话

    2024-07-22 21:28:01       12 阅读
  3. 【论文精读】Fully Sparse 3D Occupancy Prediction

    2024-07-22 21:28:01       15 阅读
  4. 如何在Linux中打开core文件

    2024-07-22 21:28:01       13 阅读
  5. 数据仓库中的数据治理流程

    2024-07-22 21:28:01       12 阅读
  6. 数据结构(特殊二叉树-线索二叉树)

    2024-07-22 21:28:01       13 阅读
  7. 代码重构实践分享

    2024-07-22 21:28:01       13 阅读
  8. Python变量

    2024-07-22 21:28:01       13 阅读
  9. Opencv的kmeans每次调用结果都会变化

    2024-07-22 21:28:01       13 阅读