Unity UI Toolkit カスタムコントロールサンプル

始めに

次のゲームでは UI Toolkit を使って UI を作る事にした。

円形の進捗表示はカスタムコントロールにする必要があるとの事なので

https://docs.unity3d.com/6000.0/Documentation/Manual/UIE-radial-progress-use-vector-api.html

を参考に作ったが、内容を整理したり機能を追加したので残しておく。

内容

UIRadialProgress.cs

using Unity.Properties;
using UnityEngine;
using UnityEngine.UIElements;

namespace Assets.MyUI
{
    [UxmlElement]
    public partial class UIRadialProgress : VisualElement
    {
        public static readonly string ussClassName = "ui-radial-progress";
        public static readonly string ussLabelClassName = "ui-radial-progress__label";

        float m_DrawStartAngle = -90f;
        static CustomStyleProperty<float> s_DrawStartAngle = new("--draw-start-angle");

        float m_DrawAngle = 360f;
        static CustomStyleProperty<float> s_DrawAngle = new("--draw-angle");

        float m_LineWidth = 10f;
        static CustomStyleProperty<float> s_LineWidth = new("--line-width");

        float m_ProgressLineWidth = 10f;
        static CustomStyleProperty<float> s_ProgressLineWidth = new("--progress-line-width");

        bool m_RoundTerminate = false;
        static CustomStyleProperty<bool> s_RoundTerminate = new("--round-terminate");

        Color m_TrackColor = Color.black;
        static CustomStyleProperty<Color> s_TrackColor = new("--track-color");

        Color m_ProgressColor = Color.white;
        static CustomStyleProperty<Color> s_ProgressColor = new("--progress-color");

        float m_Value = 0f;

        [CreateProperty]
        [UxmlAttribute]
        public float Value
        {
            get => m_Value;
            set
            {
                if (Mathf.Approximately(m_Value, value)) return;

                m_Value = value;

                float v;
                if (Mathf.Approximately(m_MinValue, MaxValue))
                {
                    v = MaxValue < Value ? 100f : 0f;
                }
                else
                {
                    v = (value - m_MinValue) * 100f / (m_MaxValue - m_MinValue);
                }
                m_Label.text = $"{Mathf.Round(v)}%";

                MarkDirtyRepaint();
            }
        }

        float m_MinValue = 0f;

        [CreateProperty]
        [UxmlAttribute]
        public float MinValue
        {
            get => m_MinValue;
            set
            {
                if (Mathf.Approximately(m_MinValue, value)) return;

                m_MinValue = value;
                MarkDirtyRepaint();
            }
        }

        float m_MaxValue = 100f;

        [CreateProperty]
        [UxmlAttribute]
        public float MaxValue
        {
            get => m_MaxValue;
            set
            {
                if (Mathf.Approximately(m_MaxValue, value)) return;

                m_MaxValue = value;
                MarkDirtyRepaint();
            }
        }

        Label m_Label;

        public UIRadialProgress()
        {
            m_Label = new Label();
            m_Label.AddToClassList(ussLabelClassName);
            Add(m_Label);

            AddToClassList(ussClassName);

            RegisterCallback<CustomStyleResolvedEvent>(evt => CustomStylesResolved(evt));

            generateVisualContent += GenerateVisualContent;
        }

        static void CustomStylesResolved(CustomStyleResolvedEvent evt)
        {
            (evt.currentTarget as UIRadialProgress)?.UpdateCustomStyles();
        }

        void UpdateCustomStyles()
        {
            bool repaint = false;
            if (customStyle.TryGetValue(s_DrawStartAngle, out m_DrawStartAngle)) repaint = true;
            if (customStyle.TryGetValue(s_DrawAngle, out m_DrawAngle)) repaint = true;
            if (customStyle.TryGetValue(s_LineWidth, out m_LineWidth)) repaint = true;
            if (customStyle.TryGetValue(s_ProgressLineWidth, out m_ProgressLineWidth)) repaint = true;
            if (customStyle.TryGetValue(s_RoundTerminate, out m_RoundTerminate)) repaint = true;
            if (customStyle.TryGetValue(s_TrackColor, out m_TrackColor)) repaint = true;
            if (customStyle.TryGetValue(s_ProgressColor, out m_ProgressColor)) repaint = true;

            if (repaint) MarkDirtyRepaint();
        }

