Help us understand the problem. What is going on with this article?

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

PONOS Advent Calendar 2019の13日目の記事です。

昨日は@nimitsuさんのGameLift RealTimeServerで遊んでみよう for Unity(Unity編)でした。

はじめに

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を実装し、リロードするためのメニューアイテムを登録するようにしています。

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

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

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

問題

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

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

解決

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

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();
    }
}

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

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

おわりに

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

明日は@karizumaiさんです!

Why not register and get more from Qiita?
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away
Comments
No comments
Sign up for free and join this conversation.
If you already have a Qiita account
Why do not you register as a user and use Qiita more conveniently?
You need to log in to use this function. Qiita can be used more conveniently after logging in.
You seem to be reading articles frequently this month. Qiita can be used more conveniently after logging in.
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away
ユーザーは見つかりませんでした