UGUI扩展N多边形或圆形组件

类似的组件早已被写烂了,这里只是分享一下。

预览:

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

 

评论 3
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值