        void GenerateVisualContent(MeshGenerationContext context)
        {
            float centerX = contentRect.width * 0.5f;
            float centerY = contentRect.height * 0.5f;
            var center = new Vector2(centerX, centerY);

            float size = Mathf.Min(centerX, centerY);

            var painter = context.painter2D;
            painter.lineCap = m_RoundTerminate ? LineCap.Round : LineCap.Butt;

            void Draw(Color color, float lineWidth, float ratio)
            {
                painter.strokeColor = color;
                painter.lineWidth = lineWidth;
                painter.BeginPath();
                painter.Arc(
                    center,
                    size - lineWidth * 0.5f,
                    m_DrawStartAngle,
                    m_DrawStartAngle + m_DrawAngle * ratio);
                painter.Stroke();
            }

            Draw(m_TrackColor, m_LineWidth, 1f);

            if (!Mathf.Approximately(m_MinValue, MaxValue))
            {
                float ratio = (Value - m_MinValue) / (MaxValue - m_MinValue);
                Draw(m_ProgressColor, m_ProgressLineWidth, ratio);
            }
        }
    }
}

UIRadialProgress.uss

.ui-radial-progress {
    --draw-start-angle: -90;
    --draw-angle: 360;
    --line-width: 10;
    --progress-line-width: 10;
    --round-terminate: false;
    --track-color: rgb(0, 0, 0);
    --progress-color: rgb(255, 255, 255);
    --percentage-color: white;
    flex-direction: row;
    justify-content: center;
}

.ui-radial-progress__label {
    -unity-text-align: middle-left;
    color: var(--percentage-color);
}

UITime.cs

進捗表示の応用で時間表示も作った。

using Unity.Properties;
using UnityEngine;
using UnityEngine.UIElements;

namespace Assets.MyUI
{
    [UxmlElement]
    public partial class UITime : VisualElement
    {
        public static readonly string ussClassName = "ui-time";

        float m_DrawStartAngle = -90f;
        static CustomStyleProperty<float> s_DrawStartAngle = new("--draw-start-angle");

        float m_DrawAngle = 360f;
        static CustomStyleProperty<float> s_DrawAngle = new("--draw-angle");

        float m_LineWidth = 10f;
        static CustomStyleProperty<float> s_LineWidth = new("--line-width");

        float m_TimeLineWidth = 10f;
        static CustomStyleProperty<float> s_TimeLineWidth = new("--time-line-width");

        Color m_TrackColor = Color.white;
        static CustomStyleProperty<Color> s_TrackColor = new("--track-color");

        Color m_MorningColor = Color.black;
        static CustomStyleProperty<Color> s_MorningColor = new("--morning-color");

        Color m_DaytimeColor = Color.black;
        static CustomStyleProperty<Color> s_DaytimeColor = new("--daytime-color");

        Color m_EveningColor = Color.black;
        static CustomStyleProperty<Color> s_EveningColor = new("--evening-color");

        Color m_NightColor = Color.black;
        static CustomStyleProperty<Color> s_NightColor = new("--night-color");

        float m_CurrentTimeLineWidth = 10f;
        static CustomStyleProperty<float> s_CurrentTimeLineWidth = new("--current-time-line-width");

        float m_CurrentTimeThickness = 10f;
        static CustomStyleProperty<float> s_CurrentTimeThickness = new("--current-time-thickness");

        Color m_CurrentTimeColor = Color.white;
        static CustomStyleProperty<Color> s_CurrentTimeColor = new("--current-time-color");

