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

始めに

UI Toolkit でのカスタムコントロールです。

グラフを作りました。

内容

MyGraph.cs

using System;
using System.Collections.Generic;
using System.Linq;
using Unity.Properties;
using UnityEngine;
using UnityEngine.UIElements;

namespace Assets.MyUI
{
    [UxmlElement]
    public partial class MyGraph : VisualElement
    {
        public static readonly string ussClassName = "my-graph";

        Color m_LineColor = Color.white;
        static readonly CustomStyleProperty<Color> s_LineColor = new("--line-color");

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

        Color m_BaseLineColor = transparentColor;
        static readonly CustomStyleProperty<Color> s_BaseLineColor = new("--base-line-color");

        float m_BaseLineWidth = 0f;
        static readonly CustomStyleProperty<float> s_BaseLineWidth = new("--base-line-width");

        Color m_OverBaseAreaColor = transparentColor;
        static readonly CustomStyleProperty<Color> s_OverBaseAreaColor = new("--over-base-area-color");

        Color m_UnderBaseAreaColor = transparentColor;
        static readonly CustomStyleProperty<Color> s_UnderBaseAreaColor = new("--under-base-area-color");


        static readonly Color transparentColor = new(0, 0, 0, 0);


        public MyGraph()
        {
            AddToClassList(ussClassName);

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

            generateVisualContent += OnGenerateVisualContent;
        }

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

        void UpdateCustomStyles()
        {
            bool repaint = false;

            if (customStyle.TryGetValue(s_LineColor, out m_LineColor)) repaint = true;
            if (customStyle.TryGetValue(s_LineWidth, out m_LineWidth)) repaint = true;
            if (customStyle.TryGetValue(s_BaseLineColor, out m_BaseLineColor)) repaint = true;
            if (customStyle.TryGetValue(s_BaseLineWidth, out m_BaseLineWidth)) repaint = true;
            if (customStyle.TryGetValue(s_OverBaseAreaColor, out m_OverBaseAreaColor)) repaint = true;
            if (customStyle.TryGetValue(s_UnderBaseAreaColor, out m_UnderBaseAreaColor)) repaint = true;

            if (repaint) MarkDirtyRepaint();
        }

        private void OnGenerateVisualContent(MeshGenerationContext context)
        {
            if (Mathf.Approximately(MinData.X, MaxData.X)) return;
            if (Mathf.Approximately(MinData.Y, MaxData.Y)) return;
            if (MaxData.X < MinData.X) return;
            if (MaxData.Y < MinData.Y) return;

            var painter = context.painter2D;

            if (0 < Datas.Count)
            {
                DrawOverBaseArea(painter);
                DrawUnderBaseArea(painter);
                DrawLine(painter);
            }

            DrawBaseValueLine(painter);
        }

        float GetLayoutX(float x) => layout.width * (x - MinData.X) / (MaxData.X - MinData.X);
        float GetLayoutY(float y) => layout.height - layout.height * (y - MinData.Y) / (MaxData.Y - MinData.Y);// 上がゼロ
        Vector2 GetLayoutPos(float x, float y) => new(GetLayoutX(x), GetLayoutY(y));
        Vector2 GetLayoutPos(MyGraphData data) => GetLayoutPos(data.X, data.Y);
        bool IsZero(float v) => Mathf.Approximately(v, 0f);
        bool IsTransparent(Color color) => IsZero(color.a);

        private void DrawOverBaseArea(Painter2D painter)
        {
            DrawLimitArea(painter, m_OverBaseAreaColor, y => Mathf.Max(y, BaseValue));
        }

        private void DrawUnderBaseArea(Painter2D painter)
        {
            DrawLimitArea(painter, m_UnderBaseAreaColor, y => Mathf.Min(y, BaseValue));
        }

