【Unity2019】UIElementsを触ってみた

本記事は、[サムザップ Advent Calendar 2019 #2](https://qiita.com/advent-calendar/2019/sumzap2) の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 の新機能」](https://blogs.unity3d.com/jp/2019/04/23/whats-new-with-uielements-in-2019-1/)

テキスト読んだだけじゃ頭に入ってこないので、取り敢えず触ってみよう。。
# 「UIElements」を触ってみる
## 開発環境
今回使用する環境は`Unity2019.3.0f1`です。
（普段Unity2018のLTS版触っているから、Editorの見た目に違和感…w）
## 「UIElements」をHelloWorld
### 1.「UIElements」のEditorWindowを開く
右クリックでメニュー開く > [Create] > [UIElements] > [Editor Window]
もしくは
[Assets] > [Create] > [UIElements] > [Editor Window]
<img width="300" alt="CreateUIElements.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/209889/b13e160e-ca2f-a354-4197-5db681650af7.png">
### 2.「UIElements」EditorWindowで作成
<img width="250" alt="CreateUIElementsWindow.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/209889/ec12dfda-d2f2-d70f-c2b0-e6a90926f005.png">
### 3.準備完了
<img width="280" alt="HelloWorldWindow.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/209889/e45e7d37-e403-f048-6629-446a4b4af29b.png">
## 作成されたデフォルトの中身
### 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);
}
```

# :warning: **つまづきポイント** :warning:
## 「UIElements」のEditorWindowでエラー
下記のようなエラーが発生した（何起因で発生したかは分からない…） 
<img width="250" alt="CreateUIElementsWindowError.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/209889/651b95c1-dea2-220c-3a63-c554e8be6550.png">
:hospital: **対処法** :hospital:
Editorフォルダ配下で
右クリックでメニュー開く > [Update UIElements Schema]
もしくは
[Assets] > [Update UIElements Schema]
<img width="240" alt="Error.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/209889/1bef12fa-bbfe-5e08-52ae-7ef25078277c.png">
## UXMLのタグ頭に「engine:」を付けないとエラー
Unity公式のリファレンスでは「engine:」がタグ頭に付いてる例と付いていない例がある。毎回「engine:」をタグ頭につけるような冗長な書き方は避けたいけど、「engine:」を失くすとエラーになる。。
:hospital: **対処法** :hospital:
デフォルトで生成される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して表示できるようにしたりと、基盤としてやらなきゃいけないことは結構あるけど、、、
って、今見たら`VisualElement`に`Bind`がなさそう…
`visualElement.Bind(serializedData);`的な感じで行けると思ったんだけど…
SerializeField表示方法をまた調べなきゃ…笑

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

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