0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

Unity6のUIToolKitでカスタムコントロールを作る

0
Posted at

(UIToolKitは)初投稿です

はじめに

最近SNSでデバッガーの開発にUIToolKitを使う記事を見かけて以来
Unity6でUIToolKitを利用してEditor拡張等々を開発しています。

今回は、
意外とUnity6になってからのカスタムコントロールの作り方の記事がなかったので
自身と同じようにUnity6で入門する人に向けて簡単に書き残しておこうと思います。

実装環境

Unity 6000.3.24f1

LTS内のバージョンアップでも結構自動生成周りのコードの出力結果が違います
お手元のUnityで実装し動作を確認しながら環境にあった実装へ変更ください

本編

共通

作成したいカスタムコントロールに対して、任意のVisualElementを継承させます。

最も基本的なのはVisualElement
UIToolKitのバインド機能を活用するコントロールを作りたければBindableElement
テキスト要素のコントロールを作りたければtextプロパティ等を持つTextElement
若干例外的ですが、即時描画を行うときに選ぶImmediateModeElement
…といった具合です。

最初の内は小難しいのでとりあえずVisualElementを継承させてしまえばよいと思います。
様々なコントロールを作るうちに理解が深まれば独自の基底コントロールを作ったり、
既存のコントロールを継承して実装を行うように進めていきましょう

自動生成

対象のVisualElementを直接・間接的に継承するクラスに対して
partialキーワードでの定義とUxmlElementAttributeを付与することで
UI Builderで要素として組み込み可能なカスタムコントロールにすることができます。

これはSourceGeneratorでの自動実装になるため、
UnityのDomainのロードが走った後にVisualStudo等から生成コードを確認できます。

Unity.UIToolKit.SourceGeneratorという名前で組み込まれています

text等の簡単な独自の値を持たせたい場合はUxmlAttributeを使う事で
自動生成コードに簡単に値を反映させてくれるコードを含めさせることができます。

手軽な反面、自動生成される以上の動作を実装することは直接的には難しいです。

定義コード

[UxmlElement]
public partial class HogeControl
    : VisualElement
{
    [UxmlAttribute] private int value;
}

自動生成コード

public partial class Test_Button
{
    [global::System.Runtime.CompilerServices.CompilerGenerated]
    [global::System.Serializable]
    public new class UxmlSerializedData : global::UnityEngine.UIElements.VisualElement.UxmlSerializedData
    {
        [global::UnityEngine.UIElements.RegisterUxmlCacheAttribute]
        [global::System.Diagnostics.Conditional("UNITY_EDITOR")]
        public new static void Register()
        {
            global::UnityEngine.UIElements.UxmlDescriptionCache.RegisterType(typeof(UxmlSerializedData), new global::UnityEngine.UIElements.UxmlAttributeNames[]
            {
                new (nameof(value), "value", null, global::System.Array.Empty<string>()),
            }
            #if UNITY_EDITOR_ONLY_COMPILATION
            , true);
            #else
            , false);
            #endif
        }
        
        #pragma warning disable 649
        [global::UnityEngine.SerializeField] int value;
        [global::UnityEngine.SerializeField, global::UnityEngine.UIElements.UxmlIgnore, global::UnityEngine.HideInInspector] global::UnityEngine.UIElements.UxmlSerializedData.UxmlAttributeFlags value_UxmlAttributeFlags;
        #pragma warning restore 649
        
        public override object CreateInstance() => new Test_Button();
        public override void Deserialize(object obj)
        {
            base.Deserialize(obj);
            var e = (Test_Button)obj;
            if (ShouldWriteAttributeValue(value_UxmlAttributeFlags))
            {
                e.value = this.value;
            }
        }
    }
}

UxmlAttributeについて

フィールド・プロパティに付与することができる属性で、
第一引数にUxml上で利用する名前を指定することができます。
これによりコーディングスタイルがUXMLの命名に合わせて縛られるのを避けられます。

また、第二引数にこれまで利用していた名前をparams形式で渡す事ができます。
あまり名前は変えるべきではありませんが、どうしても変更したい場合で
後方互換性を維持する必要のあるバージョンアップでは有効な機能です。

手書き

手書きでUxmlSerializedData型を定義する必要があります。
自動生成のコード等を参考にすれば基本的なことは分かると思いますが、
要点だけ簡単にコメントをつけておきます。

using System;
using UnityEnine;
using UnityEngine.UIElements;

public class HogeControl : VisualElement
{
    // NOTE: Serializableである必要がある
    [Serializable]
    // NOTE: VisualElement型にUxmlSerializedDataが定義されているので new で定義しつつ継承する
    public new class UxmlSerializedData : VisualElement.UxmlSerializedData
    {
        // NOTE: 同じく new 定義し、Editorでのみ動作させるためにConditionalする
        [System.Diagnostics.Conditional("UNITY_EDITOR")]
        public new static void Register()
        {
            // NOTE: 独自に定義したい値等を実際に登録する
            UxmlDescriptionCache.RegisterType(typeof(UxmlSerializedData), Array.Empty<UxmlAttributeNames>(), true);
        }

        // NOTE: D&DやUXMLから生成される時に実行されるインスタンスメソッド
        public override object CreateInstance() => new HogeControl();
    }
}

独自の値を定義する

命名

基本的にはprefix・suffixのないような命名であるほうが良いと思います。
これはなぜかというと、_を使った部分はUXML上で自動的に-に置き換わる為です。
m_expandはm-expandを使った名前になります。

決まり事

定義する値にはUxmlAttributeFlagsというenumの値をセットで定義する必要があります。
命名は必ず???_UxmlAttributeFlagsである必要があり、
これはUXMLの実装に依存している為基本的にそのルールから逃れるのは難しそうです

using System;
using UnityEngine.UIElements;

public class HogeControl : VisualElement
{
    [Serializable]
    public new class UxmlSerializedData : VisualElement.UxmlSerializedData
    {
        // NOTE: コントロールがUXMLとして表現されるときに持つデータを定義する
        [SerializeField] private bool expand;
        // NOTE: 値を変更しているか等のexpandの属性データを表現するデータ
        [SerializeField, UxmlIgnore, HideInInspector] private UxmlAttributeFlags expand_UxmlAttributeFlags;

        [System.Diagnostics.Conditional("UNITY_EDITOR")]
        public new static void Register()
        {
            UxmlDescriptionCache.RegisterType(typeof(UxmlSerializedData), new UxmlAttributeNames[]
            {
                // NOTE: 名前指定でexpandをUXMLの要素の値として定義する
                new("expand", "expand", null, null),
            }, true);
        }

        public override object CreateInstance() => new HogeControl();

        public override void Deserialize(object obj)
        {
            base.Deserialize(obj);
            var e = (HogeControl)obj;
            // NOTE: UXMLから実際のUIに変換されるときに属性を見て必要なら値を渡す
            if (ShouldWriteAttributeValue(expand_UxmlAttributeFlags))
            {
                e.expand = this.expand;
            }
        }
    }

    // NOTE: コントロールが実際に持つデータを定義する
    [SerializeField] private bool expand;
}

終わりに

CreatePropertyやINotifyValueChangedについても含めてもっと書こうと思ったのですが、
書きながら細かい仕様を調べながら実装していると全然知らないものばかりだったので
頑張って追記するなり新しい記事で紹介していこうと思います…

先は長いぞ~

0
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?