类似的组件早已被写烂了,这里只是分享一下。
预览:
SlideNumbers:多边形边数量
FillAmount:填充量
UVOffsetAngle:UV旋转偏移
MeshOffsetAngle:网格旋转偏移
AloneCenterColor:是否单独设置中心颜色
FillClockwise:顺时针或逆时针
SetAdviseRotate(按钮):设置合适的旋转偏移(UV和Mesh)
SetNativeSize(按钮):设置合适的大小
精确点击:
代码:
using System;
using System.Collections.Generic;
using UnityEngine.Sprites;
using UnityEngine.U2D;
namespace UnityEngine.UI
{
[AddComponentMenu("UI/NImage")]
public class NImage : MaskableGraphic, ICanvasRaycastFilter
{
/// <summary>
/// 最小边数量
/// </summary>
public const int MinSLIDENUMBERS = 3;
/// <summary>
/// 最大边数量
/// </summary>
public const int MAXSLIDENUMBERS = 64;
#region Field
/// <summary>
/// 精灵
/// </summary>
[SerializeField]
private Sprite m_Sprite;
/// <summary>
/// 边数量
/// </summary>
[SerializeField]
private int m_SlideNumbers = 32;
/// <summary>
/// 边长
/// </summary>
private float m_SlideLength = 50.0f;
/// <summary>
/// 填充量
/// </summary>
[SerializeField]
private float m_FillAmount = 1.0f;
/// <summary>
/// UV偏移角度
/// </summary>
[SerializeField]
private float m_UVOffsetAngle = 0.0f;
/// <summary>
/// 网格偏移角度
/// </summary>
[SerializeField]
private float m_MeshOffsetAngle = 0.0f;
/// <summary>
/// 是否单独设置圆心颜色
/// </summary>
[SerializeField]
private bool m_AloneCenterColor = false;
/// <summary>
/// 圆心颜色
/// </summary>
[SerializeField]
private Color m_CenterColor = Color.white;
/// <summary>
/// 是否是顺时针
/// </summary>
[SerializeField]
private bool m_FillClockwise = true;
/// <summary>
/// 中心位置
/// </summary>
private Vector2 m_CenterPostion;
/// <summary>
/// 是否在追踪
/// </summary>
private bool m_Tracked = false;
/// <summary>
/// 追踪器
/// </summary>
//private DrivenRectTransformTracker m_DrivenRectTransformTracker;
#endregion
#region Property
/// <summary>
/// 获取或设置精灵
/// </summary>
public Sprite Sprite
{
get => this.m_Sprite;
set
{
this.m_Sprite = value;
this.OnSpriteChange(value);
}
}
/// <summary>
/// 获取或设置边数量
/// </summary>
public int SlideNumbers
{
get => this.m_SlideNumbers;
set => this.m_SlideNumbers = this.ClampSlideNumbers(value);
}
/// <summary>
/// 获取或设置边长
/// </summary>
public float SlideLength => this.m_SlideLength;
/// <summary>
/// 获取或设置填充量
/// </summary>
public float FillAmount
{
get => this.m_FillAmount;
set => this.m_FillAmount = this.ClampFillAmount(value);
}
/// <summary>
/// 获取或设置UV偏移角度
/// </summary>
public float OffsetAngle
{
get => this.m_UVOffsetAngle;
set => this.m_UVOffsetAngle = this.ClampOffsetAngle(this.m_UVOffsetAngle, value);
}
/// <summary>
/// 获取或设置网格偏移角度
/// </summary>
public float MeshOffsetAngle
{
get => this.m_MeshOffsetAngle;
set => this.m_MeshOffsetAngle = this.ClampOffsetAngle(this.m_MeshOffsetAngle, value);
}
/// <summary>
/// 是否顺时针
/// </summary>
public bool FillClockwise
{
get => this.m_FillClockwise;
set => this.m_FillClockwise = value;
}
/// <summary>
/// 主贴图
/// </summary>
public override Texture mainTexture
{
get
{
if (this.m_Sprite == null)
{
if (material != null && material.mainTexture != null)
return material.mainTexture;
return Graphic.s_WhiteTexture;
}
return this.m_Sprite.texture;
}
}
#endregion
protected override void OnEnable()
{
base.OnEnable();
this.TrackSprite();
//this.m_DrivenRectTransformTracker.Add(this, this.rectTransform, DrivenTransformProperties.SizeDelta);
}
protected override void OnDisable()
{
base.OnDisable();
if (this.m_Tracked)
NImage.UnTrackImage(this);
//this.m_DrivenRectTransformTracker.Clear();
}
/// <summary>
/// 更新材质球
/// </summary>
protected override void UpdateMaterial()
{
base.UpdateMaterial();
if (this.m_Sprite == null)
{
this.canvasRenderer.SetAlphaTexture(null);
return;
}
Texture2D alphaTex = this.m_Sprite.associatedAlphaSplitTexture;
this.canvasRenderer.SetAlphaTexture(alphaTex);
}
/// <summary>
/// 重建网格
/// </summary>
/// <param name="toFill">顶点帮助类</param>
protected override void OnPopulateMesh(VertexHelper toFill)
{
toFill.Clear();
//获取扇形单位角度
float temp_AngleUnit = 360.0f / this.m_SlideNumbers;
//小于最小角度,即单位角度
if (this.m_FillAmount < 1 / this.m_SlideNumbers)
return;
//获取UV
Vector4 uv = (m_Sprite != null) ? DataUtility.GetOuterUV(this.m_Sprite) : Vector4.zero;
//总角度
float totalAngle = 360.0f * this.m_FillAmount;
//完美矩形
Rect temp_Rect = this.GetPixelAdjustedRect();
//获取最短边长
this.m_SlideLength = Mathf.Min(temp_Rect.width, temp_Rect.height) * 0.5f;
//偏移
Vector2 offset = new Vector2((this.rectTransform.pivot.x - 0.5f) * temp_Rect.width, (this.rectTransform.pivot.y - 0.5f) * temp_Rect.height);
//圆心位置
Vector2 temp_CenterPostion = temp_Rect.center;
this.m_CenterPostion = temp_CenterPostion;
//圆心UV
Vector2 temp_UVCenter = new Vector2((uv.x + uv.z * 0.5f), (uv.y + uv.w * 0.5f));
//添加圆心顶点
toFill.AddVert(temp_CenterPostion, this.m_AloneCenterColor ? this.m_CenterColor : this.color, temp_UVCenter);
//当前顶点索引
int index = 0;
for (int i = 0; i < this.m_SlideNumbers + 1; i++)
{
//当前角度
float currentAngle = i * temp_AngleUnit;
float x = Mathf.Cos(Mathf.Deg2Rad * (currentAngle + this.m_MeshOffsetAngle));
float y = Mathf.Sin(Mathf.Deg2Rad * (currentAngle + this.m_MeshOffsetAngle));
float uv_X = Mathf.Cos(Mathf.Deg2Rad * (currentAngle + this.m_UVOffsetAngle));
float uv_Y = Mathf.Sin(Mathf.Deg2Rad * (currentAngle + this.m_UVOffsetAngle));
Vector2 vertexPostion = new Vector2(x, y) * this.m_SlideLength - offset;
Vector2 vertexUV = new Vector2(this.Remap(uv_X, uv.x, uv.x + uv.z), this.Remap(uv_Y, uv.y, uv.y + uv.w));
if (this.m_FillClockwise)
{
if (currentAngle < totalAngle)
{
toFill.AddVert(vertexPostion, this.color, vertexUV);
toFill.AddTriangle(0, index + 1, index);
index++;
}
}
else
{
if (currentAngle >= totalAngle)
{
toFill.AddVert(vertexPostion, this.color, vertexUV);
toFill.AddTriangle(0, index + 1, index);
index++;
}
}
}
//首尾相连
if (this.m_FillClockwise)
{
if (Mathf.Approximately(360.0f, totalAngle))
toFill.AddTriangle(0, 1, this.m_SlideNumbers);
}
else
{
if (Mathf.Approximately(0.0f, totalAngle))
toFill.AddTriangle(0, 1, this.m_SlideNumbers);
}
}
/// <summary>
/// 射线检测是否是有效的
/// </summary>
/// <param name="screenPoint">点击的屏幕位置</param>
/// <param name="eventCamera">发出射线的相机</param>
/// <returns>是否有效</returns>
public bool IsRaycastLocationValid(Vector2 screenPoint, Camera eventCamera)
{
RectTransformUtility.ScreenPointToLocalPointInRectangle(this.rectTransform, screenPoint, eventCamera, out Vector2 localPoint);
/*
* 检测思路:
* 由于NImage被设计为正N变形,所以检测思路简化为
* 判断点击的位置与中心的距离是否大于此方向上与边相交的距离,
* 小于在内部,大于在外部。
*
* 具体思路为通过正弦定理计算与边相交的距离,然后判断大小
*
* B _ _ _ _ _ _ _
* d / b | A、B、C为三角形的三角
* / \ | a、b、c为三角形的三边
* a/ \c | 其中A所在的点为中心
* / \ | d点为点击的位置(此处未画线,不好画)
* / _ _ _ _ _ _ _a_|
* C b A
*
* 实际检测代码如下
*/
Vector2 pointToCenter = localPoint - this.m_CenterPostion;
//与mesh偏移轴做夹角
float angle = this.VectorAngle(new Vector2(Mathf.Cos(Mathf.Deg2Rad * this.m_MeshOffsetAngle), Mathf.Sin(Mathf.Deg2Rad * this.m_MeshOffsetAngle)), pointToCenter.normalized);
//获取扇形单位角度
float temp_AngleUnit = 360.0f / this.m_SlideNumbers;
//小于angle
int lessAngleIndex = Mathf.FloorToInt(angle / temp_AngleUnit);
//大于angle
int greaterAngleIndex = Mathf.CeilToInt(angle / temp_AngleUnit);
float lessAngle = lessAngleIndex * temp_AngleUnit;
float greaterAngle = greaterAngleIndex * temp_AngleUnit;
//卡在顶点附近,认为与中心相交的距离为边长
if (Mathf.Approximately(angle, lessAngle) || Mathf.Approximately(angle, greaterAngle))
{
if (pointToCenter.sqrMagnitude > this.m_SlideLength * this.m_SlideLength)
return false;
else
return true;
}
else
{
//底角
float baseAngle = (180.0f - temp_AngleUnit) * 0.5f;
//顶角
float apexAngle = greaterAngle - angle;
//正弦定理
float length = Mathf.Sin(Mathf.Deg2Rad * baseAngle) * (this.m_SlideLength / Mathf.Sin(Mathf.Deg2Rad * (180.0f - baseAngle - apexAngle)));
if (pointToCenter.sqrMagnitude <= length * length)
return true;
else
return false;
}
}
#region 抄官方Image的(没真机测试过)
private static List<NImage> m_TrackedTexturelessImages = new List<NImage>();
private static bool s_Initialized;
/// <summary>
/// 追踪精灵
/// </summary>
private void TrackSprite()
{
if (this.m_Sprite != null && this.m_Sprite.texture == null)
{
NImage.TrackNImage(this);
this.m_Tracked = true;
}
}
/// <summary>
/// 重建Image
/// </summary>
/// <param name="spriteAtlas">SpriteAtlas</param>
static void RebuildNImage(SpriteAtlas spriteAtlas)
{
for (var i = m_TrackedTexturelessImages.Count - 1; i >= 0; i--)
{
NImage nImage = m_TrackedTexturelessImages[i];
if (spriteAtlas.CanBindTo(nImage.m_Sprite))
{
nImage.SetAllDirty();
NImage.m_TrackedTexturelessImages.RemoveAt(i);
}
}
}
/// <summary>
/// 追踪NImage
/// </summary>
/// <param name="nImage"></param>
private static void TrackNImage(NImage nImage)
{
if (!NImage.s_Initialized)
{
SpriteAtlasManager.atlasRegistered += NImage.RebuildNImage;
NImage.s_Initialized = true;
}
NImage.m_TrackedTexturelessImages.Add(nImage);
}
/// <summary>
/// 取消追踪NImage
/// </summary>
/// <param name="nImage"></param>
private static void UnTrackImage(NImage nImage)
{
NImage.m_TrackedTexturelessImages.Remove(nImage);
}
#endregion
#region Function
/// <summary>
/// 当精灵改变时
/// </summary>
/// <param name="value">待更改的精灵</param>
private void OnSpriteChange(Sprite value)
{
if (this.m_Sprite != null)
{
if (this.m_Sprite != value)
{
#if UNITY_2019
this.m_SkipLayoutUpdate = this.m_Sprite.rect.size.Equals(value ? value.rect.size : Vector2.zero);
this.m_SkipMaterialUpdate = this.m_Sprite.texture == (value ? value.texture : null);
#endif
this.m_Sprite = value;
this.SetAllDirty();
TrackSprite();
}
}
else if (value != null)
{
#if UNITY_2019
this.m_SkipLayoutUpdate = value.rect.size == Vector2.zero;
this.m_SkipMaterialUpdate = value.texture == null;
#endif
this.m_Sprite = value;
this.SetAllDirty();
TrackSprite();
}
}
/// <summary>
/// 限制边数量
/// </summary>
/// <param name="value">待显示的边数量</param>
/// <returns>合法的边数量</returns>
private int ClampSlideNumbers(int value)
{
if (this.m_SlideNumbers == value)
return value;
this.SetVerticesDirty();
return Mathf.Clamp(value, NImage.MinSLIDENUMBERS, NImage.MAXSLIDENUMBERS);
}
/// <summary>
/// 限制填充量
/// </summary>
/// <param name="value">待限制的填充量</param>
/// <returns>合法的填充量</returns>
private float ClampFillAmount(float value)
{
if (Mathf.Approximately(this.m_FillAmount, value))
return value;
this.SetVerticesDirty();
return Mathf.Clamp01(value);
}
/// <summary>
/// 限制偏移角度
/// </summary>
/// <param name="valuelhs">原始角度</param>
/// <param name="valuerhs">待限制的偏移角度</param>
/// <returns>合法偏移角度</returns>
private float ClampOffsetAngle(float valuelhs, float valuerhs)
{
if (Mathf.Approximately(valuelhs, valuerhs))
return valuerhs;
this.SetVerticesDirty();
float temp_OffsetAngle = valuerhs;
temp_OffsetAngle %= 360.0f;
if (temp_OffsetAngle < 0)
temp_OffsetAngle += 360.0f;
return temp_OffsetAngle;
}
/// <summary>
/// 重映射
/// </summary>
/// <param name="value">待映射的值</param>
/// <param name="sourceMin">原区间最小值</param>
/// <param name="sourceMax">原区间最大值</param>
/// <param name="newMin">映射区间的最小值</param>
/// <param name="newMax">映射区间的最大值</param>
/// <returns>重映射后的值</returns>
private float Remap(float value, float newMin, float newMax, float sourceMin = -1.0f, float sourceMax = 1.0f)
{
if (sourceMin > sourceMax)
{
throw new Exception("sourceMax can not less then sourceMin.");
}
if (newMin > newMax)
{
throw new Exception("newMax can not less then newMin.");
}
if (value < sourceMin || value > sourceMax)
return 0;
float temp_Ratio = (value - sourceMin) / (sourceMax - sourceMin);
return (newMax - newMin) * temp_Ratio + newMin;
}
/// <summary>
/// 计算两个向量之间的夹角(0-360)
/// </summary>
/// <param name="from">向量1</param>
/// <param name="to">向量2</param>
/// <returns>夹角</returns>
private float VectorAngle(Vector2 from, Vector2 to)
{
float angle;
Vector3 cross = Vector3.Cross(from, to);
angle = Vector2.Angle(from, to);
return cross.z > 0 ? (360.0f - angle) : angle;
}
#if UNITY_EDITOR
/// <summary>
/// 设置精灵的原始大小
/// 编辑器扩展用的函数
/// </summary>
public override void SetNativeSize()
{
if (this.m_Sprite != null)
{
this.rectTransform.anchorMax = rectTransform.anchorMin;
this.rectTransform.sizeDelta = new Vector2(this.m_Sprite.rect.width, this.m_Sprite.rect.height);
this.SetAllDirty();
}
}
/// <summary>
/// 设置推荐的旋转
/// 编辑器扩展用的函数
/// </summary>
public void SetAdviseRotate()
{
int remainder = this.m_SlideNumbers % 2;
//偶数
if (remainder == 0)
{
this.m_MeshOffsetAngle = 0.0f;
this.m_UVOffsetAngle = 0.0f;
}
//奇数
else if (remainder == 1)
{
float value = 90.0f / this.m_SlideNumbers;
this.m_MeshOffsetAngle = value;
this.m_UVOffsetAngle = value;
}
this.SetAllDirty();
}
#endif
#endregion
}
}
这里没继承Image继承的是MaskableGraphic,关于MaskableGraphic的相关问题,请自行搜索UGUI源码。OnPopulateMesh函数(网格重建),这里的思路和其他人的思路没什么两样;IsRaycastLocationValid(确定是否点击到)的思路和其他人的不太一样,这里用的是正弦定理去计算的,大致如下:
编辑器代码(重绘Inspector面板):
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEditor;
using UnityEditor.UI;
using UnityEngine.UI;
using System.Linq;
[CustomEditor(typeof(NImage), true)]
[CanEditMultipleObjects]
public class NImageEditor : GraphicEditor
{
#region Field
/// <summary>
/// 精灵
/// </summary>
private SerializedProperty m_Sprite;
/// <summary>
/// 边数量
/// </summary>
private SerializedProperty m_SlideNumbers;
/// <summary>
/// UV偏移角度
/// </summary>
private SerializedProperty m_UVOffsetAngle;
/// <summary>
/// Mesh偏移角度
/// </summary>
private SerializedProperty m_MeshOffsetAngle;
/// <summary>
/// 填充量
/// </summary>
private SerializedProperty m_FillAmount;
/// <summary>
/// 是否单独设置中心颜色
/// </summary>
private SerializedProperty m_AloneCenterColor;
/// <summary>
/// 中心颜色
/// </summary>
private SerializedProperty m_CenterColor;
/// <summary>
/// 是否是顺时针
/// </summary>
private SerializedProperty m_FillClockwise;
private GUIContent m_AdviseButtonContent;
#endregion
protected override void OnEnable()
{
base.OnEnable();
this.m_AdviseButtonContent = EditorGUIUtility.TrTextContent("Set Advise Rotate", "Sets the rotate to match the content.");
this.m_Sprite = this.serializedObject.FindProperty("m_Sprite");
this.m_SlideNumbers = this.serializedObject.FindProperty("m_SlideNumbers");
this.m_FillAmount = this.serializedObject.FindProperty("m_FillAmount");
this.m_UVOffsetAngle = this.serializedObject.FindProperty("m_UVOffsetAngle");
this.m_MeshOffsetAngle = this.serializedObject.FindProperty("m_MeshOffsetAngle");
this.m_AloneCenterColor = this.serializedObject.FindProperty("m_AloneCenterColor");
this.m_CenterColor = this.serializedObject.FindProperty("m_CenterColor");
this.m_FillClockwise = this.serializedObject.FindProperty("m_FillClockwise");
}
public override void OnInspectorGUI()
{
this.serializedObject.Update();
EditorGUILayout.ObjectField(this.m_Sprite,new GUIContent("Source Image"));
this.AppearanceControlsGUI();
this.RaycastControlsGUI();
#if UNITY_2019
this.MaskableControlsGUI();
#endif
this.m_SlideNumbers.intValue = EditorGUILayout.IntSlider("Slide Numbers",this.m_SlideNumbers.intValue, NImage.MinSLIDENUMBERS, NImage.MAXSLIDENUMBERS);
this.m_FillAmount.floatValue = EditorGUILayout.Slider("Fill Amount",this.m_FillAmount.floatValue, 0.0f, 1.0f);
this.m_UVOffsetAngle.floatValue = EditorGUILayout.Slider("UV Offset Angle",this.m_UVOffsetAngle.floatValue, 0.0f, 360.0f);
this.m_MeshOffsetAngle.floatValue = EditorGUILayout.Slider("Mesh Offset Angle", this.m_MeshOffsetAngle.floatValue, 0.0f, 360.0f);
this.m_AloneCenterColor.boolValue = EditorGUILayout.Toggle("Alone CenterColor", this.m_AloneCenterColor.boolValue);
if (this.m_AloneCenterColor.boolValue)
this.m_CenterColor.colorValue = EditorGUILayout.ColorField("Center Color", this.m_CenterColor.colorValue);
this.m_FillClockwise.boolValue = EditorGUILayout.Toggle("Fill Clockwise", this.m_FillClockwise.boolValue);
this.SetAdviseRotate();
this.SetShowNativeSize(false);
this.NativeSizeButtonGUI();
this.serializedObject.ApplyModifiedProperties();
}
/// <summary>
/// 设置推荐旋转
/// </summary>
private void SetAdviseRotate()
{
if (EditorGUILayout.BeginFadeGroup(m_ShowNativeSize.faded))
{
EditorGUILayout.BeginHorizontal();
{
GUILayout.Space(EditorGUIUtility.labelWidth);
if (GUILayout.Button(this.m_AdviseButtonContent, EditorStyles.miniButton))
{
foreach (Graphic graphic in targets.Select(obj => obj as Graphic))
{
Undo.RecordObject(graphic.rectTransform, "Set Advise Rotate");
(graphic as NImage).SetAdviseRotate();
EditorUtility.SetDirty(graphic);
}
}
}
EditorGUILayout.EndHorizontal();
}
EditorGUILayout.EndFadeGroup();
}
void SetShowNativeSize(bool instant)
{
bool showNativeSize = this.m_Sprite.objectReferenceValue != null;
base.SetShowNativeSize(showNativeSize, instant);
}
}