实现效果
实现:
以五边形的雷达图为例
1.创建RadarChart类,继承自Image。
由于需要改变ui顶点数据,需要使用以下方法,所以继承自Image。
protected override void OnPopulateMesh(VertexHelper toFill);
- 初始化顶点
可给定handler与point的位置比例,来初始化handler的位置,没有则默认为point的位置。
RadarChart类中实现:
private List<RectTransform> m_Points;//顶点
private int m_PointCount = 5;
public void InitPoints()
{
m_Points = new List<RectTransform>();
SpawnPoints();
SetPointsPos();
}
//生成点
private void SpawnPoints()
{
for (int i = 0; i < m_PointCount; i++)
{
GameObject point = new GameObject("Point" + i);
point.transform.SetParent(transform);
RectTransform rt = point.AddComponent<RectTransform>();
m_Points.Add(rt);
}
}
//设置点位置
private void SetPointsPos()
{
float radian = 2 * Mathf.PI / m_PointCount;//单位弧度
float radius = 100;
float curRadian = Mathf.PI / 2;//起始弧度
for (int i = 0; i < m_PointCount; i++)
{
float x = radius * Mathf.Cos(curRadian);
float y = radius * Mathf.Sin(curRadian);
m_Points[i].anchoredPosition = new Vector2(x, y);
curRadian += radian;
}
}
3.初始化可拖动的handler
- 新建RadarChartHandler.cs脚本
using UnityEngine;
using UnityEngine.UI;
public class RadarChartHandler : MonoBehaviour
{
private Image m_Image;
private Image Image
{
get
{
if (m_Image == null)
{
m_Image = GetComponent<Image>();
}
return m_Image;
}
}
private RectTransform m_RectTransform;
private RectTransform RectTrans
{
get
{
if (m_RectTransform == null)
{
m_RectTransform = GetComponent<RectTransform>();
}
return m_RectTransform;
}
}
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)
{
RectTrans.anchoredPosition = _pos;
}
public void SetSize(Vector2 _size)
{
RectTrans.sizeDelta = _size;
}
}
- RadarChart类中实现:
private List<RadarChartHandler> m_HandlerList;//可改变的点
private float[] m_HandlerRatio;//handler与定点point位置的比例
private Sprite m_HandlerSprite;
private Color m_HandlerColor = Color.white;
private Vector2 m_HandlerSize = new Vector2(10, 10);
public void InitHandlers()
{
m_HandlerList = new List<RadarChartHandler>();
SpawnHandlers();
SetHandlersPos();
}
private void ClearHandlers()
{
if (m_HandlerList == null)
return;
else
{
foreach (RadarChartHandler handler in m_HandlerList)
{
if (handler)
{
DestroyImmediate(handler.gameObject);
}
}
}
m_HandlerList = null;
}
private void SpawnHandlers()
{
RadarChartHandler handler = null;
for (int i = 0; i < m_PointCount; i++)
{
GameObject point = new GameObject("Handler" + i);
point.AddComponent<RectTransform>();
point.AddComponent<Image>();
handler = point.AddComponent<RadarChartHandler>();
handler.SetParent(transform);
handler.ChangeSprite(m_HandlerSprite);
handler.ChangeColor(m_HandlerColor);
handler.SetSize(m_HandlerSize);
m_HandlerList.Add(handler);
}
}
private void SetHandlersPos()
{
if (m_HandlerRatio == null || m_HandlerRatio.Length != m_PointCount)
{
for (int i = 0; i < m_PointCount; i++)
{
m_HandlerList[i].SetPos(m_Points[i].anchoredPosition);
}
}
else
{
for (int i = 0; i < m_PointCount; i++)
{
m_HandlerList[i].SetPos(m_Points[i].anchoredPosition * m_HandlerRatio[i]);
}
}
}
3.生成三角面
确定三角形:
5个handler位置可以划分多个三角形,尽量选取较少的三角形的方式:
RadarChart.cs中:
由于继承自Image,所以可以重写OnPopulateMesh
protected override void OnPopulateMesh(VertexHelper vh)
{
vh.Clear();
AddVerts(vh);
AddTriangle(vh);
}
//添加顶点
protected void AddVerts(VertexHelper vh)
{
if (m_HandlerList == null) return;
foreach (RadarChartHandler handler in m_HandlerList)
{
if(handler) vh.AddVert(handler.transform.localPosition, color, Vector2.zero);
}
}
//添加三角形面片
private void AddTriangle(VertexHelper vh)
{
if (vh.currentVertCount < m_PointCount) return;
if (m_HandlerList == null) return;
for (int i = 0; i < m_PointCount - 2; i++)
{
vh.AddTriangle(0, i + 2, i + 1);
}
}
这样确定的三角形,拖动实现后有bug:
重新确定三角形:
以中心点确立三角形
代码更改:
//添加顶点
protected void AddVerts(VertexHelper vh)
{
if (m_HandlerList == null) return;
vh.AddVert(Vector3.zero, color, Vector2.zero);//添加原点
foreach (RadarChartHandler handler in m_HandlerList)
{
if (handler) vh.AddVert(handler.transform.localPosition, color, Vector2.zero);
}
}
//添加三角形面片
private void AddTriangle(VertexHelper vh)
{
if (vh.currentVertCount < m_PointCount) return;
if (m_HandlerList == null) return;
for (int i = 1; i <= m_PointCount; i++)
{
vh.AddTriangle(0, i % m_PointCount + 1, i);
}
}
- 自定义编辑器脚本实现
目的:
(1)使RadarChart字段显示在面板上
(2)在非运行时调用生成顶点和handler
- 注:
(1)不管非运行模式还是运行模式下,当脚本挂载到对象上时,脚本对象就创建了,其变量便存在。
(2)在非运行模式下生成的点会一直存在。
综上,所以需要实现清除生成的点和清空缓存
RadarChart.cs中:
public void InitPoints()
{
ClearPoints();//调用
//m_Points = new List<RectTransform>();
//SpawnPoints();
//SetPointsPos();
}
private void ClearPoints()
{
if (m_Points == null)
return;
foreach (RectTransform point in m_Points)
{
if (point != null)
DestroyImmediate(point.gameObject);
}
m_Points = null;
}
public void InitHandlers()
{
ClearHandlers();//调用
//m_HandlerList = new List<RadarChartHandler>();
//SpawnHandlers();
//SetHandlersPos();
}
private void ClearHandlers()
{
if (m_HandlerList == null)
return;
else
{
foreach (RadarChartHandler handler in m_HandlerList)
{
if (handler)
{
DestroyImmediate(handler.gameObject);
}
}
}
m_HandlerList = null;
}
public void ClearAll()
{
ClearPoints();
ClearHandlers();
}
- 新建类RadarChartEditor继承自UnityEditor.UI.ImageEditor:
ImageEditor:Custom Editor for the Image Component.
注:RadarChart中的静态变量需要添加[SerializeField]
using UnityEditor;
using UnityEngine;
[CustomEditor(typeof(RadarChart),true)]
public class RadarChartEditor : UnityEditor.UI.ImageEditor
{
SerializedProperty _pointCount;
SerializedProperty _pointSprite;
SerializedProperty _pointColor;
SerializedProperty _pointSize;
SerializedProperty _handlerRatio;
protected override void OnEnable()
{
base.OnEnable();
_pointCount = serializedObject.FindProperty("m_PointCount");
_pointSprite = serializedObject.FindProperty("m_HandlerSprite");
_pointColor = serializedObject.FindProperty("m_HandlerColor");
_pointSize = serializedObject.FindProperty("m_HandlerSize");
_handlerRatio = serializedObject.FindProperty("m_HandlerRatio");
}
public override void OnInspectorGUI()
{
base.OnInspectorGUI();
serializedObject.Update();
//自定义控件
EditorGUILayout.PropertyField(_pointCount);
EditorGUILayout.PropertyField(_pointSprite);
EditorGUILayout.PropertyField(_pointColor);
EditorGUILayout.PropertyField(_pointSize);
EditorGUILayout.PropertyField(_handlerRatio, true);
//EditorGUILayout.Slider(_handlerRatio, 0, 1, new GUIContent("_handlerRadio"));
RadarChart radar = target as RadarChart;
if (radar != null)
{
if (GUILayout.Button("生成雷达图顶点"))
{
radar.InitPoints();
}
if (GUILayout.Button("生成内部可操作顶点"))
{
radar.InitHandlers();
}
if (GUILayout.Button("ClearAll"))
{
radar.ClearAll();
}
}
serializedObject.ApplyModifiedProperties();
if (GUI.changed)
{
EditorUtility.SetDirty(target);
}
}
}
效果:
5.运行时实现handler的拖动
RadarChart.cs中:
void Update()
{
// Mark the vertices as dirty,实时刷新ui
SetVerticesDirty();
}
RadarChartHandler.cs中:
public class RadarChartHandler : MonoBehaviour,IDragHandler
{
public void OnDrag(PointerEventData eventData)
{
RectTrans.anchoredPosition += eventData.delta;
}
}
bug:当对象父物体结构中存在缩放时,即相对于localScale有缩放,eventData.delta的值是缩放后的值,表现为不跟手。
改:
使用lossyScale来获取物体的全局缩放,再用lossyScale与localScale的比值求得父物体结构的缩放。
public class RadarChartHandler : MonoBehaviour,IDragHandler
{
public void OnDrag(PointerEventData eventData)
{
//如果图片有缩放,移动不跟随鼠标
float x = eventData.delta.x / (transform.lossyScale.x/transform.localScale.x);
float y = eventData.delta.y / (transform.lossyScale.y/transform.localScale.y);
RectTrans.anchoredPosition += new Vector2(x,y);
}
}
总结
- unity是左手坐标系
- 面剔除就是指检查一个面相对于摄像机也就是观察者的朝向是顺时针还是逆时针,以三角形举例,如果一个三角形我们从正面看它是顺时针,那么背面就是逆时针,以此来区分正面和被面。
- UI默认的shader关闭了背面剔除,所以添加三角形时,点为顺时针或逆时针形成的三角形都可见。
- 开启shaded wireframe,可以在scene窗口看到三角面片
完整代码
RadarChart .cs
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.UI;
public class RadarChart : Image
{
[SerializeField]
private int m_PointCount = 5;
[SerializeField]
private List<RectTransform> m_Points;//顶点
[SerializeField]
private List<RadarChartHandler> m_HandlerList;//可改变的点
[SerializeField]
private Vector2 m_HandlerSize = new Vector2(10, 10);
[SerializeField]
private Color m_HandlerColor = Color.white;
[SerializeField]
private Sprite m_HandlerSprite;
[SerializeField]
private float[] m_HandlerRatio;//handler的比例
Update is called once per frame
void Update()
{
//刷新
SetVerticesDirty();
}
protected override void OnPopulateMesh(VertexHelper vh)
{
vh.Clear();
AddVerts(vh);
AddTriangle(vh);
}
//实现图片贴合雷达图,存在拉伸
//private void AddVertsTemplate(VertexHelper vh)
//{
// vh.AddVert(m_HandlerList[0].transform.localPosition, color, new Vector2(0.5f,1));
// vh.AddVert(m_HandlerList[1].transform.localPosition, color, new Vector2(0, 1));
// vh.AddVert(m_HandlerList[2].transform.localPosition, color, new Vector2(0, 0));
// vh.AddVert(m_HandlerList[3].transform.localPosition, color, new Vector2(0.5f, 0));
// vh.AddVert(m_HandlerList[4].transform.localPosition, color, new Vector2(1, 1));
//}
//添加顶点
protected void AddVerts(VertexHelper vh)
{
if (m_HandlerList == null) return;
vh.AddVert(Vector3.zero, color, Vector2.zero);//添加原点
foreach (RadarChartHandler handler in m_HandlerList)
{
if (handler) vh.AddVert(handler.transform.localPosition, color, Vector2.zero);
}
}
//添加三角形面片
private void AddTriangle(VertexHelper vh)
{
if (vh.currentVertCount < m_PointCount) return;
if (m_HandlerList == null) return;
for (int i = 1; i <= m_PointCount; i++)
{
//剔除点
//Vector2 v1 = m_HandlerList[i - 1].transform.localPosition;
//Vector2 v2 = m_HandlerList[i % m_PointCount].transform.localPosition;
//Vector3 normal = Vector3.Cross(v1, v2);
//Debug.Log("normal:" + normal);
//if (normal.z < 0) continue;//unity是左手坐标系
vh.AddTriangle(0, i % m_PointCount + 1, i);
}
}
public void InitPoints()
{
ClearPoints();
m_Points = new List<RectTransform>();
SpawnPoints();
SetPointsPos();
}
private void ClearPoints()
{
if (m_Points == null)
return;
foreach (RectTransform point in m_Points)
{
if (point != null)
DestroyImmediate(point.gameObject);
}
m_Points = null;
}
//生成点
private void SpawnPoints()
{
for (int i = 0; i < m_PointCount; i++)
{
GameObject point = new GameObject("Point" + i);
point.transform.SetParent(transform);
RectTransform rt = point.AddComponent<RectTransform>();
m_Points.Add(rt);
}
}
//设置点位置
private void SetPointsPos()
{
float radian = 2 * Mathf.PI / m_PointCount;//单位弧度
float radius = 100;
float curRadian = Mathf.PI / 2;//起始弧度
for (int i = 0; i < m_PointCount; i++)
{
float x = radius * Mathf.Cos(curRadian);
float y = radius * Mathf.Sin(curRadian);
m_Points[i].anchoredPosition = new Vector2(x, y);
curRadian += radian;
}
}
public void InitHandlers()
{
ClearHandlers();
m_HandlerList = new List<RadarChartHandler>();
SpawnHandlers();
SetHandlersPos();
}
private void ClearHandlers()
{
if (m_HandlerList == null)
return;
else
{
foreach (RadarChartHandler handler in m_HandlerList)
{
if (handler)
{
DestroyImmediate(handler.gameObject);
}
}
}
m_HandlerList = null;
}
private void SpawnHandlers()
{
RadarChartHandler handler = null;
for (int i = 0; i < m_PointCount; i++)
{
GameObject point = new GameObject("Handler" + i);
point.AddComponent<RectTransform>();
point.AddComponent<Image>();
handler = point.AddComponent<RadarChartHandler>();
handler.SetParent(transform);
handler.ChangeSprite(m_HandlerSprite);
handler.ChangeColor(m_HandlerColor);
handler.SetSize(m_HandlerSize);
m_HandlerList.Add(handler);
}
}
private void SetHandlersPos()
{
if (m_HandlerRatio == null || m_HandlerRatio.Length != m_PointCount)
{
for (int i = 0; i < m_PointCount; i++)
{
m_HandlerList[i].SetPos(m_Points[i].anchoredPosition);
}
}
else
{
for (int i = 0; i < m_PointCount; i++)
{
m_HandlerList[i].SetPos(m_Points[i].anchoredPosition * m_HandlerRatio[i]);
}
}
}
public void ClearAll()
{
ClearPoints();
ClearHandlers();
SetVerticesDirty();
}
}
RadarChartHandler.cs
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.UI;
using UnityEngine.EventSystems;
using System;
//缩放对移动时,对handler跟随鼠标的影响
public class RadarChartHandler : MonoBehaviour,IDragHandler
{
private Image m_Image;
private Image Image
{
get
{
if (m_Image == null)
{
m_Image = GetComponent<Image>();
}
return m_Image;
}
}
private RectTransform m_RectTransform;
private RectTransform RectTrans
{
get
{
if (m_RectTransform == null)
{
m_RectTransform = GetComponent<RectTransform>();
}
return m_RectTransform;
}
}
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)
{
RectTrans.anchoredPosition = _pos;
}
public void SetSize(Vector2 _size)
{
RectTrans.sizeDelta = _size;
}
public void OnDrag(PointerEventData eventData)
{
float x = eventData.delta.x / (transform.lossyScale.x / transform.localScale.x);
float y = eventData.delta.y / (transform.lossyScale.y / transform.localScale.y);
Debug.Log("x = " + transform.lossyScale.x);
Debug.Log("y = " + transform.lossyScale.y);
RectTrans.anchoredPosition += new Vector2(x, y);
}
}
Editor下的RadarChartEditor.cs
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEditor;
[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.InitHandlers();
}
}
serializedObject.ApplyModifiedProperties();
if (GUI.changed)
{
EditorUtility.SetDirty(target);
}
}
}