UIElementsで開発するときの問題と解決

[PONOS Advent Calendar 2019](https://qiita.com/advent-calendar/2019/ponos)の13日目の記事です。

昨日は@nimitsuさんの[GameLift RealTimeServerで遊んでみよう for Unity（Unity編）](https://qiita.com/nimitsu/items/4b3858a1d06cbdc3ea01)でした。

# はじめに

Unity 2019.1でついにUIElementsが正式リリースされました。
今後ランタイムUIへの採用も決定されており、Unity全体のUIフレームワークとして利用されていくことになると思うのですが、残念ながら現時点ではまだ使いづらさを感じることがあります。

今回は私がUIElementsに触れてみて不便だと感じた問題点を共有するとともに、皆様のUIElements開発が少しでも改善されるようにそれらの問題点の解消方法を紹介したいと思います。

**※もしこれから紹介する問題点について、他の解消方法をご存じの方がいらっしゃいましたらコメントください！**
**※紹介している以外に困っている点がありましたらぜひコメントください。一緒に解決しましょう！**

なお、

* Unity 2019.2.12f1
* MacOS 10.14.6

の環境で動作確認しています。

# UXMLをリロードできるようにする

## 問題

公式のコードや`Assets/Create/UIElements Editor Window`メニューで作られるコード上では、EditorWindowのOnEnable()内でUXMLやUSSを読み込み、反映するように記述されています。
EditorWindowがフォーカスされるたびにレイアウトを読み込み直す必要はないですし、EditorWindowが開いたタイミングかコンパイルが走ったタイミングでこれらの処理が実行されるのは無駄がなく効率的だと思います。

しかし、開発中などはUXMLの編集を頻繁に行いますし、コンパイルが走るタイミングとは違うタイミングでUXMLを読み込み直したいことも多いと思います。
もちろん、そのたびにEditorWindowを開き直せば再度UXML の読み込むことができますが、フローティングなウインドウならともかく、他のウインドウとドッキングして使用することを想定したウインドウの場合にはEditorWindowの開き直しは結構な手間です。

（なお、USSはリアルタイムで変更が反映されます）

## 解決

EditorWindowのメニューからUXMLのリロードを行えるようにしました。
EditorWindowにIHasCustomMenuを実装し、リロードするためのメニューアイテムを登録するようにしています。

```cs
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEditor;
using UnityEngine.UIElements;

public class ReloadableEditorWindow : EditorWindow, IHasCustomMenu
{
    public void AddItemsToMenu(GenericMenu menu)
    {
        // メニューアイテムを登録。
        menu.AddItem(new GUIContent("Reload"), false, () =>
        {
            ReloadUxml();
        });
    }

    [MenuItem("Window/UIElements/ReloadableEditorWindow")]
    static void Open()
    {
        ReloadableEditorWindow wnd = GetWindow<ReloadableEditorWindow>();
        wnd.titleContent = new GUIContent("ReloadableEditorWindow");
    }

    public void OnEnable()
    {
        ReloadUxml();
    }

    void ReloadUxml()
    {
        // 一度ルートに紐付けられた要素を全て削除する。
        rootVisualElement.Clear();

        // UXMLの読み込み。
        var visualTree = AssetDatabase.LoadAssetAtPath<VisualTreeAsset>("...");
        VisualElement labelFromUXML = visualTree.CloneTree();
        rootVisualElement.Add(labelFromUXML);

        // その他、レイアウトに関する各種処理。
    }
}

```
こんな感じで選択・実行ができます。
![ReloadableEditorWindow.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/7345/f96bfdf8-2e85-9238-f26f-ca273b702237.png)

UXMLの編集を行った後にこのメニュー項目を実行すれば、現在開いているEditorWindow上でUXMLを再読み込みさせることができます。

# UXML / USSのパスを取得しやすくする

## 問題

現在UXMLとUSSは、`Assets/`から始まるアセットパスを`AssetDatabase.LoadAssetAtPath<T>()`に渡して、それぞれVisualTreeAssetオブジェクトとStyleSheetオブジェクトとしてロードします。
現状で読み込みの機構が整備されていないためかもしれませんが、ソースファイルのパスをハードコーディングしているので、ファイルの場所が変更されたときなどにサポートしづらく、作成したUIElementsをモジュール化して他者へ配布するときにも不便です。

```cs
var visualTree = AssetDatabase.LoadAssetAtPath<VisualTreeAsset>("Assets/Editor/Sample/SampleWindow.uxml");
var styleSheet = AssetDatabase.LoadAssetAtPath<StyleSheet>("Assets/Editor/Sample/SampleWindow.uss");
```

## 解決

動的にUXMLとUSSのパスを取得します。
指定したファイル名のファイルをAssetDatabaseから探し、アセットパスを返すメソッドを作成します。
ファイル名から検索するので、後ほどプロジェクトの階層が変わってもファイル名やクラス名が変更されていなければ引き続き参照ができます。

```cs
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEditor;
using System.Linq;
using System.IO;

public static class UIElementsPath
{
    public static string FindUxml(string name)
    {
        return Find(name, "uxml");
    }

    public static string FindUss(string name)
    {
        return Find(name, "uss");
    }

    static string Find(string name, string extension)
    {
        var assetHashs = AssetDatabase.FindAssets(name);
        var results = assetHashs
            .Select(hash => AssetDatabase.GUIDToAssetPath(Path.GetFileNameWithoutExtension(hash)))
            .Where(assetPath => Path.GetFileNameWithoutExtension(assetPath) == name)
            .Where(assetPath => Path.GetExtension(assetPath) == "." + extension);

        // 同じ名前のUIElementsが複数存在する(名前空間が違う等)場合の警告。
        if (1 < results.Count())
        {
            Debug.LogWarning($"\"{name}\"で{results.Count()}件の結果が見つかりました。\n\n{results.Aggregate((a, b) => a + "\n" + b)}");
        }

        return results.FirstOrDefault();
    }
}
```

このメソッドを使用すると、先程のコードは以下のように書き換えることができます。

```cs
var visualTree = AssetDatabase.LoadAssetAtPath<VisualTreeAsset>(UIElementsPath.FindUxml("SampleWindow"));
var styleSheet = AssetDatabase.LoadAssetAtPath<StyleSheet>((UIElementsPath.FindUss("SampleWindow"));
```

# おわりに

まだ開発を効率よく進めていくには多少の工夫が必要なUIElementsですが、はじめにお伝えしたとおり将来のUnityのUIはこのフレームワークを利用して開発していくことになるので、早めに触れておくことをオススメします。

明日は@karizumaiさんです！