【Unity】【UI Toolkit】バインド可能なカスタムコントロールを作成する

UnityのUI Toolkitでバインド可能なカスタムコントロールを作成する方法です。

Unity 2022.2.19

はじめに

以下の記事のようにVisualElementを継承したクラスを作成すると、独自のコントロールを作ることができます。

light11.hatenadiary.com

しかしこのままでは、以下の記事のようにパスを指定してSerializedPropertyとバインドするということができません。

light11.hatenadiary.com

本記事ではバインド可能なカスタムコントロールの作り方についてまとめます。

作るもの

今回は以下のように、上のTextFieldにアセットパスを入力すると、下のObjectFieldにそのパスが示すアセットが表示されるコントロールを作ります。

作るもの

以下のScriptableObjectのInspectorをこのコントロールを使って実装することを本記事のアウトプットとします。

using UnityEngine;

[CreateAssetMenu]
public sealed class Example : ScriptableObject
{
    public string assetPath;
}

コントロールを実装

それでは早速コントロールを実装します。

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

namespace Examplet.Editor
{
    public sealed class AssetPathField : BindableElement, INotifyValueChanged<string> // BindableElementとINotifyValueChangedを実装する
    {
        private readonly ObjectField _objectField;
        private readonly TextField _textField;
        private string _value;

        public AssetPathField()
        {
            _textField = new TextField();
            _textField.RegisterValueChangedCallback(OnTextFieldValueChanged);
            Add(_textField);

            _objectField = new ObjectField();
            _objectField.focusable = false;
            Add(_objectField);
        }

        // INotifyValueChanged<string>の実装
        // ChangeEventを発生させずに各フィールドの値を更新する
        public void SetValueWithoutNotify(string newValue)
        {
            _value = newValue;
            _textField.SetValueWithoutNotify(newValue);
            var asset = AssetDatabase.LoadAssetAtPath<Object>(newValue);
            _objectField.SetValueWithoutNotify(asset);
        }

        // INotifyValueChanged<string>の実装
        public string value
        {
            get => _value;
            // setterは以下の二つのケースで呼ばれる
            // 1. バインド先の値が変更された時
            // 2. TextFieldが編集された時
            set
            {
                if (value == _value)
                    return;

                // イベントを発生させる
                using (var evt = ChangeEvent<string>.GetPooled(_value, value))
                {
                    evt.target = this;
                    SetValueWithoutNotify(value);
                    SendEvent(evt);
                }
            }
        }

        private void OnTextFieldValueChanged(ChangeEvent<string> evt)
        {
            // TextFieldが編集されたときにはvalueを変更し、ChangeEventを発生させる
            value = evt.newValue;
        }

        public new class UxmlTraits : BindableElement.UxmlTraits // ここもBindableElementに
        {
        }

        public new class UxmlFactory : UxmlFactory<AssetPathField, UxmlTraits>
        {
        }
    }
}

説明はコメントに書いた通りですが、ポイントとしてはBindableElementを継承している点と、INotifyValueChangedを実装している点です。
この2点を行うことでバインド可能なコントロールを作成することができます。

Inspectorのレイアウトを作成

次にUI Builderを使ってInspectorのレイアウトを表すUXMLファイルを作ります。
先ほど作ったAssetPathFieldがUI Builderからも使えるので、これを配置してBinding PathにassetPathと入力します。

Binding Pathを入力
出力される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">
    <Examplet.Editor.AssetPathField binding-path="assetPath" />
</ui:UXML>

これをeditor_layout.uxmlという名前で保存しておきます。

CustomEditorを作成

最後にExampleのInspector表示用のスクリプトを以下のように作成します。

using UnityEditor;
using UnityEngine;
using UnityEngine.UIElements;

[CustomEditor(typeof(Example))]
public sealed class ExampleEditor : Editor
{
    [SerializeField] private VisualTreeAsset layout;

    public override VisualElement CreateInspectorGUI()
    {
        var container = layout.CloneTree();
        return container;
    }
}

動作確認

Projectウィンドウで先ほどスクリプトを選択し、Inspectorから以下のように設定します。

  • Layout: 上で作ったeditor_layout.uxmlをアサイ

アサイ

次にCreate > ExampleからScriptableObjectを作成します。
作成したらそれを選択してInspectorを表示すると、正常に動作していることを確認できます。

動作を確認

参考

docs.unity3d.com