二:实现思路
——描边思路:一开始觉得这很简单,就直接绘制一个比当前大一圈的雷达图就可以了,试了试发现并没有那么简单
当每个顶点的权重都相同时,也就是雷达图为正多边形时绘制出来的描边是等宽的,但是因为雷达图每个顶点的权重是不均匀的,所以会导致描边的宽度也不均匀,效果如下
解决办法是对于每一个顶点,都找出上一个顶点和下一个顶点,并且得到它相邻的两条边的向外的垂线,沿着垂线向外延伸相同宽度后得到两个点,再把两个点沿着边的方向延伸相交于一点,对每个顶点重复上述步骤,就能得到描边外框的所有点了
例如对于顶点B,找到上一个顶点A和下一个顶点C,沿着垂线向外延伸相同宽度后得到两个点A1和C1,再将A1和C1沿着边的方向延伸相交与点B1,再对顶点C进行同样的操作找到交点C1.....
——求每条边向外的垂线方向向量:因为是求某条边的垂直向量,所以可以利用两个向量的点乘结果为0的性质来求,但是每条边的垂直向量都有两个,我们需要知道哪个是向外的垂直方向,经过如下推导可求得
对于顶点A,向量DA的符号为(+,+),向外垂直向量DD1的符号为(+,-),向量BA的符号为(+,-),向外的垂直向量BB1的符号为(+,+)
对于顶点B,向量AB的符号为(-,+),向外垂直向量AA1的符号为(+,+),向量CB的符号为(+,+),向外的垂直向量CC1的符号为(-,+)
结论:对于向量为(x,y),上一个顶点的边的向外垂直向量为(y,-x)。下一个顶点的边的向外垂直向量为(-y,x)
三:使用
——面板参数设置
Sprite:雷达图内部图片
Color:颜色
Raycast Target:是否接收射线
Radius:半径
Side Count:几边形(有几个顶点)
Show Inner:是否显示雷达图内部
Show Outline:是否显示雷达图描边
OutlineData-Width:描边宽度
OutlineData-Color:描边颜色
——常规使用
雷达图的背景一般都是美术提供素材,调整RadarChart的Radius大小使雷达图与美术素材一致并设置RadarChart组件的相关参数
SetRadarChart:设置雷达图
SetRatioList:设置雷达图每个顶点的比例值
using System.Collections.Generic;
using UnityEngine;
public class Test : MonoBehaviour
{
public RadarChart radarChart;
private void Awake()
{
List ratioList = new List() { 0.5f, 0.4f, 0.3f, 0.5f, 0.7f, 1f };
radarChart.SetRadarChart();
radarChart.SetRatioList(ratioList);
}
}
——其他使用
OnRatioValueChanged:当比例值变化后的回调
四:代码实现
using UnityEngine; using UnityEngine.Sprites; using UnityEngine.UI; using System.Collections.Generic; using System; ////// 雷达图组件 /// [AddComponentMenu("LFramework/UI/RadarChart", 51)] public class RadarChart : MaskableGraphic { protected RadarChart() { } ////// 描边数据 /// [Serializable] public class OutlineData { [SerializeField] public float width = 5; [SerializeField] public Color color = Color.red; } //Sprite图片 [SerializeField] Sprite m_Sprite; public Sprite Sprite { get { return m_Sprite; } } //贴图 public override Texture mainTexture { get { if (m_Sprite == null) { if (material != null && material.mainTexture != null) { return material.mainTexture; } return s_WhiteTexture; } return m_Sprite.texture; } } //半径 [SerializeField] float m_Radius = 100; //边数(几边形) [SerializeField] int m_SideCount; public int SideCount { get { m_SideCount = Mathf.Clamp(m_SideCount, 3, 65000); return m_SideCount; } } //是否显示雷达图内部 [SerializeField] bool m_ShowInner = true; //是否显示雷达图描边 [SerializeField] bool m_ShowOutline; //雷达图描边数据 [SerializeField] OutlineData m_OutlineData; //比例值列表 Listm_RatioList = new List (); //顶点位置列表 List m_TempVertexList = new List (); List m_VertexList = new List (); //比例值变化后 public Action OnRatioValueChanged; /// /// 初始化比例值列表 /// void InitRatioList() { int ratioCount = m_RatioList.Count; if (ratioCount < SideCount) { for (int i = 0; i < SideCount - ratioCount; i++) { m_RatioList.Add(1); } } } ////// 设置比例值列表 /// public void SetRatioList(ListratioList) { for (int i = 0; i < m_RatioList.Count; i++) { if (ratioList.Count - 1 >= i) { m_RatioList[i] = ratioList[i]; } } SetVerticesDirty(); CalcVertexPos(); OnRatioValueChanged?.Invoke(); } /// /// 设置雷达图 /// public void SetRadarChart() { rectTransform.sizeDelta = new Vector2(m_Radius * 2, m_Radius * 2); InitRatioList(); } ////// 得到比例值列表 /// public ListGetRatioList() { return m_RatioList; } protected override void OnPopulateMesh(VertexHelper vh) { vh.Clear(); m_TempVertexList.Clear(); GenerateInner(vh); if (m_ShowOutline) { GenerateOutline(vh); } } /// /// 生成雷达图内部 /// void GenerateInner(VertexHelper vh) { Vector4 uv = m_Sprite == null ? Vector4.zero : DataUtility.GetOuterUV(m_Sprite); float uvWidth = uv.z - uv.x; float uvHeight = uv.w - uv.y; float diameter = m_Radius * 2; Vector2 uvCenter = new Vector2((uv.x + uv.z) * 0.5f, (uv.y + uv.w) * 0.5f); Vector3 posCenter = new Vector2((0.5f - rectTransform.pivot.x) * diameter, (0.5f - rectTransform.pivot.y) * diameter); float uvScaleX = uvWidth / diameter; float uvScaleY = uvHeight / diameter; float deltaRad = 2 * Mathf.PI / SideCount; float curRad = 0; int vertexCount = SideCount + 1; int triangleCount = SideCount; UIVertex vertex = new UIVertex(); vh.AddVert(posCenter, color, uvCenter); for (int i = 0; i < vertexCount - 1; i++) { float r = m_RatioList.Count <= i ? m_Radius : m_RatioList[i] == 0 ? m_Radius : m_Radius * m_RatioList[i]; Vector3 posOffset = new Vector3(r * Mathf.Cos(curRad), r * Mathf.Sin(curRad)); vertex.position = posCenter + posOffset; vertex.color = color; vertex.uv0 = new Vector2(uvCenter.x + posOffset.x * uvScaleX, uvCenter.y + posOffset.y * uvScaleY); vh.AddVert(vertex); m_TempVertexList.Add(vertex.position); curRad += deltaRad; } if (m_ShowInner) { for (int i = 0; i < triangleCount; i++) { vh.AddTriangle(0, i + 1, i + 2 >= vertexCount ? 1 : i + 2); } } } ////// 生成雷达图描边 /// void GenerateOutline(VertexHelper vh) { int vertexCount = m_TempVertexList.Count + 1; int triangleCount = m_TempVertexList.Count * 2; for (int i = 0; i < m_TempVertexList.Count; i++) { Vector2 curPos = m_TempVertexList[i]; Vector2 prePos = i - 1 < 0 ? m_TempVertexList[m_TempVertexList.Count - 1] : m_TempVertexList[i - 1]; Vector2 nextPos = m_TempVertexList[(i + 1) % m_TempVertexList.Count]; Vector2 dir1 = (curPos - prePos).normalized; Vector2 dir2 = (curPos - nextPos).normalized; Vector2 normal1 = GetNormal(dir1); Vector2 normal2 = GetNormal(-dir2); Vector2 pos1 = prePos + normal1 * m_OutlineData.width; Vector2 pos2 = nextPos + normal2 * m_OutlineData.width; Vector2 crossPoint = GetCrossPoint(pos1, dir1, pos2, dir2); vh.AddVert(curPos, m_OutlineData.color, Vector2.zero); vh.AddVert(crossPoint, m_OutlineData.color, Vector2.zero); } for (int i = vertexCount; i < m_TempVertexList.Count * 3 + 1; i += 2) { vh.AddTriangle(i, i + 1, i + 3 >= m_TempVertexList.Count * 3 + 1 ? m_TempVertexList.Count + 2 : i + 3); vh.AddTriangle(i, i + 2 >= m_TempVertexList.Count * 3 + 1 ? m_TempVertexList.Count + 1 : i + 2, i + 3 >= m_TempVertexList.Count * 3 + 1 ? m_TempVertexList.Count + 2 : i + 3); } } ////// 得到法线 /// Vector2 GetNormal(Vector2 dir) { return new Vector2(dir.y, -dir.x); } //误差范围 const float ERROR_RANGE = 0.001f; ////// 得到交点 /// Vector2 GetCrossPoint(Vector2 pos1, Vector2 dir1, Vector2 pos2, Vector2 dir2) { bool parallelToY1 = false; bool parallelToY2 = false; float k1; float k2; if (Mathf.Abs(dir1.x) <= ERROR_RANGE || Mathf.Abs(dir1.y) <= ERROR_RANGE) { k1 = 0; if (Mathf.Abs(dir1.x) <= ERROR_RANGE) { parallelToY1 = true; } } else { k1 = dir1.y / dir1.x; } if (Mathf.Abs(dir2.x) <= ERROR_RANGE || Mathf.Abs(dir2.y) <= ERROR_RANGE) { k2 = 0; if (Mathf.Abs(dir2.x) <= ERROR_RANGE) { parallelToY2 = true; } } else { k2 = dir2.y / dir2.x; } float b1 = pos1.y - k1 * pos1.x; float b2 = pos2.y - k2 * pos2.x; if (parallelToY1) { float x = pos1.x; float y = k2 * x + b2; return new Vector2(x, y); } else if (parallelToY2) { float x = pos2.x; float y = k1 * x + b1; return new Vector2(x, y); } else { float x = (b2 - b1) / (k1 - k2); float y = k1 * x + b1; return new Vector2(x, y); } } ////// 计算顶点位置 /// void CalcVertexPos() { m_VertexList.Clear(); float diameter = m_Radius * 2; Vector3 posCenter = new Vector2((0.5f - rectTransform.pivot.x) * diameter, (0.5f - rectTransform.pivot.y) * diameter); float deltaRad = 2 * Mathf.PI / SideCount; float curRad = 0; for (int i = 0; i < SideCount; i++) { float r = m_RatioList.Count <= i ? m_Radius : m_RatioList[i] == 0 ? m_Radius : m_Radius * m_RatioList[i]; Vector3 pos = posCenter + new Vector3(r * Mathf.Cos(curRad), r * Mathf.Sin(curRad)); m_VertexList.Add(pos); curRad += deltaRad; } } }



