🍩 UI Toolkitで円形のProgressのUIを作る

2025-11-04 /Development #Unity

最近個人開発ではUI Toolkitをせっせと使っているのですが、ProgresBarを円形に見せたかったのでVisualElementを継承したクラスを作ってみました Claude Codeが結構手直ししてくれたやつですが、そのうちまた使う気がするので残しておこう…

DonutProgress

コピーしました
using UnityEngine;
using UnityEngine.UIElements;

[UxmlElement("DonutProgress")]
public partial class DonutProgress : VisualElement
{
    [UxmlAttribute("value")]
    public float Value
    {
        get => _value; set { _value = Mathf.Clamp01(value); MarkDirtyRepaint(); }
    }

    [UxmlAttribute("thickness")]
    public float Thickness
    {
        get => _thickness; set { _thickness = Mathf.Max(1f, value); MarkDirtyRepaint(); }
    }

    [UxmlAttribute("track-color")]
    public Color TrackColor
    {
        get => _track; set { _track = value; MarkDirtyRepaint(); }
    }

    [UxmlAttribute("fill-color")]
    public Color FillColor
    {
        get => _fill; set { _fill = value; MarkDirtyRepaint(); }
    }

    float _value = 1f; // デバッグしやすいよう既定=1
    float _thickness = 8f;
    Color _fill = Color.white;
    Color _track = new(1, 1, 1, 0.15f);

    public DonutProgress()
    {
        generateVisualContent += OnGenerate;

        style.flexShrink = 0;
        style.width = 50;
        style.height = 50;
    }

    void OnGenerate(MeshGenerationContext mgc)
    {
        var r = contentRect;
        float w = Mathf.Max(1f, r.width);
        float h = Mathf.Max(1f, r.height);

        // ローカル座標系の中心(r.x/y を足さない)
        var center = new Vector2(w * 0.5f, h * 0.5f);

        // 線幅ぶん内側に。下限を 1 にして“点化”防止
        float radius = Mathf.Max(1f, (Mathf.Min(w, h) - Thickness) * 0.5f);

        var p = mgc.painter2D;
        p.lineWidth = Thickness;
        p.lineJoin = LineJoin.Round;

        // 背景トラック:完全な円として描画(LineCap.Buttで重なり回避)
        p.lineCap = LineCap.Butt;
        p.strokeColor = TrackColor;
        p.BeginPath();
        p.Arc(center, radius, 0f, 360f);
        p.Stroke();

        float startDeg = -90f;
        float sweepDeg = Mathf.Clamp01(_value) * 360f;

        // フィル:値が完全(>= 0.999)な場合はButt、そうでない場合はRound
        bool isComplete = _value >= 0.999f;
        p.lineCap = isComplete ? LineCap.Butt : LineCap.Round;
        p.strokeColor = FillColor;
        p.BeginPath();

        if (isComplete)
        {
            // 完全な円の場合は、わずかなギャップを作って重なりを回避
            p.Arc(center, radius, startDeg, startDeg + 359.5f);
        }
        else if (sweepDeg > 0.1f) // 極小値の場合は描画しない
        {
            p.Arc(center, radius, startDeg, startDeg + sweepDeg);
        }

        p.Stroke();
    }
}

PieProgress

コピーしました
using UnityEngine;
using UnityEngine.UIElements;

[UxmlElement("PieProgress")]
public partial class PieProgress : VisualElement
{
    [UxmlAttribute("value")]
    public float Value
    {
        get => _value; set { _value = Mathf.Clamp01(value); MarkDirtyRepaint(); }
    }

    [UxmlAttribute("start-angle")]
    public float StartAngleDeg
    {
        get => _start; set { _start = value; MarkDirtyRepaint(); }
    }

    [UxmlAttribute("clockwise")]
    public bool Clockwise
    {
        get => _clockwise; set { _clockwise = value; MarkDirtyRepaint(); }
    }

    [UxmlAttribute("fill-color")]
    public Color FillColor
    {
        get => _fill; set { _fill = value; MarkDirtyRepaint(); }
    }

    [UxmlAttribute("track-color")]
    public Color TrackColor
    {
        get => _track; set { _track = value; MarkDirtyRepaint(); }
    }

    float _value = 0.6f;
    float _start = -90f; // 上から開始
    bool _clockwise = true;
    Color _fill = Color.white;
    Color _track = new(1, 1, 1, 0.15f);

    public PieProgress()
    {
        generateVisualContent += OnGenerate;

        style.flexShrink = 0;
        style.width = 50;
        style.height = 50;
    }

    void OnGenerate(MeshGenerationContext mgc)
    {
        var r = contentRect;
        float w = Mathf.Max(1f, r.width);
        float h = Mathf.Max(1f, r.height);
        var c = new Vector2(w * 0.5f, h * 0.5f);
        float R = Mathf.Min(w, h) * 0.5f; // 外半径

        var p = mgc.painter2D;
        p.fillColor = _track;
        p.BeginPath();

        // 外周 0..359.9°
        p.MoveTo(c + new Vector2(R, 0));
        p.Arc(c, R, 0f, 359.9f);

        // 中心へ閉じる
        p.LineTo(c);
        p.ClosePath();
        p.Fill();

        float sweep = Mathf.Clamp01(_value) * 359.9f; // 360は避ける
        if (sweep <= 0.0001f) return;

        float a0 = _start;
        float a1 = _clockwise ? (_start + sweep) : (_start - sweep);

        var pd = mgc.painter2D;
        pd.fillColor = _fill;
        pd.BeginPath();
        // ピザ:中心→外周弧→中心で閉じる
        pd.MoveTo(c);
        pd.Arc(c, R, a0, a1);
        pd.ClosePath();
        pd.Fill();
    }
}

所感

UI Toolkit、ところどころ痒いところに手が届かないんだけど、uGUIより簡単にレイアウトしたり調整したりできるので、とても便利ですね…

Profile

石原 悠 / Yu Ishihara

デザインとプログラミングと編み物とヨーグルトが好きです。