こんにちは、カシカでエンジニアをしております、村本と申します。

Unityを使った開発の中でUIToolkitでプログレスバーの自作コンポーネントを作った話をします。

作成中のアプリケーションでデータ処理状況を表示するプログレスバーを追加することになり、デザインが上がってきました。

デザイン

左右が丸くなっているデザインでした。

UIToolkit標準のプログレスバーのデザインを変更するには限界があるため、自作することにしました。

仕様

0~100%で数値を指定します。数値は半円を含まない位置を基準とします。

仕様1

0%の時は背景色のみ 100%の時は前景色のみとします。 仕様2

開発

作成中のアプリケーションには既に似たデザインのスライダーがありました。背景部分で数値の範囲による状態の違いを表現する機能を持っています。

スライダー

数値の部分が半円かどうかの違いはありますが、スライダーのつまみ以外は同じような構成にできるだろうと思い、これを参考にプログレスバーを実装することにしました。

この時点ではプログレスバーの幅が複数種類必要になる可能性があったため、進捗部分の幅は%で指定したいと思いました。 そこで下記画像に示すような構造にしようと実装を始めました。

バー構成

ところが実装中に問題が発生しました。端の半円をborder-radiusの指定で作ろうと思ったのですが、UIToolkitでは要素の幅がborder-radiusの指定値の倍未満だと半円部分の比率が変わってしまうようです。(Unity2022.3.16f1)

半円のVisualElementが作れない

そこで回避するために、こういう構造にしました。

バー構成

ところが、今度は別の問題が発生しました。このアプリケーションではフルHDを基本にウィンドウサイズに合わせて画面を拡大縮小する処理が入っていて、ウィンドウサイズによって要素と要素の隙間が開いて見える現象が発生してしまいました。

バー不具合

そこで、上記の問題を解決するために、半円部分と塗り部分をひとつのVisualElementにまとめる方法を思い付きました。paddingで左右に半円分の幅を確保するという方法です。下記画像のような構造になりました。 バー構成

この方法だと重ねるレイヤー数も減り、隙間ができる問題も解消します。参考にした元のスライダーは塗り部分が真っ直ぐのデザインのためこの構造にはできなかったのですが、このプログレスバーは塗り部分の右側が半円になっているデザインなので実現できました。

実装

最終的にこのような実装になりました。

ProcessProgressBarComponent.uxml

<ui:UXML xmlns:ui="UnityEngine.UIElements" xmlns:uie="UnityEditor.UIElements" xsi="http://www.w3.org/2001/XMLSchema-instance" engine="UnityEngine.UIElements" editor="UnityEditor.UIElements" noNamespaceSchemaLocation="../../../../../UIElementsSchema/UIElements.xsd" editor-extension-mode="False">
    <Style src="ProcessProgressBarComponent.uss" />
    <ui:VisualElement class="process-progress-component" style="flex-grow: 1; flex-direction: row;">
        <ui:VisualElement name="process_progress_bar">
             <ui:VisualElement class="process-progress-bg" />
             <ui:VisualElement name="process_progress_fill" class="process-progress-fill" />
        </ui:VisualElement>
    </ui:VisualElement>
</ui:UXML>

ProcessProgressBarComponent.uss

#process_progress_bar {
    margin-left: 4px;
    margin-top: 4px;
    width: 100px;
    height: 8px;
    border-radius: 4px;
    background-color: #707070;
    opacity: 1;
}
.process-progress-bg {
    position:absolute;
    width:100%;
    height:100%;
    border-radius: 4px;
    background-color: black;
    opacity: 0.5;
}
.process-progress-fill {
    padding: 0 4px; /* 角丸分のスペースを確保します */
    position:relative;
    width:0; /* プログラムから変更します */
    height:100%;
    background-color: yellow;
    border-radius: 4px;
}
.process-progress-min .process-progress-fill { /* 0%の時は前景を非表示にします */
    visibility: hidden;
}

ProcessProgressBarComponent.cs

using System.Collections.Generic;
using UnityEngine;
using UnityEngine.UIElements;

public class ProcessProgressBarComponent : VisualElement
{
    public new class UxmlFactory : UxmlFactory<ProcessProgressBarComponent, UxmlTraits> { }

    private VisualElement _process_progress_bar;
    private VisualElement _process_progress_fill;

    private float _progress = 0f; // 0~100
    public float progress { get { return _progress; } set { _progress = value; SetProgress(_progress); } }

    public new class UxmlTraits : VisualElement.UxmlTraits
    {
        ///
        /// 省略
        ///
    }

    public ProcessProgressBarComponent()
    {
        var treeAsset = Resources.Load<VisualTreeAsset>("UI/Component/ProcessProgressBarComponent");
        var ve = treeAsset.Instantiate();
        this.Add(ve);

        _process_progress_bar = ve.Q<VisualElement>("process_progress_bar");
        _process_progress_fill = ve.Q<VisualElement>("process_progress_fill");
    }

    private void SetProgress(float p)
    {
        _process_progress_bar.ClearClassList();
        if (Mathf.Approximately(p, 100.0f))
        {
            _process_progress_fill.style.width = Length.Percent(100);
        }
        else if (Mathf.Approximately(p, 0.0f))
        {
            // 0%の時はクラスリストに"process-progress-min"を追加します
            _process_progress_bar.AddToClassList("process-progress-min");
            _process_progress_fill.style.width = Length.Percent(0);
        }
        else
        {
            _process_progress_fill.style.width = Length.Percent(p);
        }
    }
}

ProcessProgressBarComponentのインスタンス.progressに0~100の数値を指定すれば見た目に反映されます。

まとめ

UnityのUIToolkitで自作のプログレスバーを作成した際に躓いた点について紹介しました。 UXML+USSはHTML+CSSと比べて使えるプロパティが少なかったり仕様が異なる部分もあったりするので、要求された仕様を満たす方法を考えるために試行錯誤が必要になることもあります。

株式会社カシカでは社員募集しています! リクルートページ

Unityを使った開発、UIToolkitを用いた独自コンポーネントの開発に興味がある方はぜひお問い合わせください。 お問い合わせページ

参考文献

Unity スタイルシート (USS)