UnityのUI Elements (UI Toolkit)を触ってみた

概要

UnityでEditor WindowをUI Elementsで試してみました。 従来のIMGUIだとUIの配置などもスクリプトでやっていると地味に時間が取られてしまうものでした。 EditorのUIはある程度楽に実装したいですよね。

EditorWindowのUI Elementsのセットアップ

  • EditorWindowの場合、一式をメニューから生成します
  • 名称を設定しConfirm
  • 生成されるファイルは以下の通り
  • 「Window」→「UI Toolkit」→「名称」でサンプルが表示されます。

    • 楽です。

UI Elements一連ファイルについて

スクリプト(cs)

uxmlのロードと、各VisualElement(要素)のイベント処理を実装します。
最低限以下を記述すれば画面が生成されます。

    public void CreateGUI()
    {
        // Each editor window contains a root VisualElement object
        VisualElement root = rootVisualElement;

        // Import UXML
        var visualTree = AssetDatabase.LoadAssetAtPath<VisualTreeAsset>("Assets/UIElements/Blog/Blog.uxml");
        root.Add(visualTree.Instantiate());
    }

対象の要素をQuery(Q)で名前を指定して取得。各プロパティ・イベントを設定する流れが基本となりそうです。

var textField = root.Q<TextField>("name"); // 要素取得
textField.value = "value"; // 値変更
textField.AddToClassList("class"); // スタイルのクラス追加
textField.RegisterValueChangedCallback(evt => {~~~}); // value change イベントアクション
textField.RegisterCallback<MouseOverEvent>(evt => {~~~}); // mouse over イベントアクション

UI Document(uxml)

xmlで要素を定義していきます。

<ui:UXML xmlns:ui="UnityEngine.UIElements" xmlns:uie="UnityEditor.UIElements" xsi=~~~ >
    <Style src="project://database/Assets/UIElements/Blog/Blog.uss?fileID=7433441132597879392&amp;guid=20c827d3be187c146b576d843f7d1a60&amp;type=3#Blog" />
    <ui:GroupBox name="Toolbar" ~~~>
        <uie:ToolbarMenu display-tooltip-when-elided="true" text="メニューアクション" name="ToolbarMenu" />
        <uie:ToolbarToggle focusable="false" label="トグル" name="ToolbarToggle" text="無効" />
    </ui:GroupBox></ui:UXML>

がGUIのデザインツールのUI Builderがあるのでこちらが楽なので基本となりそうです。

スタイルシート(uss)

webのcssとほぼほぼ同じように記述できます。
セレクタに対してプロパティを定義していきます。

Label.pagetitle {
   color: red;
}

セレクタや、プロパティについては公式参照です docs.unity3d.com
また、ussより直接設定したプロパティ値の方が優先されます。これもhtmlと同様ですね。
楽ですね。

各種Component

LabelやTextField等シンプルなものはスキップして個人的に使いそうなものをピックアップしました。

ToolbarMenu


こちらはEditor専用のコンポーネントでUI Builderで有効にすることで利用可能になります。
HierarchyのTopのinspecterから設定します。

スクリプトでアイテムを追加し、クリックイベントを実装します

        // Menu
        var menu = root.Q<ToolbarMenu>("ToolbarMenu");
        menu.menu.AppendAction("アクション", action =>
        {
            if (EditorUtility.DisplayDialog("メニューアクション", action.name, "OK"))
            {
                Debug.Log("Click");
            }
        });
        menu.menu.AppendSeparator("------------");
        menu.menu.AppendAction("アクション2", action => {});

ToolbarToggle


こちらもEditor専用でメニューのトグルになります。

        var toggle = root.Q<ToolbarToggle>("ToolbarToggle");
        toggle.RegisterValueChangedCallback(evt =>
        {
            toggle.text = (evt.newValue) ? "有効" : "無効";
        });

ScrollView


こちらはスクロール付きのリストビューとなります。
シーンのオブジェクトを取得しLabelとして追加、表示してみました。
また、列をマウスオーバー時にクラスを切り替えることで背景色を変化させたりします。

        var scene = EditorSceneManager.GetActiveScene();
        var objects = scene.GetRootGameObjects();
        var menu = root.Q<ScrollView>("SceneList");

        foreach (var obj in objects)
        {
            var label = new Label(obj.name);
            label.RegisterCallback<MouseOverEvent>(evt =>
            {
                label.AddToClassList("MouseOver");
            });
            label.RegisterCallback<MouseLeaveEvent>(evt =>
            {
                label.RemoveFromClassList("MouseOver");
            });
            menu.Add(label);
        }

css

.MouseOver {
    background-color: darkblue;
}

Foldout


