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

一、效果展示

二、实现过程

2.1 准备工作

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

public class RadarChart : Image
{
    
    
}

2.2 生成顶点

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

[SerializeField]  
private int _pointCount;  
[SerializeField]  
private List<RectTransform> _points;

/// <summary>
/// 创建顶点
/// </summary>
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)点为圆心,让生成的点位于雷达图中心与顶点的连线上

/// <summary>
/// 设置顶点初始位置
/// </summary>
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中已存在的顶点。这些方法统一在初始化顶点时执行

/// <summary>
/// 初始化顶点
/// </summary>
public void InitPoint()
{
    
    
	ClearPoint();
	_points = new List<RectTransform>();
	SpawnPoint();
	SetPointPos();
}
/// <summary>
/// 清除点
/// </summary>
private void ClearPoint()
{
    
    
	if(_points == null)
		return;
	foreach (var point in _points)
	{
    
    
		if(point != null)
			DestroyImmediate(point);
	}
}

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;
        }
    }
    /// <summary>
    /// 设置父物体
    /// </summary>
    /// <param name="parentTrans"></param>
    public void SetParent(Transform parentTrans)
    {
    
    
        transform.SetParent(parentTrans);
    }
    /// <summary>
    /// 设置大小
    /// </summary>
    /// <param name="size"></param>
    public void SetSize(Vector2 size)
    {
    
    
        Rect.sizeDelta = size;
    }
    /// <summary>
    /// 设置图片
    /// </summary>
    /// <param name="sprite"></param>
    public void SetSprite(Sprite sprite)
    {
    
    
        Img.sprite = sprite;
    }
    /// <summary>
    /// 设置颜色
    /// </summary>
    /// <param name="color"></param>
    public void SetColor(Color color)
    {
    
    
        Img.color = color;
    }
    /// <summary>
    /// 设置位置
    /// </summary>
    /// <param name="pos"></param>
    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;
/// <summary>
/// 初始化操作点
/// </summary>
public void InitHandler()
{
    
    
	ClearHandler();
	_handlers = new List<RadarChartHandler>();
	SpawnHandler();
	SetHandlerPos();
}

/// <summary>
/// 清除操作点
/// </summary>
private void ClearHandler()
{
    
    
	if(_handlers == null)
		return;
	foreach (var point in _handlers)
	{
    
    
		if(point != null)
			DestroyImmediate(point);
	}
}
/// <summary>
/// 创建操作点
/// </summary>
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);
	}
}
/// <summary>
/// 设置操作点位置
/// </summary>
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]);
		}
	}
}

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);
        }

    }
}

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

2.5 填充雷达图

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

protected override void OnPopulateMesh(VertexHelper toFill)
{
    
    
	toFill.Clear();
	AddVert(toFill);
	AddTriangle(toFill);
}
/// <summary>
/// 添加顶点
/// </summary>
/// <param name="toFill"></param>
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);
	}
}
/// <summary>
/// 添加三角形
/// </summary>
/// <param name="toFill"></param>
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;
}

效果如下


源码下载

猜你喜欢

转载自blog.csdn.net/LWR_Shadow/article/details/126804838