【Unity】【UIElements】独自のUXML要素を作る

UnityのUIElementsで独自のUXML要素を作る方法をまとめました。

Unity2019.3.0

はじめに

この記事ではUIElementsで独自のUXML要素を作る方法をまとめます。
UIElementsの基本的な使い方については以下の記事で説明しています。

light11.hatenadiary.com

本記事はUIElementsの基礎知識を前提としますので、必要に応じて上の記事を参照してください。

なお本記事の内容は執筆時点でちゃんとした公式ドキュメントがないのと、
UIElementsのランタイム対応時に一部変わりそうな予感がしているので、
誤りやうまく動かない点、記述が古い点などありましたらご指摘いただけますと幸いです。

やりたいこと

UIElementsにはデフォルトで様々なUXML要素が定義されています。

light11.hatenadiary.com

本記事ではこのUXML要素自体を自作する方法を紹介します。
実装例として以下のようにRGBA情報を表示するカラーピッカーを作成します。

f:id:halya_11:20200223171632g:plain

VisualElementを作る

独自のUXML要素を作るにはVisualElementを継承したクラスを定義します。

using UnityEngine;
using UnityEngine.UIElements;
using UnityEditor.UIElements;

class ColorAndText : VisualElement // VisualElementを継承
{
    public Color Color
    {
        get { return _colorField.value; }
        set {
            _colorField.value = value;
            _label.text = _colorField.value.ToString();
        }
    }

    private Label _label;
    private ColorField _colorField;

    public ColorAndText()
    {
        // レイアウトを調整(横並び&上下中央に)
        style.flexDirection = new StyleEnum<FlexDirection>(FlexDirection.Row);
        style.alignItems = new StyleEnum<Align>(Align.Center);
        
        // ColorFieldを子要素として追加
        _colorField = new ColorField();
        SetMargin(_colorField, 0); // 子要素のMarginはUSSで変更されないようにスクリプトで定義
        SetPadding(_colorField, 0); // 子要素のPaddingはUSSで変更されないようにスクリプトで定義
        _colorField.style.width = new StyleLength(new Length(50, LengthUnit.Percent));
        Add(_colorField);
        
        // Labelを子要素として追加
        _label = new Label();
        SetMargin(_label, 0);
        SetPadding(_label, 0);
        _label.style.width = new StyleLength(new Length(50, LengthUnit.Percent));
        _label.text = _colorField.value.ToString();
        Add(_label);

        _colorField.RegisterValueChangedCallback(x => _label.text = x.newValue.ToString());
    }
    
    private void SetMargin(VisualElement element, float px)
    {
        element.style.marginLeft = px;
        element.style.marginTop = px;
        element.style.marginRight = px;
        element.style.marginBottom = px;
    }

    private void SetPadding(VisualElement element, float px)
    {
        element.style.paddingLeft = px;
        element.style.paddingTop = px;
        element.style.paddingRight = px;
        element.style.paddingBottom = px;
    }
}

今回はColorFieldとLabelを持つUIを作りたいので、コンストラクタでこの二つを子要素として追加しています。

またこのUXML要素に対してUSSファイルが定義されたときにmarginやpaddingが変わってしまうと
見た目的におかしくなるためこれらはスクリプトで設定しています。
これは以下の記事で説明している通り、スクリプトから設定されたスタイルは最優先になる性質を利用しています。

light11.hatenadiary.com

ファクトリを定義する

次にこれをUXMLファイルで使えるようにするためにUxmlFactoryを継承したクラスを定義します。
これは先ほど定義したColorAndTextクラス内に定義する方法が推奨されています。

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

class ColorAndText : VisualElement
{
    // ファクトリとして使われるクラス
    public new class UxmlFactory : UxmlFactory<ColorAndText> {}
    ...(以下省略)

これでUXMLファイル内でこの要素を使い準備ができました。

定義した要素をUXMLで使う

さてそれでは前節までで作った要素をUXMLファイル内で使ってみます。

<?xml version="1.0" encoding="utf-8"?>
<engine:UXML
    xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
    xmlns:engine="UnityEngine.UIElements"
    xmlns:editor="UnityEditor.UIElements"
    xsi:noNamespaceSchemaLocation="../../UIElementsSchema/UIElements.xsd"
>

