【Unity2019】UIElementsを触ってみた


本記事は、サムザップ Advent Calendar 2019 #2 の12/23の記事です。

Unity2019.1新機能「UIElements」とは

IMGUIからUIElementsに置き換わるであろう新しいGUIシステムです。

UIElementsはUXML/USS/UQueryで構成されます。

所感、
UXMLはUnityで扱えるXMLベースのマークアップ言語のイメージ
USSはCSSに近い書き方でデザインが出来そう
UQueryはC#で主にロジック部分を書くのかな…
って感じでした。

Unity Blog「Unity 2019.1 の UIElements の新機能」

テキスト読んだだけじゃ頭に入ってこないので、取り敢えず触ってみよう。。

「UIElements」を触ってみる

開発環境

今回使用する環境はUnity2019.3.0f1です。
(普段Unity2018のLTS版触っているから、Editorの見た目に違和感…w)

「UIElements」をHelloWorld

1.「UIElements」のEditorWindowを開く

右クリックでメニュー開く > [Create] > [UIElements] > [Editor Window]
もしくは
[Assets] > [Create] > [UIElements] > [Editor Window]

2.「UIElements」EditorWindowで作成

3.準備完了

作成されたデフォルトの中身

DefaultUIElementsEditorWindow.cs

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


public class DefaultUIElementsEditorWindow : EditorWindow
{
    [MenuItem("Window/UIElements/DefaultUIElementsEditorWindow")]
    public static void ShowExample()
    {
        DefaultUIElementsEditorWindow wnd = GetWindow<DefaultUIElementsEditorWindow>();
        wnd.titleContent = new GUIContent("DefaultUIElementsEditorWindow");
    }

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

        // VisualElements objects can contain other VisualElement following a tree hierarchy.
        VisualElement label = new Label("Hello World! From C#");
        root.Add(label);

        // Import UXML
        var visualTree = AssetDatabase.LoadAssetAtPath<VisualTreeAsset>("Assets/Editor/DefaultUIElementsEditorWindow.uxml");
        VisualElement labelFromUXML = visualTree.CloneTree();
        root.Add(labelFromUXML);

        // A stylesheet can be added to a VisualElement.
        // The style will be applied to the VisualElement and all of its children.
        var styleSheet = AssetDatabase.LoadAssetAtPath<StyleSheet>("Assets/Editor/DefaultUIElementsEditorWindow.uss");
        VisualElement labelWithStyle = new Label("Hello World! With Style");
        labelWithStyle.styleSheets.Add(styleSheet);
        root.Add(labelWithStyle);
    }
}

DefaultUIElementsEditorWindow.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"
>
    <engine:Label text="Hello World! From UXML" />
</engine:UXML>

DefaultUIElementsEditorWindow.uss

Label {
    font-size: 20px;
    -unity-font-style: bold;
    color: rgb(68, 138, 255);
}

つまづきポイント

「UIElements」のEditorWindowでエラー

下記のようなエラーが発生した(何起因で発生したかは分からない…)

対処法
Editorフォルダ配下で
右クリックでメニュー開く > [Update UIElements Schema]
もしくは
[Assets] > [Update UIElements Schema]

UXMLのタグ頭に「engine:」を付けないとエラー

Unity公式のリファレンスでは「engine:」がタグ頭に付いてる例と付いていない例がある。毎回「engine:」をタグ頭につけるような冗長な書き方は避けたいけど、「engine:」を失くすとエラーになる。。
対処法
デフォルトで生成されるUXMLは下記のようにnamespaceが指定されているので、毎回「engine:」をタグ頭につける必要があったっぽい
xmlns:engine="UnityEngine.UIElements"
下記のようにnamespaceを失くすことで、毎回「engine:」をタグ頭に付けなくても良くなる。
※必要な時にnamespaceで区切ろうかな…
xmlns="UnityEngine.UIElements"

軽く基盤を作ってみる

built-inのHelloWorldファイルは色々イケてない

  1. UXML内に既にnamespaceが指定されている
  2. USSがLabelにしか適用されない

ってことで、
とりあえず、複数のUXMLとUSSをデータとして受け取ったら、それを使ってEditorWindowを構築してくれるものが欲しい

試しに作ってみる

コレで、基盤のEditorWindowクラスが設定データを元に構築してくれる。

