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

最近個人開発では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より簡単にレイアウトしたり調整したりできるので、とても便利ですね…
よく見られている記事