  <ColorAndText/> <!-- 定義した要素を使う -->

</engine:UXML>

これを読み込むと、正常に表示が行われていることが確認できます。

f:id:halya_11:20200223173619p:plain

独自のUXML属性を追加する

さて次にこのUXML要素に独自の属性を追加してみます。
属性を追加するにはUxmlFactoryの他にVisualElement.UxmlTraitsを継承したクラスも定義します。

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

class ColorAndText : VisualElement
{
    // ジェネリックの二つ目の型にUxmlTraitsを指定
    public new class UxmlFactory : UxmlFactory<ColorAndText, UxmlTraits> {}
    
    // ファクトリによるColorAndTextの初期化時に使うクラス
    public new class UxmlTraits : VisualElement.UxmlTraits
    {
        // UXMLの属性を定義
        UxmlColorAttributeDescription _initialColor = new UxmlColorAttributeDescription { name = "initial-color" };
        
        // 子を持たない場合はこのように書く
        public override IEnumerable<UxmlChildElementDescription> uxmlChildElementsDescription
        {
            get { yield break; }
        }
        
        // 初期化処理
        public override void Init(VisualElement ve, IUxmlAttributes bag, CreationContext cc)
        {
            base.Init(ve, bag, cc);
            var colorAndText = ve as ColorAndText;

            // UXMLの属性に入っている値を代入する
            colorAndText.Color = _initialColor.GetValueFromBag(bag, cc);
            colorAndText._label.text = colorAndText._colorField.value.ToString();
        }
    }
    ...(以下省略)

これはUXML要素で使える属性を定義するためのものです。
上記ではinitial-colorという色の初期値を入力するための属性を定義しています。

属性の定義自体はUxmlColorAttributeDescriptionクラスで行います。
このクラスを_initialColor.GetValueFromBag(bag, cc);のように使うことで値を取り出しています。

さて属性を定義できたので次にこれをUXML内で使ってみます。

<?xml version="1.0" encoding="utf-8"?>
<engine:UXML
    xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
    xmlns:engine="UnityEngine.UIElements"
    xmlns:editor="UnityEditor.UIElements"
    xsi:noNamespaceSchemaLocation="../../UIElementsSchema/UIElements.xsd"
>

  <ColorAndText initial-color="red"/> <!-- 定義した属性を指定 -->

</engine:UXML>

これで色の初期値が赤色になりました。

f:id:halya_11:20200223175720p:plain

なお今回は属性定義用のクラスにUxmlColorAttributeDescriptionを使いましたが、
全部で以下の種類が用意されています。

名前 説明
UxmlStringAttributeDescription string用
UxmlFloatAttributeDescription float用
UxmlDoubleAttributeDescription double用
UxmlIntAttributeDescription int用
UxmlLongAttributeDescription long用
UxmlBoolAttributeDescription bool用
UxmlColorAttributeDescription Color用
UxmlEnumAttributeDescription Enum

名前空間を使う

上記では新しく追加した要素に名前空間を定義していませんでした。
ここで、この要素をExampleNamespaceという名前空間に所属させることを考えます。

namespace ExampleNamespace
{
    class ColorAndText : VisualElement
    {
    }
}

このように名前空間を定義するとこのままではUXMLファイルから呼び出せなくなります。
UXMLファイルから呼び出すには、適当なcsファイルを作って以下のようなUxmlNamespacePrefixアトリビュートを定義します。

[assembly: UxmlNamespacePrefix("ExampleNamespace", "example")]

これは「ExampleNamespaceに所属するVisualElementをUXML内でexampleというprefixで使えるようにする」という意味です。

次にAssets > Update UIElements SchemaからUXMLスキーマを更新します。

f:id:halya_11:20200223184129p:plain

ここまで行うと、次に作成するUXMLファイルからは、UXML内で以下のように名前空間付きのVisualElementを使用できるようになります。

<example:ColorAndText initial-color="red"/>

関連

light11.hatenadiary.com

light11.hatenadiary.com

light11.hatenadiary.com

参考

docs.unity3d.com