• UGUI学习笔记(十)自制雷达图


    一、效果展示

    二、实现过程

    2.1 准备工作

    首先导入一张雷达图的背景图,将其挂载到「Image」上添加到场景中,命名为「RadarBg」。
    请添加图片描述
    在「RadarBg」下添加一个空的子物体,命名为「RadarChart」并挂载同名脚本。它用来展示雷达图上层的数据信息。「RadarChart」需要继承「Image」类。

    public class RadarChart : Image
    {
    }
    
    • 1
    • 2
    • 3

    2.2 生成顶点

    这里的顶点用来标记雷达图背景的范围。我们需要在编辑器中规定顶点的个数,因此创建一个成员变量_pointCount。为了能在编辑器模式下保存数据信息,所以给成员变量添加[SerializeField]特性。然后就是根据顶点个数创建顶点,并将顶点缓存起来

    [SerializeField]  
    private int _pointCount;  
    [SerializeField]  
    private List<RectTransform> _points;
    
    /// 
    /// 创建顶点
    /// 
    private void SpawnPoint()
    {
    	for (int i = 0; i < _pointCount; i++)
    	{
    		GameObject point = new GameObject("Point" + i);
    		point.transform.SetParent(transform);
    		_points.Add(point.AddComponent<RectTransform>());
    	}
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17

    生成顶点后,需要设置顶点的默认位置。我们可以以(0,0)点为圆心,让生成的点位于雷达图中心与顶点的连线上

    /// 
    /// 设置顶点初始位置
    /// 
    private void SetPointPos()
    {
    	// 顶点间间隔的弧长
    	float radian = 2 * Mathf.PI / _pointCount;
    	// 生成点与中心的距离
    	float radius = 100f;
    	// 起始点从1/2π开始
    	float curRadian = Mathf.PI / 2;
    	for (int i = 0; i < _pointCount; i++)
    	{
    		float x = Mathf.Cos(curRadian) * radius;
    		float y = Mathf.Sin(curRadian) * radius;
    		curRadian += radian;
    		_points[i].anchoredPosition = new Vector2(x, y);
    	}
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19

    另外,在生成顶点前,需要清除_points中已存在的顶点。这些方法统一在初始化顶点时执行

    /// 
    /// 初始化顶点
    /// 
    public void InitPoint()
    {
    	ClearPoint();
    	_points = new List<RectTransform>();
    	SpawnPoint();
    	SetPointPos();
    }
    /// 
    /// 清除点
    /// 
    private void ClearPoint()
    {
    	if(_points == null)
    		return;
    	foreach (var point in _points)
    	{
    		if(point != null)
    			DestroyImmediate(point);
    	}
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23

    2.3 生成操作点

    操作点是指雷达图中表示数据部分的图形的顶点。它可以根据不同的比例,在中心与顶点的连线上移动。
    操作点的生成与顶点生成的过程差不多。首先我们新建一个操作点类「RadarChartHandler」,在类中定义好必须的API

    public class RadarChartHandler : MonoBehaviour
    {
        private RectTransform _rect;
        private RectTransform Rect
        {
            get
            {
                if (_rect == null)
                    _rect = GetComponent<RectTransform>();
                return _rect;
            }
        }
    
        private Image _img;
        private Image Img
        {
            get
            {
                if (_img == null)
                    _img = GetComponent<Image>();
                return _img;
            }
        }
        /// 
        /// 设置父物体
        /// 
        /// 
        public void SetParent(Transform parentTrans)
        {
            transform.SetParent(parentTrans);
        }
        /// 
        /// 设置大小
        /// 
        /// 
        public void SetSize(Vector2 size)
        {
            Rect.sizeDelta = size;
        }
        /// 
        /// 设置图片
        /// 
        /// 
        public void SetSprite(Sprite sprite)
        {
            Img.sprite = sprite;
        }
        /// 
        /// 设置颜色
        /// 
        /// 
        public void SetColor(Color color)
        {
            Img.color = color;
        }
        /// 
        /// 设置位置
        /// 
        /// 
        public void SetPos(Vector2 pos)
        {
            Rect.anchoredPosition = pos;
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48
    • 49
    • 50
    • 51
    • 52
    • 53
    • 54
    • 55
    • 56
    • 57
    • 58
    • 59
    • 60
    • 61
    • 62
    • 63
    • 64

    在「RadarChart」类中,同样是「清空->创建->设置位置」的过程,直接上代码

    [SerializeField]  
    private List<RadarChartHandler> _handlers;  
    [SerializeField]  
    private Sprite _pointSprite;  
    [SerializeField]  
    private Color _pointColor = Color.white;  
    [SerializeField]  
    private Vector2 _pointSize = new Vector2(10,10);  
    [SerializeField]  
    private float[] _handlerRadio;
    /// 
    /// 初始化操作点
    /// 
    public void InitHandler()
    {
    	ClearHandler();
    	_handlers = new List<RadarChartHandler>();
    	SpawnHandler();
    	SetHandlerPos();
    }
    
    /// 
    /// 清除操作点
    /// 
    private void ClearHandler()
    {
    	if(_handlers == null)
    		return;
    	foreach (var point in _handlers)
    	{
    		if(point != null)
    			DestroyImmediate(point);
    	}
    }
    /// 
    /// 创建操作点
    /// 
    private void SpawnHandler()
    {
    	for (int i = 0; i < _pointCount; i++)
    	{
    		
    		GameObject point = new GameObject("Handler" + i);
    		point.AddComponent<RectTransform>();
    		point.AddComponent<Image>();
    		RadarChartHandler handler = point.AddComponent<RadarChartHandler>();
    		handler.SetParent(transform);
    		handler.SetColor(_pointColor);
    		handler.SetSprite(_pointSprite);
    		handler.SetSize(_pointSize);
    		_handlers.Add(handler);
    	}
    }
    /// 
    /// 设置操作点位置
    /// 
    private void SetHandlerPos()
    {
    	if (_handlerRadio == null || _handlerRadio.Length == 0)
    	{
    		for (int i = 0; i < _pointCount; i++)
    		{
    			_handlers[i].SetPos(_points[i].anchoredPosition);
    		}
    	}
    	else
    	{
    		for (int i = 0; i < _pointCount; i++)
    		{
    			_handlers[i].SetPos(_points[i].anchoredPosition * _handlerRadio[i]);
    		}
    	}
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48
    • 49
    • 50
    • 51
    • 52
    • 53
    • 54
    • 55
    • 56
    • 57
    • 58
    • 59
    • 60
    • 61
    • 62
    • 63
    • 64
    • 65
    • 66
    • 67
    • 68
    • 69
    • 70
    • 71
    • 72
    • 73

    2.4 将成员变量添加到编辑器

    我们只是给成员变量添加了[SerializeField] 特性,要想在编辑器上显示出来还需要进行编辑器扩展。代码都是固定的,这里不再详述

    [CustomEditor(typeof(RadarChart), true)]
    [CanEditMultipleObjects]
    public class RadarChartEditor : UnityEditor.UI.ImageEditor
    {
        SerializedProperty _pointCount;
        SerializedProperty _pointSprite;
        SerializedProperty _pointColor;
        SerializedProperty _pointSize;
        SerializedProperty _handlerRadio;
    
        protected override void OnEnable()
        {
            base.OnEnable();
            _pointCount = serializedObject.FindProperty("_pointCount");
            _pointSprite = serializedObject.FindProperty("_pointSprite");
            _pointColor = serializedObject.FindProperty("_pointColor");
            _pointSize = serializedObject.FindProperty("_pointSize");
            _handlerRadio = serializedObject.FindProperty("_handlerRadio");
        }
    
        public override void OnInspectorGUI()
        {
            base.OnInspectorGUI();
    
            serializedObject.Update();
            
            EditorGUILayout.PropertyField(_pointCount);
            EditorGUILayout.PropertyField(_pointSprite);
            EditorGUILayout.PropertyField(_pointColor);
            EditorGUILayout.PropertyField(_pointSize);
            EditorGUILayout.PropertyField(_handlerRadio,true);
    
            RadarChart radar = target as RadarChart;
            if (radar != null)
            {
                if (GUILayout.Button("生成雷达图顶点"))
                {
                    radar.InitPoint();
                }
    
                if (GUILayout.Button("生成内部可操作顶点"))
                {
                    radar.InitHandler();
                }
            }
            serializedObject.ApplyModifiedProperties();
            if (GUI.changed)
            {
                EditorUtility.SetDirty(target);
            }
    
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48
    • 49
    • 50
    • 51
    • 52
    • 53

    接下来设定好操作点的sprite、大小、颜色等属性,就可以进行生成操作了。生成后的效果如下

    2.5 填充雷达图

    观察上面的截图可以发现,雷达图中黄色的填充部分还没有处理。那么如何让它填充成五边形呢?这就又要用到父类中的OnPopulateMesh()方法了。我们在这个方法中重新添加顶点,就可以让它渲染成多边形

    protected override void OnPopulateMesh(VertexHelper toFill)
    {
    	toFill.Clear();
    	AddVert(toFill);
    	AddTriangle(toFill);
    }
    /// 
    /// 添加顶点
    /// 
    /// 
    private void AddVert(VertexHelper toFill)
    {
    	// 添加中心点  
    	toFill.AddVert(Vector3.zero, color,Vector4.zero);
    	foreach (var handler in _handlers)
    	{
    		toFill.AddVert(handler.transform.localPosition,color,Vector4.zero);
    	}
    }
    /// 
    /// 添加三角形
    /// 
    /// 
    private void AddTriangle(VertexHelper toFill)
    {
    	for (int i = 1; i < _pointCount; i++)
    	{
    		// 始终以中心点为起点
    		toFill.AddTriangle(0,i+1,i);
    	}
    	toFill.AddTriangle(0,_pointCount,1);
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32

    返回Unity,可以看到效果如下

    但目前拖动操作点,填充图并不会发生变化,需要在Update()方法中实时刷新。Graphic类中自带的SetVerticesDirty()方法可以将顶点标记为脏数据,并重建。

    private void Update()
    {
    	SetVerticesDirty();
    }
    
    • 1
    • 2
    • 3
    • 4

    效果如下

    为了能在运行时也可以进行拖动操作,我们给「RadarChartHandler」类加上OnDrag()方法。在拖动时,给操作点的位置加上鼠标的位移即可实现。但是这个位移值会受到父物体缩放的影响,可以通过Rect.lossyScale获取到总的缩放系数,然后用位移除以缩放系数,即可抵消影响。

    public void OnDrag(PointerEventData eventData)
    {
    	Rect.anchoredPosition += new Vector2(GetScaleX(eventData),GetScaleY(eventData));
    }
    
    private float GetScaleX(PointerEventData eventData)
    {
    	return eventData.delta.x/Rect.lossyScale.x;
    }
    private float GetScaleY(PointerEventData eventData)
    {
    	return eventData.delta.y/Rect.lossyScale.y;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13

    效果如下


    源码下载

  • 相关阅读:
    算法通过村第十一关-位运算|黄金笔记|位运算压缩
    Session会话追踪的实现机制
    【电商】电商后台设计—购物车
    LeetCode - 1700. 无法吃午餐的学生数量
    kubernetes
    【PAT甲级 - C++题解】1137 Final Grading
    2022高教杯思路合集!!全国大学生数学建模竞赛
    【我的OpenGL学习进阶之旅】如何在Android中使用ARCore结合OpenGL ES来实现增强人脸Augmented Faces?
    RabbitMQ的五种常见消费模型
    MyEclipse数据库工具使用教程:使用驱动程序
  • 原文地址:https://blog.csdn.net/LWR_Shadow/article/details/126804838