実際の基盤クラス

EditorWindowのデータクラス

using UnityEngine;
using UnityEngine.UIElements;

namespace System.Common.Editor.Window
{
    /// <summary>
    /// エディタウィンドウのデータの設定クラス
    /// </summary>
    [CreateAssetMenu(fileName = "EditorWindowSettingData", menuName = "Tools/Editor Window/Create Setting Data/EditorWindowData")]
    public class EditorWindowData : ScriptableObject
    {
        /// <summary>
        /// UXML一覧
        /// </summary>
        [SerializeField]
        private VisualTreeAsset[] uxmlArray;
        public VisualTreeAsset[] UxmlArray { get => uxmlArray; }

        /// <summary>
        /// USS一覧
        /// </summary>
        [SerializeField]
        private StyleSheet[] ussArray;
        public StyleSheet[] UssArray { get => ussArray; }
    }
}

EditorWindowのBaseクラス

using UnityEditor;
using UnityEngine;
using UnityEngine.UIElements;

namespace System.Common.Editor.Window
{
    /// <summary>
    /// エディタウィンドウのベースクラス
    /// </summary>
    public class EditorWindowBase<T> : EditorWindow where T : EditorWindow
    {
        /// <summary>
        /// 設定データ
        /// </summary>
        [SerializeField]
        private EditorWindowData editorSettingData;

        private static T _instance;
        public static T Instance { get => _instance; private set => _instance = value; }

        /// <summary>
        /// Window開く
        /// </summary>
        public static void Open()
        {
            Instance = GetWindow<T>();
            Instance.titleContent = new GUIContent(Instance.GetType().Name);
        }

        /// <summary>
        /// アクティブ処理
        /// </summary>
        protected virtual void OnEnable() { Init(); }

        /// <summary>
        /// 非アクティブ処理
        /// </summary>
        protected virtual void OnDisable() { Disable(); }

        /// <summary>
        /// 初期化
        /// </summary>
        protected virtual void Init()
        {
            if (editorSettingData == null)
                return;

            // Each editor window contains a root VisualElement object
            VisualElement root = rootVisualElement;
            if (root == null)
                return;

            // Import UXML
            foreach (var uxml in editorSettingData.UxmlArray)
            {
                if (uxml == null)
                    continue;

                root.Add(uxml.CloneTree());
            }

            // Import USS
            foreach (var uss in editorSettingData.UssArray)
            {
                if (uss == null)
                    continue;
                root.styleSheets.Add(uss);
            }
        }

        /// <summary>
        /// 停止処理
        /// </summary>
        protected virtual void Disable() { }
    }
}

テストデータ

EditorWindowの呼び出し口(TestEditorWindowを開くだけ)

using UnityEditor;
using Test.Editor.Window;

namespace Tools
{
    public class ToolMenu
    {
        [MenuItem("Tools/Editor Window/TestWindow")]
        public static void Open() { TestEditorWindow.Open(); }
    }
}

EditorWindow(中身なし)

using System.Common.Editor.Window;

namespace Test.Editor.Window
{
    public class TestEditorWindow : EditorWindowBase<TestEditorWindow>
    {

    }
}

UXML(中身なし)

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

参考にしたリファレンス

UIElements Developer Guide
https://docs.unity3d.com/Manual/UIElements.html
UXML関連
https://docs.unity3d.com/Manual/UIE-ElementRef.html

触ってみて…

IMGUIよりUIElementsの方が、Viewと処理を分けられるから読みやすそう。(小規模なツールはIMGUIの方が書きやすそうだけど…)

でも、毎回タグ頭に「engine:」をつけるのはダルいので、UXML内の対応で回避してみた。
UXMLのテンプレートを用意したり、SerializeField表示のためにBindして表示できるようにしたりと、基盤としてやらなきゃいけないことは結構あるけど、、、
って、今見たらVisualElementBindがなさそう…
visualElement.Bind(serializedData);的な感じで行けると思ったんだけど…
SerializeField表示方法をまた調べなきゃ…笑

Assetsと同階層のUIElementsSchemaディレクトリにUIElements.xsdとか色々定義されていたので、CustomしたXMLSchemaを作ってみるのをどっかで試してみたい。

明日は @m-nakayama さんの記事です。