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

ゝ一纸荒年。 2023-09-27 09:25 263阅读 0赞

一、效果展示

de8ae92f513e70bc4f33890a06bdd608.gif

二、实现过程

2.1 准备工作

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

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

2.2 生成顶点

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

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

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

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

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

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

2.3 生成操作点

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

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

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

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

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

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

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

916890d6ebac0e79831ac5a7097eb076.png

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

2.5 填充雷达图

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

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

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

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

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

效果如下
77b7dcf0962c22ac3dd81bcd97b82945.gif

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

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

效果如下
2c3cb191fa5297da5c4a044598a1b800.gif


源码下载

发表评论

表情:
评论列表 (有 0 条评论,263人围观)

还没有评论,来说两句吧...

相关阅读