新建Scene命名为RadarChart,在Scripts文件夹内新建同名文件夹,并在其中新建RadarChart
脚本。
生成背景标记点 每一个背景标记点都是一个GameObject,我们将这些点称为Point,修改RadarChart
脚本
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 using System.Collections;using System.Collections.Generic;using UnityEngine;using UnityEngine.UI;public class RadarChart : Image { [SerializeField ] private int mPointCount; [SerializeField ] private List<RectTransform> mPoints; public void InitPoint () { ClearPoints(); mPoints = new List<RectTransform>(); SpawnPoints(); SetPointPos(); } public void InitHandlers () { } private void ClearPoints () { if (mPoints != null ) { foreach (var point in mPoints) { if (point != null ) { DestroyImmediate(point.gameObject); } } } } private void SpawnPoints () { for (int i = 0 ; i < mPointCount; i++) { GameObject point = new GameObject("Point" + i); var rect = point.AddComponent<RectTransform>(); point.transform.SetParent(transform); mPoints.Add(rect); } } private void SetPointPos () { float radian = 2 * Mathf.PI / mPointCount; float radius = 100 ; float curRadian = 2 * Mathf.PI / 4.0f ; for (int i = 0 ;i < mPointCount;i++) { float x = Mathf.Cos(curRadian) * radius; float y = Mathf.Sin(curRadian) * radius; curRadian += radian; mPoints[i].anchoredPosition = new Vector2(x, y); } } }
生成可拖拽点 每一个可拖拽点都是一个GameObject,我们将这些点称为Handler,修改RadarChart
脚本
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 public class RadarChart : Image { [SerializeField ] private int mPointCount; [SerializeField ] private List<RectTransform> mPoints; [SerializeField ] private Sprite mPointSprite; [SerializeField ] private Vector2 mPointSize = new Vector2(10 ,10 ); [SerializeField ] private Color mPointColor = Color.white; [SerializeField ] private float [] mHandlerRatio; [SerializeField ] private List<RadarChartHandler> mChartHandlers; public void InitHandlers () { ClearHandlers(); mChartHandlers = new List<RadarChartHandler>(); SpawnHandlers(); SetHandlerPos(); } private void ClearHandlers () { if (mChartHandlers != null ) { foreach (var handler in mChartHandlers) { if (handler != null ) { DestroyImmediate (handler.gameObject); } } } } private void SpawnHandlers () { RadarChartHandler chartHandler; for (int i = 0 ; i < mPointCount; i++) { GameObject handler = new GameObject("Handler" + i); handler.AddComponent<RectTransform>(); handler.AddComponent<Image>(); chartHandler = handler.AddComponent<RadarChartHandler>(); chartHandler.SetParent(transform); chartHandler.ChangeSprite(mPointSprite); chartHandler.ChangeColor(mPointColor); chartHandler.SetSize(mPointSize); mChartHandlers.Add(chartHandler); } } private void SetHandlerPos () { if (mHandlerRatio == null || mHandlerRatio.Length != mPointCount) { for (int i = 0 ; i < mPointCount;i++) { mChartHandlers[i].SetPos(mPoints[i].anchoredPosition); } } else { for (int i = 0 ; i < mPointCount; i++) { mChartHandlers[i].SetPos(mPoints[i].anchoredPosition * mHandlerRatio[i]); } } } }
在RadarChart文件夹内新建RadarChartHandler
脚本
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 using UnityEngine;using UnityEngine.UI;public class RadarChartHandler : MonoBehaviour { private Image mImage; public Image Image { get { if (mImage == null ) mImage = GetComponent<Image>(); return mImage; } } private RectTransform mRectTransform; public RectTransform Rect { get { if (mRectTransform == null ) mRectTransform = GetComponent<RectTransform>(); return mRectTransform; } } public void SetParent (Transform parent ) { transform.SetParent(parent); } public void ChangeSprite (Sprite sprite ) { Image.sprite = sprite; } public void ChangeColor (Color color ) { Image.color = color; } public void SetPos (Vector2 pos ) { Rect.anchoredPosition = pos; } public void SetSize (Vector2 size ) { Rect.sizeDelta = size; } }
编辑器代码 在RadarChart文件夹内新建Editor文件夹,并在其中新建RadarChartEditor
脚本
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 using UnityEditor;using UnityEngine;[CustomEditor(typeof(RadarChart),true) ] [CanEditMultipleObjects ] public class RadarChartEditor : UnityEditor.UI.ImageEditor { SerializedProperty mPointCount; SerializedProperty mPointSprite; SerializedProperty mPointColor; SerializedProperty mPointSize; SerializedProperty mHandlerRatio; protected override void OnEnable () { base .OnEnable(); mPointCount = serializedObject.FindProperty("mPointCount" ); mPointSprite = serializedObject.FindProperty("mPointSprite" ); mPointColor = serializedObject.FindProperty("mPointColor" ); mPointSize = serializedObject.FindProperty("mPointSize" ); mHandlerRatio = serializedObject.FindProperty("mHandlerRatio" ); } public override void OnInspectorGUI () { base .OnInspectorGUI(); serializedObject.Update(); EditorGUILayout.PropertyField(mPointCount,new GUIContent("顶点个数" )); EditorGUILayout.PropertyField(mPointSize,new GUIContent("顶点大小" )); EditorGUILayout.PropertyField(mPointSprite,new GUIContent("顶点图片" )); EditorGUILayout.PropertyField(mPointColor,new GUIContent("顶点颜色" )); EditorGUILayout.PropertyField(mHandlerRatio,new GUIContent("顶点比例" ),true ); RadarChart chart = (RadarChart)target; if (chart != null ) { if (GUILayout.Button("生成雷达图顶点" )) { chart.InitPoint(); } if (GUILayout.Button("生成内部可操作顶点" )) { chart.InitHandlers(); } } serializedObject.ApplyModifiedProperties(); } }
使用方法 我们在Canvas下新建一个子对象,命名为RadarChart。
给RadarChart挂载RadarChart
脚本,修改脚本的“顶点个数”,然后点击脚本的“生成雷达图顶点”按钮,此时RadarChart就会生成对应个数的子对象,我们可以随意移动这些命名为“Point”的子对象。这些子对象anchorPoint就是可操作顶点生成的位置。可以随时点击“生成雷达图顶点”按钮来重新生成这些点。
然后点击“生成内部可操作顶点”的按钮,这些按钮的样子可以在“顶点颜色”、“顶点图片”和“顶点大小”中设置。这些可操作顶点的位置和Point的中心重合。可以随时点击“生成内部可操作顶点”按钮来重新生成这些点。
生成Mesh 修改RadarChart
脚本
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 protected override void OnPopulateMesh (VertexHelper toFill ){ toFill.Clear(); AddVerts(toFill); AddTriangle(toFill); } private void AddVerts (VertexHelper toFill ){ for (int i = 0 ; i < mChartHandlers.Count; i++) { toFill.AddVert(mChartHandlers[i].transform.localPosition, color, Vector4.zero); } } private void AddTriangle (VertexHelper toFill ){ for (int i = 1 ; i < mPointCount - 1 ; i++) { toFill.AddTriangle(0 ,i,i+1 ); } }
注意,这里把uv直接设置成了Vector4.zero,如果需要贴图的话需要参考CircleImage
脚本来设置uv。
注意,这里说的“五边形只需要三个三角形就可以绘制”是以凸多边形作为讨论的,因为雷达图其实可以是凹多边形,所以这里的三角形绘制方式仅供参考。后面会修改绘制方式。
一般情况下,Unity的三角形顺时针是正面,逆时针是背面,这里的三角形顶点顺序是逆时针的,但是依然可以绘制,因为UnityUI的默认shader是关闭了剔除的:Cull Off
如果想要顺时针,修改成toFill.AddTriangle(0,i+1,i);
移动Handler来重绘Mesh 每次我们修改RadarChart组件的Inspector面板时,OnPopulateMesh
都会更新,但是我们如果在Scene面板拖动Handler,OnPopulateMesh
是不会更新的。此时我们在RadarChart
脚本中添加如下的方法:
1 2 3 4 private void Update (){ SetVerticesDirty(); }
这个方法的意思是每一帧都给Image的顶点设置脏标记,这个标记意味着此时顶点是可以更新并移动的。这样我们不需要每次都修改Inspector面板,也能控制顶点的位置了。
此时我们在Scene窗口中移动Handler,顶点也会跟着移动了。
运行时移动Handler来重绘Mesh 修改RadarChartHandler
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 using UnityEngine.EventSystems;using UnityEngine.UI;public class RadarChartHandler : MonoBehaviour ,IDragHandler { public void OnDrag (PointerEventData eventData ) { Rect.anchoredPosition += eventData.delta / GetScale(); } private float GetScale () { return Rect.lossyScale.x; } }
如果我们的RadarChart组件经过父级层层放大,我们直接调用Rect.anchoredPosition += eventData.delta
会让移动效果放大响应的倍数。所以需要除Rect.lossyScale.x
,lossyScale记录的是层层放大后倍数的总量,比如RadarChart的父级放大倍数是2倍,父级的父级放大倍数是4倍,那么此时的lossyScale就是8.
关于uv 由于雷达图多种多样,每个顶点的uv坐标需要我们单独指定。
这里我们假设生成的是一张5边形雷达图,我们给RadarChart
添加新的AddVertsTemplate
方法
1 2 3 4 5 6 7 8 private void AddVertsTemplate (VertexHelper vh ){ vh.AddVert(mChartHandlers[0 ].transform.localPosition, color, new Vector2(0.5f ,1f )); vh.AddVert(mChartHandlers[1 ].transform.localPosition, color, new Vector2(0f ,1f )); vh.AddVert(mChartHandlers[2 ].transform.localPosition, color, new Vector2(0f ,0f )); vh.AddVert(mChartHandlers[3 ].transform.localPosition, color, new Vector2(1f ,0f )); vh.AddVert(mChartHandlers[4 ].transform.localPosition, color, new Vector2(1f ,1f )); }
假设我们把一张方形贴图贴在五边形内,我们把每一个顶点的uv一个一个加进去,这样就能得到一张拉伸的图片
修改Bug 修改上面生成Mesh时,如果是凹多边形生成的面会重合的Bug。
我们可以想办法防止生成重合的面,但是之前的绘制方式是无法绘制凹多边形的。
如上图所示,如果想防止生成重合的面,我们先计算出0——2所在的直线,然后判断和顶点1相同y坐标的在直线0——2上的点,也就是m点,m点的x坐标比顶点1的x坐标大,那么就绘制这个三角面,否则不绘制。
这个原理和之前CircleImage判断点击位置的原理相同。但是这样只能防止生成重合的面,不能绘制出凹多边形。
我们直接在多边形中央添加一个虚拟顶点来解决这个问题。
修改RadarChart
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 private void AddVerts (VertexHelper toFill ){ toFill.AddVert(Vector3.zero, color, Vector4.zero); for (int i = 0 ; i < mChartHandlers.Count; i++) { toFill.AddVert(mChartHandlers[i].transform.localPosition, color, Vector4.zero); } } private void AddTriangle (VertexHelper toFill ){ for (int i = 1 ; i < mPointCount; i++) { toFill.AddTriangle(0 ,i+1 ,i); } toFill.AddTriangle(0 , mPointCount, 1 ); }