目录
[TOC]
一、UI遮挡3D物体响应
1.UI元素在3D物体之上时,只响应UI元素
- Step1:在MainCamera上添加Physics Raycaster物理射线组件
- Step2:3D物体的点击实现方式同UI元素一样:实现点击接口
IPointerClickHandler
代码:
public class ClickThreeD : MonoBehaviour, IPointerClickHandler
{
private int mIndex;
private void Start()
{
mIndex = 0;
}
public void OnPointerClick(PointerEventData eventData)
{
ChangeColor();
}
private void ChangeColor()
{
if (mIndex == 0)
{
GetComponent<MeshRenderer>().material.SetColor("_Color", Color.white);
}
else
{
GetComponent<MeshRenderer>().material.SetColor("_Color", Color.black);
}
mIndex = mIndex == 0 ? 1 : 0;
}
}
2.UI元素在3D物体之上时,同时响应UI元素和3D物体
延续1中的方法,只需要在响应UI元素时增加对3D物体的响应即可。
注意:
- UI元素(继承自
Graphic
)接收Graphic Raycaster
图形射线的检测。 - 3D物体接收
Physics Raycaster
物理射线的检测。 EventSystem.current.RaycastAll(...)
会获取上述所有的射线。
修改后的UI元素的代码
public class ClickUI : MonoBehaviour, IPointerClickHandler
{
private int mIndex;
private void Start()
{
mIndex = 0;
}
public void OnPointerClick(PointerEventData eventData)
{
ChangeColor();
//获取除当前UI元素以外的所有接受到射线检测的物体(包括图形射线、物理射线)
//执行物体身上的点击事件pointerClickHandler
ExecuteAll(eventData, ExecuteEvents.pointerClickHandler);
}
private void ExecuteAll<T>(PointerEventData eventData, ExecuteEvents.EventFunction<T> eventFunction)
where T : IEventSystemHandler
{
//获取所有射线(包括图形射线、物理射线)
List<RaycastResult> results = new List<RaycastResult>();
EventSystem.current.RaycastAll(eventData, results);
for (int i = 0; i < results.Count; i++)
{
if (results[i].gameObject != gameObject)//获取非当前物体
{
//执行指定的事件eventFunction
ExecuteEvents.Execute(results[i].gameObject, eventData, eventFunction);
}
}
}
private void ChangeColor()
{
var img = GetComponent<Image>();
if (mIndex == 0)
{
img.color = Color.red;
}
else
{
img.color = Color.blue;
}
mIndex = mIndex == 0 ? 1 : 0;
}
}
3.判断鼠标是否点击在UI上
UI元素接收Graphic Raycaster
图形射线检测。
public class MouseClick : MonoBehaviour
{
private GraphicRaycaster mGraphicRaycaster;
private void Start()
{
mGraphicRaycaster = FindObjectOfType<GraphicRaycaster>();
}
private bool IsUI()
{
//创建当前鼠标点击位置的点击事件参数PointerEventData
PointerEventData eventData = new PointerEventData(EventSystem
.current)
{
//pressPosition = Input.mousePosition,
position = Input.mousePosition,
};
//获取图形射线响应的射线结果
List<RaycastResult> results = new List<RaycastResult>();
mGraphicRaycaster.Raycast(eventData, results);
//存在图形射线响应的结果说明点击位置存在UI元素
return results.Count > 0;
}
}
二、用顶点描绘圆形图片–制作技能图标(精确点击响应)
技能图标一般为圆形,UGUI没有现成的圆形Image组件来制作技能图标。所以做圆形技能图标需要用到Mask组件,使用圆形图片作为遮罩,但使用Mask组件会带来很大的性能开销,增加2个Draw Call。为了避免使用Mask组件,所以可以自己制作一个圆形Image组件。
圆形可以看做是由多个三角形组合而成,为此可以设置参数mSegements
(圆分割的份数)
为了制作技能CD效果,需要记录技能CD的时间百分比,用圆形中小三角形填充的百分比代替mFillPercent
。技能CD状态图标为灰色,可以定义CD状态图标颜色:private readonly Color32 Gray = new Color32(60, 60, 60, 255);
。
制作圆形Image步骤如下:
- Step1:创建脚本并继承自Image,重写Image自带的方法
OnPopulateMesh
(填充网格) - Step2:清空
VertexHelper
- Step3:向
VertexHelper
中添加顶点(注意消除移动轴心点pivot所带来的顶点坐标的差异) - Step4:向
VertexHelper
中添加三角形
此时已经有了一个圆形的可以表现技能CD效果的Image组件,接着需要处理图标精确点击的问题。
精确点击算法原理:判断一点是否在图形内部,方法:以该点为起点向某一方向引出一条射线,射线与图形边界交点若为奇数=>点在内部。
代码:
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.Sprites;
using UnityEngine.UI;
namespace StudyUGUIExample
{
public class CircleImage : Image
{
#region Config
private readonly Color32 Gray = new Color32(60, 60, 60, 255);
#endregion
/// <summary>
/// 填充百分比
/// </summary>
[SerializeField]
private float mFillPercent = default;
/// <summary>
/// 圆分割的份数
/// </summary>
[SerializeField]
private int mSegements = default;
private List<Vector3> mVertexList;
/// <summary>
/// 使用顶点填充网格
/// </summary>
/// <param 待填充的="toFill"></param>
protected override void OnPopulateMesh(VertexHelper toFill)
{
toFill.Clear();
AddVertex(toFill);
AddTriangle(toFill);
}
private void AddVertex(VertexHelper toFill)
{
//当前的小三角形份数
mVertexList = new List<Vector3>();
int currentSegements = (int)(mFillPercent * mSegements);
float width = GetComponent<RectTransform>().rect.width;
float height = GetComponent<RectTransform>().rect.height;
float radius = width * 0.5f;//圆半径
float radian = 2f * Mathf.PI / mSegements;//每一块的弧度
Vector4 uv = overrideSprite != null ? DataUtility.GetOuterUV(overrideSprite) : Vector4.zero;
float uvWidth = uv.z - uv.x;
float uvHeight = uv.w - uv.y;
//转换系数=uv/实际宽高
Vector2 convertRatio = new Vector2(uvWidth / width, uvHeight / height);
Vector2 uvCenter = new Vector2(0.5f * (uv.x + uv.z), 0.5f * (uv.y + uv.w));
//Vector2 uvCenter = new Vector2(0.5f * uvWidth, 0.5f * uvHeight);
//圆心
UIVertex origin = new UIVertex();
origin.color = GetColorFromFillPercent(Gray, mFillPercent);
origin.position = new Vector3((0.5f - rectTransform.pivot.x) * width, (0.5f - rectTransform.pivot.y) * height);//图形中心不随轴心点改变
origin.uv0 = new Vector2(uvCenter.x, uvCenter.y);
toFill.AddVert(origin);//添加顶点--圆心
//添加顶点--圆周
for (int i = 1; i <= mSegements + 1; i++)
{
float x = Mathf.Cos(radian * (i - 1)) * radius;
float y = Mathf.Sin(radian * (i - 1)) * radius;
UIVertex temp = new UIVertex();
if (i <= currentSegements || mFillPercent >= 1f)
{
temp.color = color;
}
else
{
temp.color = Gray;
}
temp.position = new Vector3(x + (0.5f - rectTransform.pivot.x) * width, y + (0.5f - rectTransform.pivot.y) * height);
temp.uv0 = new Vector2(x * convertRatio.x + uvCenter.x, y * convertRatio.y + uvCenter.y);
toFill.AddVert(temp);
mVertexList.Add(temp.position);
}
}
private void AddTriangle(VertexHelper toFill)
{
//添加三角形
for (int i = 1; i <= mSegements; i++)
{
toFill.AddTriangle(i, 0, i + 1);
}
}
private Color32 GetColorFromFillPercent(Color32 filledColor, float fillPercent)
{
return new Color32
{
r = (byte)(filledColor.r + (255 - filledColor.r) * fillPercent),
g = (byte)(filledColor.g + (255 - filledColor.g) * fillPercent),
b = (byte)(filledColor.b + (255 - filledColor.b) * fillPercent),
a = 255,
};
}
#region 精确点击判断
/*
* 原理:判断一点是否在图形内部
* 以该点为起点向某一方向引出一条射线,射线与图形边界交点若为奇数=>点在内部。
*/
public override bool IsRaycastLocationValid(Vector2 screenPoint, Camera eventCamera)
{
Vector2 pos;
RectTransformUtility.ScreenPointToLocalPointInRectangle(rectTransform, screenPoint, eventCamera, out pos);
return IsValid(pos, mVertexList);
}
private bool IsValid(Vector3 pos, List<Vector3> vertexList)
{
return GetCrossPointNum(pos, vertexList) % 2 == 1;//交点为奇数则pos在内部
}
private int GetCrossPointNum(Vector3 pos, List<Vector3> vertexList)
{
int count = 0;
for (int i = 0; i < vertexList.Count; i++)
{
Vector3 vertex1 = vertexList[i];
Vector3 vertex2 = vertexList[(i + 1) % vertexList.Count];
if ((pos.y < vertex2.y && pos.y > vertex1.y) || (pos.y > vertex2.y && pos.y < vertex1.y))
{
if (GetX(vertex1, vertex2, pos.y) > pos.x) count++;
}
}
return count;
}
private float GetX(Vector3 vertex1, Vector3 vertex2, float y)
{
//获取两顶点连线上的一点
return (y - vertex1.y) * (vertex2.x - vertex1.x) / (vertex2.y - vertex1.y) + vertex1.x;
}
#endregion
}
}
为了编辑方便,为该组件创建编辑扩展脚本:
using UnityEngine;
using UnityEditor;
using UnityEditor.UI;
namespace StudyUGUIExample
{
[CustomEditor(typeof(CircleImage), true), CanEditMultipleObjects]
public class CircleImageEditor : ImageEditor
{
private SerializedProperty mFillPercent;//填充百分比
private SerializedProperty mSegements;//圆分割的份数
protected override void OnEnable()
{
base.OnEnable();
mFillPercent = serializedObject.FindProperty("mFillPercent");
mSegements = serializedObject.FindProperty("mSegements");
}
public override void OnInspectorGUI()
{
base.OnInspectorGUI();
//更新序列化物体
serializedObject.Update();
EditorGUILayout.Slider(mFillPercent, 0f, 1f, new GUIContent("显示百分比"));
EditorGUILayout.PropertyField(mSegements, new GUIContent("圆分割的份数"));
//应用更改
serializedObject.ApplyModifiedProperties();
if (GUI.changed)
{
EditorUtility.SetDirty(target);
}
}
}
}
自定义的圆形Image组件编辑界面如下图:
测试效果:任意多边形+CD效果+精确点击
后续还可进行更多扩展,此处略。
三、UGUI不规则响应区域
1.UGUI自带的响应不规则区域的方法(严重影响性能,不可使用)
public class AlphaHitTest : MonoBehaviour
{
private void Start()
{
//设置透明度测试阈值
//小于此值的点不响应点击
//使用需要勾选图片设置中的“可读写”
//此法严重增加内存,同时阻止使用图集、增加DrawCall,严重影响性能,不可使用
GetComponent<Image>().alphaHitTestMinimumThreshold = 0.1f;
}
}
2.自定义Image组件,支持不规则区域响应。
主要依赖2D的不规则Collider组件:Polygon Collider 2D
代码:
public class CustomImage : Image
{
private PolygonCollider2D mCollider2D;
public PolygonCollider2D Collider2D
{
get
{
if (mCollider2D == null)
{
mCollider2D = GetComponent<PolygonCollider2D>();
}
return mCollider2D;
}
}
public override bool IsRaycastLocationValid(Vector2 screenPoint, Camera eventCamera)
{
Vector3 worldPos;
RectTransformUtility.ScreenPointToWorldPointInRectangle(rectTransform, screenPoint, eventCamera, out worldPos);
return Collider2D.OverlapPoint(worldPos);//返回:坐标是否在2D碰撞器区域内
}
}
编辑器扩展代码:
using UnityEngine;
using UnityEditor;
using UnityEngine.UI;
namespace StudyUGUIExample
{
public class CustomImageEditor : Editor
{
private const int LAYER_UI = 5;
[MenuItem("GameObject/UI/Custom Image", priority = 0)]
private static void AddCustomImage()
{
CustomImage image;
if (Selection.activeGameObject != null && Selection.activeGameObject.layer == LAYER_UI)
{
image = GenerateCustomImage();
image.transform.SetParent(Selection.activeGameObject.transform);
}
else
{
Canvas canvas = GetCanvas();
image = GenerateCustomImage();
image.transform.SetParent(canvas.transform);
}
image.transform.localPosition = Vector3.zero;
}
private static Canvas GetCanvas()
{
Canvas canvas = FindObjectOfType<Canvas>();
if (canvas == null)
{
GameObject canvasObj = new GameObject("Canvas");
canvasObj.layer = LAYER_UI;
canvas = canvasObj.AddComponent<Canvas>();
canvasObj.AddComponent<CanvasScaler>();
canvasObj.AddComponent<GraphicRaycaster>();
}
return canvas;
}
private static CustomImage GenerateCustomImage()
{
GameObject customImageObj = new GameObject("Custom Image");
customImageObj.layer = LAYER_UI;
customImageObj.AddComponent<CanvasRenderer>();
CustomImage customImage = customImageObj.AddComponent<CustomImage>();
customImageObj.AddComponent<PolygonCollider2D>();
return customImage;
}
}
}
四、使用2DImage制作仿3D轮转图
3D轮转图常见实现方法有两种:1、使用3D模型制作真3D轮转图;2、使用2D来模拟3D效果来制作轮转图。
下面将实现第二种方案。
效果如下:
设置如下:
实现原理:(以横向轮转为例,纵向类似)
- Step1:2D模拟3D效果主要依靠设置图片合适的X轴坐标
PosX
和图片缩放值Scale
。 - Step2:注意处理UI的层级,近处的需要遮挡远处的。
- Step3:加入DoTween动画。
RotationDiagrams
代码:
using System.Collections.Generic;
using System.Linq;
using UnityEngine;
using UnityEngine.UI;
using System.Collections;
namespace StudyUGUIExample
{
public class RotationDiagrams : MonoBehaviour
{
private const int LAYER_UI = 5;
private RectTransform mRect;
private float mPerimeter;
private List<DiagramData> mDiagramDatas;
private List<DiagramItem> mDiagramItems;
[Tooltip("子项大小")]
public Vector2 ItemSize;
[Tooltip("子项最大缩放")]
public float MaxScale;
[Tooltip("子项最小缩放")]
public float MinScale;
[Tooltip("轮转动画时间")]
public float AnimTime;
public List<Sprite> Sprites = new List<Sprite>();
public RectTransform Rect
{
get
{
if (mRect == null)
mRect = GetComponent<RectTransform>();
return mRect;
}
}
public int Count => Sprites.Count;
private void Awake()
{
mDiagramDatas = new List<DiagramData>();
mDiagramItems = new List<DiagramItem>();
mPerimeter = CalculatePerimeter(Rect.sizeDelta.x, Count, ItemSize.x, MaxScale, MinScale);
}
private void Start()
{
GenerateDiagrams();
UpdataOrder();
}
private void UpdataOrder()
{
//自然层级更新原理:缩放值越小自然层级越小(越靠上)
//依据缩放值排序
List<DiagramData> datas = mDiagramDatas.OrderBy((data) => data.Scale).ToList();//升序
for (int i = 0; i < datas.Count; i++)
{
datas[i].OrderIndex = i;//设置自然层级
}
for (int i = 0; i < mDiagramItems.Count; i++)
{
//更新子项的自然层级
mDiagramItems[i].transform.SetSiblingIndex(mDiagramDatas[i].OrderIndex);
}
}
private void GenerateDiagrams()
{
for (int i = 0; i < Sprites.Count; i++)
{
DiagramData data = GetDiagramData(i);
mDiagramDatas.Add(data);
GenerateDiagramItem(Sprites[i], data);
}
}
private DiagramData GetDiagramData(int index)
{
return new DiagramData
{
CurrentId = index,
PosX = GetX(GetRatio(index), mPerimeter),
Scale = GetScale(GetRatio(index), MaxScale, MinScale),
};
}
private GameObject GetTemplateItem()
{
GameObject template = new GameObject("Diagram Item");
template.layer = LAYER_UI;
template.AddComponent<RectTransform>();
template.AddComponent<CanvasRenderer>();
template.AddComponent<Image>();
template.AddComponent<DiagramItem>();
return template;
}
private void GenerateDiagramItem(Sprite sprite, DiagramData data)
{
GameObject itemGo = GetTemplateItem();
DiagramItem item = Instantiate(itemGo).GetComponent<DiagramItem>();
item.SetSprite(sprite);
item.SetParent(transform);
item.SetData(data);
item.SetSize(ItemSize);
item.AddMoveListener(Change);
mDiagramItems.Add(item);
Destroy(itemGo);
}
private float GetRatio(int index)
{
return index / (float)Count;
}
private float CalculatePerimeter(float totalWidth, int count, float itemWidth, float maxScale, float minScale)
{
return 2f * (totalWidth - itemWidth * GetScale(GetRatio((count + 1) / 4), maxScale, minScale));
}
private float GetX(float ratio, float perimeter)
{
if (ratio < 0 || ratio > 1)
{
Debug.LogError("GetX出错,比率ratio超出范围!ratio:" + ratio);
return 0f;
}
else if (ratio >= 0 && ratio < 0.25)
return perimeter * ratio;
else if (ratio >= 0.25 && ratio < 0.75)
return perimeter * (0.5f - ratio);
else
return perimeter * (ratio - 1f);
}
private float GetScale(float ratio, float max, float min)
{
if (ratio < 0 || ratio > 1)
{
Debug.LogError("GetScale出错,比率ratio超出范围!ratio:" + ratio);
return 0f;
}
else if (ratio >= 0 && ratio < 0.5)
return max - (max - min) * ratio * 2f;
else
return 2f * min - max + (max - min) * ratio * 2f;
}
private void Change(float deltaX)
{
Change(deltaX > 0 ? 1 : -1);
}
private void Change(int symbol)
{
for (int i = 0; i < mDiagramDatas.Count; i++)
{
int id = mDiagramDatas[i].CurrentId;
id += symbol;
if (id < 0)
id += Count;
else if (id >= Count)
id %= Count;
mDiagramDatas[i].CurrentId = id;
}
for (int i = 0; i < mDiagramItems.Count; i++)
{
DiagramData data = mDiagramDatas[i];
data.PosX = GetX(GetRatio(data.CurrentId), mPerimeter);
data.Scale = GetScale(GetRatio(data.CurrentId), MaxScale, MinScale);
mDiagramItems[i].SetData(data, AnimTime);
}
StartCoroutine(DelayUpdateOrder(AnimTime * 0.5f));
}
private IEnumerator DelayUpdateOrder(float delayTime)
{
WaitForSeconds delay = new WaitForSeconds(delayTime);
yield return delay;
UpdataOrder();
}
}
}
轮转图的单个图形项脚本DiagramItem
:
using System;
using UnityEngine;
using UnityEngine.UI;
using UnityEngine.EventSystems;
using DG.Tweening;
namespace StudyUGUIExample
{
public class DiagramItem : MonoBehaviour, IDragHandler, IEndDragHandler
{
private Image mImage;
private RectTransform mRect;
private float mDeltaX = 0f;
private Action<float> mMoveHandler;
public Image Image
{
get
{
if (mImage == null)
mImage = GetComponent<Image>();
return mImage;
}
}
public RectTransform Rect
{
get
{
if (mRect == null)
mRect = GetComponent<RectTransform>();
return mRect;
}
}
public void SetSize(Vector2 size)
{
Rect.sizeDelta = size;
}
public void SetSprite(Sprite sprite)
{
Image.sprite = sprite;
}
public void SetParent(Transform parent)
{
transform.SetParent(parent);
}
public void SetData(DiagramData data, float animTime = 0f)
{
if (animTime == 0f)
{
Rect.anchoredPosition = Vector3.right * data.PosX;
Rect.localScale = Vector3.one * data.Scale;
}
else
{
Rect.DOAnchorPos(Vector2.right * data.PosX, animTime);
Rect.DOScale(data.Scale, animTime);
}
}
public void OnDrag(PointerEventData eventData)
{
mDeltaX += eventData.delta.x;//累计X轴偏移
}
public void OnEndDrag(PointerEventData eventData)
{
mMoveHandler(mDeltaX);
mDeltaX = 0;//重置
}
public void AddMoveListener(Action<float> onMove)
{
mMoveHandler = onMove;
}
}
}
轮转图图形项数据DiagramData
:
namespace StudyUGUIExample
{
public class DiagramData
{
public int CurrentId;//当前序号
public float PosX;
public float Scale;
public int OrderIndex;//自然层级序号
}
}
使用的时候只需要挂载RotationDiagram
脚本到UI空物体上,然后设置该物体RectTransform的Width宽度值和RotationDiagram
脚本相关参数。
可继续扩展,增加竖向轮转的支持、动态改变等。
五、雷达图
雷达图生成原理:
- 雷达图背景需要现有的图片
- 雷达图中间填充颜色部分是在Image组件中的
OnPopulateMesh
方法中填充顶点和三角形获得的。 - 如果需要在运行时实时修改中间颜色填充部分,则需要在
Update()
中添加SetVerticesDirty();
以标记顶点信息需要实时刷新。
步骤:
- Step1:雷达图上的蓝色圆点为
RadarHandler
,使用单个脚本来处理相关逻辑。 - Step2:雷达图主要生成脚本为
RadarGraph
,脚本继承自Image,为了相关参数方便在编辑器中实时修改,所以添加其对应的编辑器扩展脚本RadarGraphEditor
- Step3:注意Handler在运行模式下实时拖动时,需要限制其拖动的方向和最大最小位置,保证其合理性。
编辑器下的参数设置:
使用方法:
-
Step1:拖拽雷达图背景到Image中,给其重命名为RadarGraphBackGround
雷达图背景:
-
Step2:设置RadarGraph组件的相关参数
-
Step3:点击初始化雷达图按钮,此时会生成一个Radar Probe,移动它,使其与雷达图背景的边缘任意一个顶点重合,其作用在于获取雷达图半径。
-
Step4:点击生成内部可移动顶点,之后会生成雷达图顶点。顶点的生成位置与Ratio参数有关,如果设置有Ratio参数,则顶点依据参数生成,未设置则生成在边缘顶点位置。
-
注意:Ratio参数的含义:比例值,例如竖直向上方向的为HP值,则Ratio表示占最大HP的比例值,取值范围在0到1之间。
使用效果:
代码:
RadarGraph
脚本:
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.UI;
namespace StudyUGUIExample
{
/*
* 默认 雷达图中心点12点钟方向存在一个边缘顶点
*/
public class RadarGraph : Image
{
[SerializeField, Tooltip("雷达图的顶点数")]
private int mVertexCount = 5;
[SerializeField, Tooltip("内部可移动顶点的图片")]
private Sprite mHandlerSprite = default;
[SerializeField, Tooltip("内部可移动顶点的颜色")]
private Color mHandlerColor = Color.black;
[SerializeField, Tooltip("填充的颜色")]
private Color mFilledColor = Color.red;
[SerializeField, Tooltip("内部可移动顶点的大小")]
private Vector2 mHandlerSize = Vector2.zero;
[SerializeField, Tooltip("各边上的占比")]
private List<float> mRatios = default;
[SerializeField]
private GameObject mProbe;//探头=>获取半径
[SerializeField]
private float mRadius;
[SerializeField]
private List<Vector2> mDirs;//标准化的方向向量List(中心点指向边缘顶点)
[SerializeField]
private List<RadarHandler> mHandlers;
private void Update()
{
SetVerticesDirty();
}
protected override void OnPopulateMesh(VertexHelper toFill)
{
if (mHandlers == null || mHandlers.Count == 0) return;
toFill.Clear();
AddVertex(toFill);
AddTriangle(toFill);
}
private void AddVertex(VertexHelper toFill)
{
//此处由于不需要贴图,所以不用管UV
toFill.AddVert(Vector3.zero, mFilledColor, Vector2.zero);
for (int i = 0; i < mVertexCount; i++)
{
if (mHandlers[i] == null)
Debug.LogError("mHandlers列表中的RadarHandler丢失,请在RadarGraph的检视面板中检查!");
toFill.AddVert(mHandlers[i].transform.localPosition, mFilledColor, Vector2.zero);
}
}
private void AddTriangle(VertexHelper toFill)
{
for (int i = 0; i < mVertexCount; i++)
{
//顺时针=>三角形正面朝上(左手法则)
toFill.AddTriangle(0, (i + 2) == (mVertexCount + 1) ? 1 : (i + 2), i + 1);
}
}
public void InitRadarGraph()
{
ClearDirs();
CalculateDirs(mDirs, mVertexCount);
ResetProbe();
GenerateProbe();
}
public void InitHandlers()
{
ClearHandlers();
CalculateRadius(mProbe);
GenerateHandlers();
}
private void GenerateProbe()//生成探头=>获取半径
{
mProbe = CreatePoint("Radar Probe");
mProbe.transform.SetParent(transform);
mProbe.GetComponent<RectTransform>().anchoredPosition = Vector2.zero;
}
private void ResetProbe()
{
if (mProbe != null)
{
DestroyImmediate(mProbe);
mProbe = null;
}
}
private void ClearDirs()
{
if (mDirs == null)
mDirs = new List<Vector2>();
mDirs.Clear();//清除标准化的方向向量List
}
private void CalculateDirs(List<Vector2> dirs, int vertexCount)
{
if (dirs == null) dirs = new List<Vector2>();
float radius = 1f;//标准化
float perRadian = 2f * Mathf.PI / vertexCount;
float currentRadian;
float x, y;
for (int i = 0; i < vertexCount; i++)
{
currentRadian = Mathf.PI * 0.5f + i * perRadian;
x = radius * Mathf.Cos(currentRadian);
y = radius * Mathf.Sin(currentRadian);
dirs.Add(new Vector2(x, y));
}
}
private void CalculateRadius(GameObject probe)
{
if (probe == null)
{
Debug.LogError("计算雷达图半径出错,请先生成probe!");
}
mRadius = Vector3.Distance(Vector3.zero, probe.transform.localPosition);
}
private void GenerateHandlers()
{
if (mDirs == null || mDirs.Count != mVertexCount)
{
Debug.LogError("标准化的方向向量List出错!"); return;
}
for (int i = 0; i < mVertexCount; i++)
{
RadarHandler handler = CreateHandler("Handler" + (i + 1), mHandlerSprite, mHandlerColor, mHandlerSize, mDirs[i]);
if (mRatios == null || mRatios.Count != mVertexCount)
SetHandlerPos(handler, mDirs[i], mRadius);
else
SetHandlerPos(handler, mDirs[i], mRadius, mRatios[i]);
mHandlers.Add(handler);
}
}
private void ClearHandlers()
{
if (mHandlers == null)
mHandlers = new List<RadarHandler>();
if (mHandlers.Count == 0) return;
for (int i = 0; i < mHandlers.Count; i++)
{
DestroyImmediate(mHandlers[i].gameObject);
}
mHandlers.Clear();
}
private void SetHandlerPos(RadarHandler handler, Vector2 dir, float radius, float ratio = 1f)
{
handler.SetPos(new Vector2(dir.x * radius * ratio, dir.y * radius * ratio));
}
private GameObject CreatePoint(string name)
{
GameObject point = new GameObject(name);
point.AddComponent<RectTransform>();
return point;
}
private RadarHandler CreateHandler(string name, Sprite sprite, Color color, Vector2 size, Vector2 dir)
{
GameObject insideVertex = CreatePoint(name);
insideVertex.AddComponent<CanvasRenderer>();
insideVertex.AddComponent<Image>();
RadarHandler handler = insideVertex.AddComponent<RadarHandler>();
handler.SetParent(transform);
handler.SetSprite(sprite);
handler.SetColor(color);
handler.SetSize(size);
handler.SetDir(dir);
handler.SetRadius(mRadius);
return handler;
}
}
}
RadarHandler
脚本:
using UnityEngine;
using UnityEngine.UI;
using UnityEngine.EventSystems;
namespace StudyUGUIExample
{
public class RadarHandler : MonoBehaviour, IDragHandler
{
[SerializeField]
private Vector2 mDir;
[SerializeField]
private float mRadius;
private RectTransform mRect;
private Image mImage;
public RectTransform Rect
{
get
{
if (mRect == null)
mRect = GetComponent<RectTransform>();
return mRect;
}
}
public Image Image
{
get
{
if (mImage == null)
mImage = GetComponent<Image>();
return mImage;
}
}
public void SetParent(Transform parent)
{
transform.SetParent(parent);
}
public void SetPos(Vector2 pos)
{
Rect.anchoredPosition = pos;
}
public void SetSize(Vector2 size)
{
Rect.sizeDelta = size;
}
public void SetSprite(Sprite sprite)
{
Image.sprite = sprite;
}
public void SetColor(Color color)
{
Image.color = color;
}
public void SetDir(Vector2 dir)
{
mDir = dir;
}
public void SetRadius(float radius)
{
mRadius = radius;
}
public void OnDrag(PointerEventData eventData)
{
Rect.anchoredPosition += DragDeltaOnDir(eventData, mDir);
//雷达图中handler取值不可以为负数
if (Vector2.Dot(Rect.anchoredPosition, mDir) / (Rect.anchoredPosition.magnitude * mDir.magnitude) < 0f)//若为负数,则夹角余弦=-1
{
Rect.anchoredPosition = Vector2.zero;
}
//雷达图中handler取值不可以超出上限
if (Rect.anchoredPosition.magnitude > mRadius)
{
Rect.anchoredPosition = mDir * mRadius;
}
}
private Vector2 DragDeltaOnDir(PointerEventData eventData, Vector2 dir)
{
float scale = Rect.lossyScale.x;//父物体的累计缩放倍数
//evenData中的delta需要除去这个累计缩放倍数
Vector2 delta = new Vector2(eventData.delta.x / scale, eventData.delta.y / scale);
float projectionLength = CalculateProjectionLength(delta, dir);
return new Vector2(dir.x * projectionLength, dir.y * projectionLength) / scale;
}
private float CalculateProjectionLength(Vector2 a, Vector2 b)
{
//求a在单位向量b上的投影
return a.x * b.x + a.y * b.y;
}
}
}
RadarGraphEditor
脚本:
using UnityEngine;
using UnityEditor;
using UnityEditor.UI;
namespace StudyUGUIExample
{
[CustomEditor(typeof(RadarGraph), true), CanEditMultipleObjects]
public class RadarGraphEditor : ImageEditor
{
private SerializedProperty mVertexCount;//雷达图的顶点数
private SerializedProperty mHandlerSprite;//内部可移动顶点的图片
private SerializedProperty mHandlerColor;//内部可移动顶点的颜色
private SerializedProperty mFilledColor;//填充的颜色
private SerializedProperty mHandlerSize;//内部可移动顶点的大小
private SerializedProperty mHandlers;//
private SerializedProperty mRatios;//各边上的占比
protected override void OnEnable()
{
base.OnEnable();
mVertexCount = serializedObject.FindProperty("mVertexCount");
mHandlerSprite = serializedObject.FindProperty("mHandlerSprite");
mHandlerColor = serializedObject.FindProperty("mHandlerColor");
mFilledColor = serializedObject.FindProperty("mFilledColor");
mHandlerSize = serializedObject.FindProperty("mHandlerSize");
mHandlers = serializedObject.FindProperty("mHandlers");
mRatios = serializedObject.FindProperty("mRatios");
}
public override void OnInspectorGUI()
{
base.OnInspectorGUI();
//更新序列化物体
serializedObject.Update();
EditorGUILayout.PropertyField(mVertexCount, new GUIContent("雷达图的顶点数"));
EditorGUILayout.PropertyField(mHandlerSprite, new GUIContent("内部可移动顶点的图片"));
EditorGUILayout.PropertyField(mHandlerColor, new GUIContent("内部可移动顶点的颜色"));
EditorGUILayout.PropertyField(mFilledColor, new GUIContent("填充的颜色"));
EditorGUILayout.PropertyField(mHandlerSize, new GUIContent("内部可移动顶点的大小"));
EditorGUILayout.PropertyField(mHandlers, true);
EditorGUILayout.PropertyField(mRatios, true);
RadarGraph radar = target as RadarGraph;
if (radar != null)
{
if (GUILayout.Button("初始化雷达图"))
{
radar.InitRadarGraph();
}
if (GUILayout.Button("生成内部可移动顶点"))
{
radar.InitHandlers();
}
}
//应用更改
serializedObject.ApplyModifiedProperties();
if (GUI.changed)
{
EditorUtility.SetDirty(target);
}
}
}
}