【UI Toolkit】ランタイム(ゲーム側)とエディタ側それぞれでUIを表示してみる【Unity】

UI Toolkitは、UnityのUIを開発するための機能です。ランタイム(ゲーム部分)側におけるuGUI、エディター側におけるIMGUIに代わるランタイム、エディター共に利用できる新しいUIとして現在も開発が続けられています。すでにエディター側は既存のIMGUIと比較しても実用に耐えうるレベルになっています。ランタイム側はワールドで利用できない、パーティクルエフェクト等の使用ができない等、まだまだ実験的な部分があります。

UI Toolkitは、HTML、CSSに似た、UXML、USSファイルに記述します。そのため、プログラマとデザイナーの分業や構造的な開発ができる点が特徴です。UXML、USSは本家C#でGUIアプリケーションを開発する際に利用するようなグラフィカルインターフェイスを備えたツール「UI Builder」を用いて視覚的に更新が可能です。また、プログラマの方はUXML、USSへ記述する該当箇所をc#コードオンリーでの記述も可能です。

UI BuilderでUIを開発する

例として簡単な足し算電卓をUIを構築してみます。数字を入力するInt型の入力フィールドがあり、入力してからボタンを押すと、ラベルに今まで入力した数字の合計が表示されます。また、クリアボタンを押すと数値が0になります。

ProjectViewを右クリックしてCreate > UI Toolkit > UI Documentを押すとUXMLファイルが生成されます。ファイル名をSampleUI.uxmlに変更します。生成されたUxmlファイルをダブルクリックすることでUI Builderが開きます。

Unity UI Builder

UI Builder画面の構成は以下の通りです。

  • Hierarchy(画面左中央):Uxmlに記述している構造順に要素を表示します。
  • Library(画面左下):選択できる要素一覧。ここからHierarchyにドラッグ&ドロップすることで要素を追加します。
  • StyleSheets(画面左上):このUxmlに反映させるUSSファイルを追加・選択・削除ができます。
  • Vieport(画面中央):この設定で実際に表記される画面をプレビューします。さらに一部はドラッグしてサイズを変えたりも可能。
  • Inspector(画面右):Hierarchyで選択した要素のプロパティが表示されます。

まず、Hierarchyに必要な要素を追加します。LibraryからHierarchyに「Label」、「Integer」、「Button」、「Button」、「Label」の順番で要素をドラッグ&ドロップします。

Unity UI Builder

ラベル・テキストの初期値や、名前をつけたりします。Hierarchyで各要素を選択してInspectorで次のように入力してください。

要素NameLabelTextValueTab
Index
Label1つ目lblAdd足し算
IntegerintInput空白に
する
00
Button1つ目btnAdd足し算1
Button2つ目btnClearクリア2
Label2つ目lblAnswer0
  • Name:要素名。プログラム側からその要素にアクセスするために使用します。できれば一意な名前が望ましいです。
  • Label:入力フィールド系要素はフィールドに左にラベルを表示し、そのラベルに表示する文字。空白だとラベルが表示されなくなる。
  • Text:要素に表示する文字。
  • Value:入力フィールドの初期値
  • Tab Index:タブで移動する順番。-1だと移動しない。
Unity UI Builder3

ボタンが縦並びだと気持ち悪いので横並びにします。そのため、ボタンを新たな何もない要素で囲みます。LibraryからVisualElementをHierarchyのintInputの下にドラッグ&ドロップ。btnAddとbtnClearは二つを追加したVisualElementにドラッグ&ドロップすることで入れ子構造にできます。

画面上の見た目は同じですが、プログラム上ではVisual Element(NameをveBtnRootに変更)の子要素として存在します。さらにveBtnRootに子要素を横並びにするスタイルを設定します。UI Builderで子要素を設定する方法は、Inlined Stylesを更新するか、適用するUSSファイルを追加し、USSファイルを更新する2種類の方法があります。前者は更新した要素のみに適用され、後者は設定した条件化の要素全てに同じスタイルを適用できます。

今回はUSSファイルを作成する方法で実現してみます。

StyleSheets(画面左上)画面のプラスを押すとUssファイルを追加できます。今回は新規で作成するので、Create New Ussを選択。フォルダーはUxmlファイルと同じ位置にします。

Unity UI Builder34

USSはCSSファイルと基本の書き方は同じです。そのため、.〇〇〇で記述するとUxml側でその名称でクラス定義した要素全てに反映します。先ほど作成したUssファイルを選択してInspectorのSelector項目に.ButtonBoxと記述してCreate New USS Selectorボタンを押します。

Unity UI Builder5

veBtnRootのInspectorでStyle Class Listに.ButtonBoxを追加します。これでUSSファイルの.ButtonBoxで設定したスタイル項目がVeBtnRootに反映されるようになります。