        bool m_RoundTerminate = false;
        static CustomStyleProperty<bool> s_RoundTerminate = new("--round-terminate");

        float m_Time = 0f;

        [CreateProperty]
        [UxmlAttribute]
        public float Time
        {
            get => m_Time;
            set
            {
                if (Mathf.Approximately(m_Time, value)) return;

                m_Time = value;
                MarkDirtyRepaint();
            }
        }

        float m_Range = 24f;

        [CreateProperty]
        [UxmlAttribute]
        public float Range
        {
            get => m_Range;
            set
            {
                if (Mathf.Approximately(m_Range, value)) return;

                m_Range = value;
                MarkDirtyRepaint();
            }
        }

        float m_MorningStart = 6f;

        [CreateProperty]
        [UxmlAttribute]
        public float MorningStart
        {
            get => m_MorningStart;
            set
            {
                if (Mathf.Approximately(m_MorningStart, value)) return;

                m_MorningStart = value;
                MarkDirtyRepaint();
            }
        }

        float m_MorningEnd = 10f;

        [CreateProperty]
        [UxmlAttribute]
        public float MorningEnd
        {
            get => m_MorningEnd;
            set
            {
                if (Mathf.Approximately(m_MorningEnd, value)) return;

                m_MorningEnd = value;
                MarkDirtyRepaint();
            }
        }

        float m_DaytimeStart = 10f;

        [CreateProperty]
        [UxmlAttribute]
        public float DaytimeStart
        {
            get => m_DaytimeStart;
            set
            {
                if (Mathf.Approximately(m_DaytimeStart, value)) return;

                m_DaytimeStart = value;
                MarkDirtyRepaint();
            }
        }

        float m_DaytimeEnd = 17f;

        [CreateProperty]
        [UxmlAttribute]
        public float DaytimeEnd
        {
            get => m_DaytimeEnd;
            set
            {
                if (Mathf.Approximately(m_DaytimeEnd, value)) return;

                m_DaytimeEnd = value;
                MarkDirtyRepaint();
            }
        }

        float m_EveningStart = 17f;

        [CreateProperty]
        [UxmlAttribute]
        public float EveningStart
        {
            get => m_EveningStart;
            set
            {
                if (Mathf.Approximately(m_EveningStart, value)) return;

                m_EveningStart = value;
                MarkDirtyRepaint();
            }
        }

        float m_EveningEnd = 19f;

        [CreateProperty]
        [UxmlAttribute]
        public float EveningEnd
        {
            get => m_EveningEnd;
            set
            {
                if (Mathf.Approximately(m_EveningEnd, value)) return;

                m_EveningEnd = value;
                MarkDirtyRepaint();
            }
        }

        float m_NightStart = 19f;

        [CreateProperty]
        [UxmlAttribute]
        public float NightStart
        {
            get => m_NightStart;
            set
            {
                if (Mathf.Approximately(m_NightStart, value)) return;

                m_NightStart = value;
                MarkDirtyRepaint();
            }
        }

        float m_NightEnd = 6f;

        [CreateProperty]
        [UxmlAttribute]
        public float NightEnd
        {
            get => m_NightEnd;
            set
            {
                if (Mathf.Approximately(m_NightEnd, value)) return;

                m_NightEnd = value;
                MarkDirtyRepaint();
            }
        }

        public UITime()
        {
            AddToClassList(ussClassName);

            RegisterCallback<CustomStyleResolvedEvent>(evt => CustomStylesResolved(evt));

            generateVisualContent += GenerateVisualContent;
        }

        static void CustomStylesResolved(CustomStyleResolvedEvent evt)
        {
            (evt.currentTarget as UITime)?.UpdateCustomStyles();
        }