        private void DrawLimitArea(Painter2D painter, Color fillColor, Func<float, float> func)
        {
            if (IsTransparent(fillColor)) return;

            painter.strokeColor = transparentColor;
            painter.lineWidth = 0f;
            painter.fillColor = fillColor;

            bool bFirst = true;
            MyGraphData prevData = new();

            var baseLine1 = new Vector2(MinData.X, BaseValue);
            var baseLine2 = new Vector2(MaxData.X, BaseValue);

            foreach (var data in Datas)
            {
                float y = func(data.Y);
                var pos = GetLayoutPos(data.X, y);

                if (bFirst)
                {
                    bFirst = false;
                    painter.BeginPath();
                    painter.MoveTo(pos);
                }
                else
                {
                    if (FindIntersection(baseLine1, baseLine2, prevData.ToVector2(), data.ToVector2(), out var result))
                    {
                        var pos2 = GetLayoutPos(new MyGraphData(result));
                        painter.LineTo(pos2);
                    }

                    painter.LineTo(pos);
                }

                prevData = data;
            }

            if (!bFirst)
            {
                var pos = GetLayoutPos(MaxData.X, BaseValue);
                painter.LineTo(pos);

                pos = GetLayoutPos(MinData.X, BaseValue);
                painter.LineTo(pos);

                painter.Fill();
            }
        }

        private void DrawLine(Painter2D painter)
        {
            if (IsTransparent(m_LineColor)) return;
            if (IsZero(m_LineWidth)) return;

            painter.strokeColor = m_LineColor;
            painter.lineWidth = m_LineWidth;

            bool bFirst = true;

            foreach (var data in Datas)
            {
                var pos = GetLayoutPos(data);

                if (bFirst)
                {
                    bFirst = false;
                    painter.BeginPath();
                    painter.MoveTo(pos);
                }
                else
                {
                    painter.LineTo(pos);
                }
            }

            if (!bFirst) painter.Stroke();
        }

        void DrawBaseValueLine(Painter2D painter)
        {
            if (IsTransparent(m_BaseLineColor)) return;
            if (IsZero(m_BaseLineWidth)) return;
            if (BaseValue < MinData.Y) return;
            if (MaxData.Y < BaseValue) return;

            painter.strokeColor = m_BaseLineColor;
            painter.lineWidth = m_BaseLineWidth;

            painter.BeginPath();
            painter.MoveTo(GetLayoutPos(MinData.X, 0));
            painter.LineTo(GetLayoutPos(MaxData.X, 0));
            painter.Stroke();
        }

        [CreateProperty]
        [UxmlAttribute]
        public Color LineColor
        {
            get => m_LineColor;
            set
            {
                if (m_LineColor == value) return;
                m_LineColor = value;
                MarkDirtyRepaint();
            }
        }

        [CreateProperty]
        [UxmlAttribute]
        public float LineWidth
        {
            get => m_LineWidth;
            set
            {
                if (Mathf.Approximately(m_LineWidth, value)) return;
                m_LineWidth = value;
                MarkDirtyRepaint();
            }
        }

        [CreateProperty]
        [UxmlAttribute]
        public Color BaseLineColor
        {
            get => m_BaseLineColor;
            set
            {
                if (m_BaseLineColor == value) return;
                m_BaseLineColor = value;
                MarkDirtyRepaint();
            }
        }

        [CreateProperty]
        [UxmlAttribute]
        public float BaseLineWidth
        {
            get => m_BaseLineWidth;
            set
            {
                if (Mathf.Approximately(m_BaseLineWidth, value)) return;
                m_BaseLineWidth = value;
                MarkDirtyRepaint();
            }
        }

        [CreateProperty]
        [UxmlAttribute]
        public Color OverBaseAreaColor
        {
            get => m_OverBaseAreaColor;
            set
            {
                if (m_OverBaseAreaColor == value) return;
                m_OverBaseAreaColor = value;
                MarkDirtyRepaint();
            }
        }

        [CreateProperty]
        [UxmlAttribute]
        public Color UnderBaseAreaColor
        {
            get => m_UnderBaseAreaColor;
            set
            {
                if (m_UnderBaseAreaColor == value) return;
                m_UnderBaseAreaColor = value;
                MarkDirtyRepaint();
            }
        }