TreeViewを実現したく、Foldoutというコンポーネントが近かったのでこれで再現してみました。
合わせてDragAndDropも試してみました。

        // left
        var leftTree = root.Q<Foldout>("LeftFoldout");
        leftTree.text = "root";

        var child = new Foldout();
        child.AddToClassList("TreeFirst");
        child.text = "child";
        leftTree.Add(child);

        var child2 = new Foldout();
        child2.AddToClassList("Tree");
        child2.text = "child2";
        child.Add(child2);

        var child3 = new Foldout();
        child3.AddToClassList("Tree");
        child3.text = "child3";
        child.Add(child3);

        var child3a = new Label();
        child3a.AddToClassList("Tree");
        child3a.text = "childA";
        child3.Add(child3a);

        child3.RegisterCallback<MouseDownEvent>(evt =>
        {
            DragAndDrop.PrepareStartDrag();
            DragAndDrop.StartDrag("Dragging");
            DragAndDrop.SetGenericData("treedata", child3);
        });

        // right
        var rightTree = root.Q<Foldout>("RightFoldout");
        rightTree.text = "root";

        var rightChild = new Foldout();
        rightChild.AddToClassList("TreeFirst");
        rightChild.text = "ここにドロップ";
        rightTree.Add(rightChild);
        rightChild.RegisterCallback<DragUpdatedEvent>(evt =>
        {
            object draggedLabel = DragAndDrop.GetGenericData("treedata");
            if (draggedLabel != null)
            {
                DragAndDrop.visualMode = DragAndDropVisualMode.Move;
            }

        });
        rightChild.RegisterCallback<DragExitedEvent>(evt =>
        {
            var draggedLabel = (Label) DragAndDrop.GetGenericData("treedata");
            if (draggedLabel == null)
            {
                return;
            }

            var newchild = new Label();
            newchild.AddToClassList("Tree");
            newchild.text = draggedLabel.text;
            rightChild.Add(newchild);
        });

css

.Tree {
    padding-left: 0px;
}
.TreeFirst {
    padding-left: 15px;
}

Foldout要素に追加していくことで階層構造が構築できます。
見栄え的には縦に並んでいくだけだったので、スタイルシートでpaddingでそれっぽく調整しています。

ドラッグアンドドロップですが、Editorの場合UnityEditor.DragAndDropが利用できるので簡単なものならこれで実現できそうです。
ただ凝ったものや、ランタイムの場合は別の方法で実現するようです。
docs.unity.cn

また、Foldout要素自体にMouseDownEventイベントを登録してもうまく動作しませんでした。ツリーごとドラッグしたい場合は、別の手段で実現する必要がありそうです。

ちなみに要素の「userData」プロパティに任意のobjectを入れておくことでDrop時のカスタマイズも色々行えそうです。
また、DragAndDrop.objectReferencesにオブジェクトをセットしてHierarchyにドラッグすると、オブジェクトを配置
DragAndDrop.pathsにパスを入れてProjectにドラッグするとpathsのアセットをコピーしたりできます。


こちらはシンプルにDropdownメニューです。
スクリプトからアイテムを追加して選択時のアクションを設定します。

        var dropdown = root.Q<DropdownField>("Dropdown");
        dropdown.choices.Clear();
        dropdown.choices.Add("アイテム1");
        dropdown.choices.Add("アイテム2");
        dropdown.choices.Add("アイテム3");
        dropdown.choices.Add("アイテム4");

        dropdown.index = 0;

        dropdown.RegisterValueChangedCallback(evt =>
        {
            if (EditorUtility.DisplayDialog("ドロップダウン", evt.newValue, "OK"))
            {
                Debug.Log("Click");
            }
        });

RadioButtonGroup


こちらもシンプルにRadioボタンです。
スクリプトから追加します。

        List<string> radioButtons = new List<string>();
        radioButtons.Add("radio1");
        radioButtons.Add("radio2");
        var radiogroup = root.Q<RadioButtonGroup>("RadioGroup");
        radiogroup.choices = radioButtons;
        radiogroup.RegisterValueChangedCallback(evt =>
        {
            Debug.Log(evt.newValue);
        });

なおアイテムはstringだけ指定可能なようなので、文字以外で表現する場合は通常のRadio Button等でカスタマイズが必要そうです。

Progress bar


プログレスバー(とついでにスライダ)です。
値を設定するだけです。シンプルです。
なお下限値・上限値はプロパティから設定することができます

        var progress = root.Q<ProgressBar>("Progress");
        var slider = root.Q<Slider>("ProgressSlider");

        progress.value = slider.value;
        progress.title = $"{progress.value}%";
        slider.RegisterValueChangedCallback(evt =>
        {
            progress.value = evt.newValue;
            progress.title = $"{progress.value}%";
        });

レイアウトについて少し

基本的にはwindowのサイズに応じてフレキシブルに配置したく試していたのですが、
特に横並びの均等配置がcssになれてないとわかりづらかったので残しておきます。

GroupBox


要素をグループ化します。VisualElement等でも良いのですが、これを例に横並び配置します。
Positionはデフォルトのまま相対配置の自動サイズ

FlexのDirectionを横並びを指定。

alignでクロス軸方向の揃え位置を指定します。(MDN クロス軸)


次に並べる子要素側のFlexのShink/Growに1を指定します。
これで要素の表示領域の割合を決めます。

flex shink/growについてはcssの説明がわかりやすいです。MDN flex-grow
また要素が増減しない単純な場合はwidthで「%」指定でも良いです。
こういう設定をスタイルシートにまとめておくと良さそうですね。

最後に

やはりViewの分離とスタイルシートでの制御は簡潔で楽になったと思います。
ツール作成する際はわかりやすさも大事かと思うので、積極的に利用していきたいです。

またScreenSpaceのみですが、ランタイムでも利用できるそうです。
ただレスポンシブやゲームUI等細かい表現を実現しようとすると標準機能だけだとまだまだ難しそうかな(?)という印象です。
今回はEditorWindowを試しましたがもちろんカスタムインスペクターもUI Elementで対応できます。
カスタムインスペクターの作成