        void UpdateCustomStyles()
        {
            bool repaint = false;
            if (customStyle.TryGetValue(s_DrawStartAngle, out m_DrawStartAngle)) repaint = true;
            if (customStyle.TryGetValue(s_DrawAngle, out m_DrawAngle)) repaint = true;
            if (customStyle.TryGetValue(s_LineWidth, out m_LineWidth)) repaint = true;
            if (customStyle.TryGetValue(s_TimeLineWidth, out m_TimeLineWidth)) repaint = true;
            if (customStyle.TryGetValue(s_TrackColor, out m_TrackColor)) repaint = true;
            if (customStyle.TryGetValue(s_MorningColor, out m_MorningColor)) repaint = true;
            if (customStyle.TryGetValue(s_DaytimeColor, out m_DaytimeColor)) repaint = true;
            if (customStyle.TryGetValue(s_EveningColor, out m_EveningColor)) repaint = true;
            if (customStyle.TryGetValue(s_NightColor, out m_NightColor)) repaint = true;
            if (customStyle.TryGetValue(s_CurrentTimeLineWidth, out m_CurrentTimeLineWidth)) repaint = true;
            if (customStyle.TryGetValue(s_CurrentTimeThickness, out m_CurrentTimeThickness)) repaint = true;
            if (customStyle.TryGetValue(s_CurrentTimeColor, out m_CurrentTimeColor)) repaint = true;
            if (customStyle.TryGetValue(s_RoundTerminate, out m_RoundTerminate)) repaint = true;

            if (repaint) MarkDirtyRepaint();
        }

        void GenerateVisualContent(MeshGenerationContext context)
        {
            if (Mathf.Approximately(0f, Range)) return;
            if (Range < 0f) return;

            float centerX = contentRect.width * 0.5f;
            float centerY = contentRect.height * 0.5f;
            var center = new Vector2(centerX, centerY);

            float size = Mathf.Min(centerX, centerY);

            var painter = context.painter2D;
            painter.lineCap = m_RoundTerminate ? LineCap.Round : LineCap.Butt;

            void Draw(Color color, float lineWidth, float startAngle, float endAngle)
            {
                painter.lineWidth = lineWidth;
                painter.strokeColor = color;
                painter.BeginPath();
                painter.Arc(
                    center,
                    size - lineWidth * 0.5f,
                    m_DrawStartAngle + startAngle,
                    m_DrawStartAngle + endAngle);
                painter.Stroke();
            }

            Draw(m_TrackColor, m_LineWidth, 0f, m_DrawAngle);

            void DrawTime(Color color, float startTime, float endTime)
            {
                if (startTime < 0f || Range < startTime) return;
                if (endTime < 0f || Range < endTime) return;

                float startAngle = m_DrawAngle * startTime / Range;
                float endAngle = m_DrawAngle * endTime / Range;
                Draw(color, m_TimeLineWidth, startAngle, endAngle);
            }

            DrawTime(m_MorningColor, m_MorningStart, m_MorningEnd);
            DrawTime(m_DaytimeColor, m_DaytimeStart, m_DaytimeEnd);
            DrawTime(m_EveningColor, m_EveningStart, m_EveningEnd);
            DrawTime(m_NightColor, m_NightStart, m_NightEnd);

            if (0f < m_CurrentTimeThickness)
            {
                float angle = m_DrawAngle * Time / Range;
                float thickness = m_CurrentTimeThickness * 0.5f;
                Draw(m_CurrentTimeColor, m_CurrentTimeLineWidth, angle - thickness, angle + thickness);
            }
        }
    }
}

UITime.uss

.ui-time {
    --draw-start-angle: -90;
    --draw-angle: 360;
    --line-width: 10;
    --time-line-width: 10;
    --track-color: rgb(0, 0, 0);
    --morning-color: rgb(0, 255, 255);
    --daytime-color: rgb(255, 216, 0);
    --evening-color: rgb(255, 106, 0);
    --night-color: rgb(0, 148, 255);
    --current-time-line-width: 10;
    --current-time-thickness: 10;
    --current-time-color: rgb(255, 255, 255);
    --round-terminate: false;
}

更新

2024/08/31

top