
首先导入一张雷达图的背景图,将其挂载到「Image」上添加到场景中,命名为「RadarBg」。

在「RadarBg」下添加一个空的子物体,命名为「RadarChart」并挂载同名脚本。它用来展示雷达图上层的数据信息。「RadarChart」需要继承「Image」类。
public class RadarChart : Image
{
}
这里的顶点用来标记雷达图背景的范围。我们需要在编辑器中规定顶点的个数,因此创建一个成员变量_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>());
}
}
生成顶点后,需要设置顶点的默认位置。我们可以以(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);
}
}
另外,在生成顶点前,需要清除_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);
}
}
操作点是指雷达图中表示数据部分的图形的顶点。它可以根据不同的比例,在中心与顶点的连线上移动。
操作点的生成与顶点生成的过程差不多。首先我们新建一个操作点类「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;
}
}
在「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]);
}
}
}
我们只是给成员变量添加了[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);
}
}
}

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

观察上面的截图可以发现,雷达图中黄色的填充部分还没有处理。那么如何让它填充成五边形呢?这就又要用到父类中的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);
}
返回Unity,可以看到效果如下

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

为了能在运行时也可以进行拖动操作,我们给「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;
}
效果如下