StyleSeetsのSampleUI.ussのFoldoutを拡げると先ほど追加したセレクター.ButtonBoxが出てきますので選択し、InspectorのStyles > Flex項目のDirectionから一番右を選択します。これは、要素内にある子要素を右並びにするようにします。Viewportを見るとボタンが右寄せ横並びになっていることが分かります。

次は、計算結果を表示するlblAnswerを格好よくします。同じようにUSSに新たな定義を作ってもよいですが、汎用性がなさそうなので、lblAnswerのStyleに直接記述してみます。まず、が、絶対座標で好きな位置に配置できるようにします。

lblAnserを選択肢、InspectorのInlined Stylesにある項目PositonをRelativeからAbsoluteに変更します。Uxmlの基本は左上から構造順に順々に表示するHtmlと同じような表示形式です。Absoluteにすると絶対座標配置となり、自由な位置に配置ができるようになります。

ViewportでlblAnswerをドラッグし、画面中央ぐらいに持ってきてドロップしてください。さらにViewportで要素の4隅の線を引き延ばして要素のサイズを大きくします。あとはPosition下の数値を微調整します。

その後、色々微調整します。

Text Size48
Text Color
Text Alian中央を選択
Background Color水色

少々不格好ですが、これで一応、デザイン側は完成とします。保存はVieportのFile > Saveで行います。UxmlとUssファイルに値が反映されます。なお、主動でUxmlとUssを変更することも可能です。UI Builder側と単語がほぼ同じため、何をやっているかは見ればわかるかと思います。

<ui:UXML xmlns:ui="UnityEngine.UIElements" xmlns:uie="UnityEditor.UIElements" xsi="http://www.w3.org/2001/XMLSchema-instance" engine="UnityEngine.UIElements" editor="UnityEditor.UIElements" noNamespaceSchemaLocation="../UIElementsSchema/UIElements.xsd" editor-extension-mode="False">
    <Style src="project://database/Assets/SampleUI.uss?fileID=7433441132597879392&amp;guid=8797b0f7d7c19f64bae9435890e51240&amp;type=3#SampleUI" />
    <ui:Label tabindex="-1" text="足し算" display-tooltip-when-elided="true" name="lblAdd" />
    <ui:IntegerField value="0" name="intInput" />
    <ui:VisualElement name="veBtnRoot" class="ButtonBox">
        <ui:Button tabindex="-1" text="クリア" display-tooltip-when-elided="true" name="btnClear" />
        <ui:Button tabindex="-1" text="足し算" display-tooltip-when-elided="true" name="btnAdd" />
    </ui:VisualElement>
    <ui:Label tabindex="-1" text="0&#x9;" display-tooltip-when-elided="true" name="lblAnswer" style="position: absolute; top: 180px; left: 68px; width: 225px; height: 82px; align-items: stretch; font-size: 48px; background-color: rgba(73, 253, 255, 255); color: rgba(238, 10, 10, 255); -unity-text-align: middle-right; white-space: nowrap; right: auto;" />
</ui:UXML>
.ButtonBox {
    flex-wrap: nowrap;
    flex-direction: row-reverse;
}

UI Toolkitをランタイム側で利用する

Hierarchyを右クリックし、UI Toolkit > UI Documentを選択します。ProjectビューにはUI Toolkitフォルダが作成され、Panel Settingアセットが自動的に生成され、作成したUI Documenntコンポーネントに自動的にアタッチされます。

UI DocumenntのInspectorの項目Source Assetに、先ほど作成したSampleUI.uxmlをアタッチします。

プレイモードにすると、表示されているのが分かります。

lblAnswerがUI Builderだと中央に配置されていたのに、ランタイムだと左寄りになるのは、Positionが左と上の絶対値配置をしているからです。今回はデザインが趣旨ではないため省きますが、このあたりの調整はHTML、CSS系と同じようになります。

次はプログラムから、足し算ボタンを押した時、値を足す計算、及びクリアボタンを押した時に0になる計算をします。

SampleOperator.csを作成し、UIDocumentゲームオブジェクトにコンポーネントとして加えます。コードは次の通りになります。

using UnityEngine;
using UnityEngine.UIElements;
public class SampleOperator : MonoBehaviour
{
    void Start()
    {
        var doc = GetComponent<UIDocument>();
        var root = doc.rootVisualElement;

        var uiIntInput = root.Q<IntegerField>("intInput");
        var uiBtnAdd = root.Q<Button>("btnAdd");
        var uiBtnClear = root.Q<Button>("btnClear");
        var uiLblAnswer = root.Q<Label>("lblAnswer");

        //足し算ボタン押下
        uiBtnAdd.clickable.clicked += () =>
        {
            int answer = uiIntInput.value + int.Parse(uiLblAnswer.text);
            uiLblAnswer.text = answer.ToString();
        };

        //クリアボタン押下
        uiBtnClear.clickable.clicked += () =>
        {
            uiLblAnswer.text = "0";
        };

        var test = root.Query<Button>();
    }
}