        MyGraphData _minData = new();
        [CreateProperty]
        [UxmlObjectReference("min-data")]
        public MyGraphData MinData
        {
            get => _minData;
            set
            {
                if (_minData.Equals(value)) return;
                _minData = value ?? new();
                MarkDirtyRepaint();
            }
        }

        MyGraphData _maxData = new();
        [CreateProperty]
        [UxmlObjectReference("max-data")]
        public MyGraphData MaxData
        {
            get => _maxData;
            set
            {
                if (_maxData.Equals(value)) return;
                _maxData = value ?? new();
                MarkDirtyRepaint();
            }
        }

        List<MyGraphData> datas = new();
        [CreateProperty]
        [UxmlObjectReference("items-data")]
        public List<MyGraphData> Datas
        {
            get => datas;
            set
            {
                if (datas.SequenceEqual(value)) return;
                datas = value ?? new();
                MarkDirtyRepaint();
            }
        }

        float _baseValue = 0f;
        [CreateProperty]
        [UxmlAttribute]
        public float BaseValue
        {
            get => _baseValue;
            set
            {
                if (Mathf.Approximately(_baseValue, value)) return;
                _baseValue = value;
                MarkDirtyRepaint();
            }
        }

        static bool FindIntersection(Vector2 A1, Vector2 A2, Vector2 B1, Vector2 B2, out Vector2 intersectionPoint)
        {
            intersectionPoint = new Vector2(float.NaN, float.NaN);

            Vector2 directionA = A2 - A1;
            Vector2 directionB = B2 - B1;

            float crossProduct = (directionA.x * directionB.y) - (directionA.y * directionB.x);

            if (Mathf.Abs(crossProduct) < 0.0001f)
            {
                return false;
            }

            Vector2 toB1 = B1 - A1;
            float t = (toB1.x * directionB.y - toB1.y * directionB.x) / crossProduct;
            float u = (toB1.x * directionA.y - toB1.y * directionA.x) / crossProduct;

            if (t >= 0 && t <= 1 && u >= 0 && u <= 1)
            {
                intersectionPoint = A1 + directionA * t;
                return true;
            }

            return false;
        }
    }

    [UxmlObject]
    public partial class MyGraphData : IEquatable<MyGraphData>
    {
        public MyGraphData()
        {

        }

        public MyGraphData(float x, float y)
        {
            X = x;
            Y = y;
        }

        public MyGraphData(Vector2 vector2)
            : this(vector2.x, vector2.y)
        {
        }

        [UxmlAttribute]
        public float X { get; set; }

        [UxmlAttribute]
        public float Y { get; set; }

        public bool Equals(MyGraphData other)
        {
            return other is not null && Mathf.Approximately(X, other.X) && Mathf.Approximately(Y, other.Y);
        }

        public override bool Equals(object obj) => Equals(obj as MyGraphData);

        public override int GetHashCode() => (X, Y).GetHashCode();

        public Vector2 ToVector2() => new(X, Y);
    }
}

結果

結果

終わりに

前に作ったカスタムコントロールが 2024/08/24 で、一年以上前であることに驚愕

top

その他の投稿
20250913-01 Unity アプリで Android 16KB ページサイズ確認
20250912-01 Unity 6 で Editor がちらつく問題
20250906-01 Win10サポート終了 ChromeOS Flex を入れてみてつまづいた所
20250812-02 Unity UI Toolkit でのタップ操作検出
20250812-01 Unity UI Toolkit でマップ画面 with RenderTexture
20250811-01 Unity UI Toolkit での座標変換
20250721-01 Unityでカメラが平行投影の場合にScreenToWorldPointがズレる
20250712-01 Unity既存プロジェクトにURP追加
20250320-01 Unity のナビゲーションシステム 追記
20241013-01 Google Play で開発者情報が公開される件