Unity UI Toolkit カスタムコントロールサンプル
始めに
次のゲームでは UI Toolkit を使って UI を作る事にした。
円形の進捗表示はカスタムコントロールにする必要があるとの事なので
https://docs.unity3d.com/6000.0/Documentation/Manual/UIE-radial-progress-use-vector-api.html
を参考に作ったが、内容を整理したり機能を追加したので残しておく。
- Unity 6000.0.16f1
内容
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
- 同値なら更新が掛からないようにする。
- データバインドできるようにする。