UI Toolkitのラベルやボタンなど各要素はVisualElementを継承したクラスです。UI BuilderのHierarchyのように各要素は親子関係で結び付けられます。GameObjectの親子構造と似たイメージです。

rootVisualElementには、Uxmlで適用した構造やスタイルが親子構造で格納されています。つまりintInputやbtnAdd、veButtonBoxなどがrootVisualElementの直下にあり、さらにveButtonBoxにはbtnAddやbtnClearが親子関係で格納されています。

以下の方法で対象VisualElement以下にある全要素(例のveButtonBox以下の要素も含む)から該当要素1つを取得することができます。

VisualElement.Q<VisualElement要素クラス>("Uxmlで定義した要素の名前");

該当する要素が2件以上ある場合はまとめて取得することも次のように可能です。Uxmlで定義した要素の名前は省略可能です。

VisualElement.Query<VisualElement要素クラス>("Uxmlで定義した要素の名前");

UI Toolkitを独自エディタで利用する

ランタイムと同じようにエディタでも簡単に表示することができます。ただし、ランタイムと異なりUxmlのアタッチができないため、明示的にUxmlを呼び出す必要があります。それ以外はEditorでも共通のAPIを利用してVisualElementを操作することができます。

using UnityEditor;
using UnityEngine.UIElements;

public class SampleWindow : EditorWindow
{
    [MenuItem("Sample/Sample1")]
    public static void ShowWindow()
    {
        var window = GetWindow<SampleWindow>("SampleWindow");
    }

    private void OnEnable()
    {
        var visualTree = AssetDatabase.LoadAssetAtPath<VisualTreeAsset>("Assets/SampleUI.uxml");
        var root = rootVisualElement;
        visualTree.CloneTree(root);

        var uiIntInput = root.Q<IntegerField>("intInput");
        var uiBtnAdd = root.Q<Button>("btnAdd");
        var uiBtnClear = root.Q<Button>("btnClear");
        var uiLblAnswer = root.Q<Label>("lblAnswer");

        //足し算ボタン押下
        uiBtnAdd.clickable.clicked += () =>
        {
            int answer = uiIntInput.value + int.Parse(uiLblAnswer.text);
            uiLblAnswer.text = answer.ToString();
        };

        //クリアボタン押下
        uiBtnClear.clickable.clicked += () =>
        {
            uiLblAnswer.text = "0";
        };
    }
}

UI ToolkitをInspector拡張で利用する

Inspector拡張でも同様に利用できます。例として足す値と答えを保持するScriptableObjectの拡張を作ってみます。Inspector拡張の場合は、戻り値にルートとなるVisualElementを返すようにします。

using UnityEngine;

public class SampleDatabase : ScriptableObject
{
    public string AddNumber;
    public string AnswerNumber;
}
using UnityEditor;
using UnityEngine.UIElements;

[CustomEditor(typeof(SampleDatabase))]
public class SampleDatabaseEditor : Editor
{
    public override VisualElement CreateInspectorGUI()
    {
        serializedObject.Update();

        var visualTree = AssetDatabase.LoadAssetAtPath<VisualTreeAsset>("Assets/SampleUI.uxml");
        VisualElement root = new VisualElement();
        visualTree.CloneTree(root);
        
        //足す数字
        var uiIntInput = root.Q<IntegerField>("intInput");
        uiIntInput.value = serializedObject.FindProperty("AddNumber").intValue;

        //答え
        var uiLblAnswer = root.Q<Label>("lblAnswer");
        uiLblAnswer.text = serializedObject.FindProperty("AnswerNumber").intValue.ToString();

        var uiBtnAdd = root.Q<Button>("btnAdd");
        var uiBtnClear = root.Q<Button>("btnClear");

        //足し算ボタン押下
        uiBtnAdd.clickable.clicked += () =>
        {
            int answer = uiIntInput.value + int.Parse(uiLblAnswer.text);
            uiLblAnswer.text = answer.ToString();

            serializedObject.FindProperty("AddNumber").intValue = uiIntInput.value;
            serializedObject.FindProperty("AnswerNumber").intValue = answer;
            serializedObject.ApplyModifiedProperties();
        };

        //クリアボタン押下
        uiBtnClear.clickable.clicked += () =>
        {
            uiLblAnswer.text = "0";
            serializedObject.FindProperty("AddNumber").intValue = 0;
            serializedObject.ApplyModifiedProperties();
        };

        return root;
    }
}

コメントを残す

メールアドレスが公開されることはありません。 が付いている欄は必須項目です

CAPTCHA