UI Elements & UI Builderをつかってみた

はじめに

こんにちは。
KLabのテクニカルアーティストグループに所属しているこかろまと申します。

新規プロジェクトへテクニカルアーティストとしてコミットする中で、組織横断的にテクニカルアーティストグループへのリード業務とマネジメント業務を行っています。

KLab Creative Blogでは「Maya Pythonの undo・redo あれこれ」以来の執筆となります。
今回の記事は入門ということで、触りの内容となりますが予めご了承ください。

UI Elementsとは?

UIElements は Unity の新しいリテインドモードの UI フレームワークで、現在は Unity 2019.1 のパブリック API として提供されています。現在の形は、Unity エディターの拡張を容易にするツールです。今後のリリースで、ゲーム内サポートやビジュアルオーサリングが追加される予定です。

(リンク元 : https://blogs.unity3d.com/jp/2019/04/23/whats-new-with-uielements-in-2019-1/)

ここでの「Retained Mode」とは実装側の呼び出しによって必ず描画が発生するのではなく、ライブラリ内で維持しているモデルが更新される際に描画を更新するモードになります。

そのため、「Immediated Mode(即時モード)」である、従来Editor拡張で利用されてきたIMGUIと比べて描画・レンダリングコストの最適化を行いやすいため、パフォーマンスの向上が期待できます。


image2.png

ImmediateMode

image1.png

Retained Mode

(引用 : https://docs.microsoft.com/ja-jp/windows/win32/learnwin32/retained-mode-versus-immediate-mode)

また、IMGUIにおけるエディタ拡張では、C#スクリプト内でGUIと実装をセットにするのが基本でした。

レイアウトの微調整など、ほんの少しの調整でコンパイルし直す必要があったり、実装内容においてはUIと実装が強く密結合しがちなため、開発するツールに破壊的な仕様変更が加わった際への柔軟な対応ができない...など、様々な点において課題がありましたが、UI Elementsではスタイルシート・データ構造・実装ロジックそれぞれを分離し、より抽象化に重きをおいた設計となっています。

これにより、UIアーティスト、プログラマがそれぞれで分担して作業ができること、データと実装それぞれを別用途で流用しやすくなり、よりスケーラブルな開発ができることが期待されます。

UI Elements自体は UIToolkit 内の UI Systemの中核をなすシステムとして、Unity 2019.1でついにexperimental(実験的要素)が外れ、 エディタで正式に利用可能になりました。

Unity社自身も「今後の新規UI開発プロジェクトには UI Toolkit を利用することを推奨する」としていますが、2020/07 現在ではIMGUI や uGUI で利用可能ないくつかの機能はまだ利用できません。どのソリューションが現在最適解なのかについてはUnity社が公式で発表している比較表を参考にすることを推奨します。

UI Builder とは?

UIToolkitには、UIの作成に役立ついくつかのツールとリソースが含まれています。

Webブラウザにおける「Webページの検証ツール」に近い「UI Debugger」と、
UIの配置をグラフィカルに行える 「UI Builder」 になります。

image7.png

UI Debugger

image3.png

UI Builder

従来のIMGUIでは前述の通り、C#スクリプト内のコーディングでUIを組んでいくのが基本であり、簡易的なワイヤーフレーム、プロトタイピングを組み上げるにも時間がかかっていましたが、UI Elementsによってスタイルシート・UIデータ構造・実装ロジックが分けられたことにより、このうちUIデータ構造をGUIで作り上げることが可能になりました。

Qt Designerなどのレイアウト作成ツールに、Unityの操作性をプラスしてよりUnityに慣れている方向けに扱いやすくした印象を受けます。

2020/07 現在では1.0までバージョンは引き上げられていますが、まだ preview は取れておらず、あくまで開発途中版であることに注意してください。

    • previewなのは UI内部の実装ロジック側(UI Elements) ではなく、
      UIアセットを構成するためのエディタツール(UI Builder) ということ
      • 破壊的変更による影響を受けても軌道修正が比較的容易であることが想定される

  • UI Builder自体、もう1.0ということで正式バージョンのリリースも間近では?

ということを鑑みて今後のプロジェクトでUI Elementsを正式採用するためにも、
まずは触ってみるべきという気持ちで入門してみることにしました。

開発環境は Unity 2019.3.7f1 & Universal RP 7.4.1 です。

インストール・簡単なUIをつくる

Package Managerを利用してインストールします。

2020/07現時点ではまだpreviewですので、「All Packages」表示・Advancedから
「Show Preview Packages」に切り替え、「UI Builder」をリストからインストールします。

インストールが完了すると、「Window」→「UI」→「UI Builder」からツールを表示することができます。


image11.png

左下のLibraryウィンドウから、UI要素をドラッグ&ドロップして配置します。

これらのUI要素をElementと呼び、各要素を自作することも可能です。

(参考 : https://www.slideshare.net/UnityTechnologiesJapan/uielementsui-buildereditor)

今回は簡易的なUIをまず作って表示したいので、ボタンを配置するにとどめます。

image6.png

左下のLibrary(Standard)から、Buttonをドラッグ&ドロップします。

image8.png

配置したボタンを選択後、右上のInspectorビューから配置したElementに対するプロパティを設定することができます。

プロパティにはボタンのサイズやアライン、位置、ボーダー線など、さまざまなカスタマイズを行うことが可能です。

今回はそのオブジェクトを一意に特定できる名前である、Nameのみを設定しておきます。

Button直下のNameに「TestButton」と設定し、Save(Ctrl+S)します。

image13.png

保存したアセットのUI要素は上図のように、実際に適用する前にUnityのInspectorから表示することも可能です。

次に作成したUI構成データを使い、カスタムインスペクタとしてUI描画を行ってみます。

UI Elementsによるカスタムインスペクタ描画と
バインディングをしてみる


通常のCustomEditorでの実装と同じく、実装とカスタムインスペクター拡張用の.csをそれぞれ下記のように用意しました。

TestTools.cs(実装)

using System.Collections;
using System.Collections.Generic;
using UnityEngine;
 
public class TestTools : MonoBehaviour
{
}

TestToolsInspector.cs(UI描画のカスタムインスペクター拡張)

using UnityEngine;
using UnityEditor;
using UnityEngine.UIElements;
using UnityEditor.UIElements;
 
[CustomEditor(typeof(TestTools))]
public class TestToolsWindow : Editor
{
   private TestTools _testTools;
   private VisualElement rootElement;
   private VisualTreeAsset visualTree;
 
   public override VisualElement CreateInspectorGUI()
   {
    _testTools = (TestTools)target;
       rootElement = new VisualElement();
 
       // 作成したUXMLデータ構造を読み込みます
       visualTree = AssetDatabase.LoadAssetAtPath("Assets/Editor/Test/TestTools.uxml");
      
       // スタイルシートデータを読み込みます(今回は作成していないのでコメントアウト)
       //StyleSheet stylesheet = AssetDatabase.LoadAssetAtPath("Assets/Editor/Test/TestTools.uss");
       //rootElement.styleSheets.Add(stylesheet);
 
       // 読み込んだツリー構造体をルートのUI要素としてクローン
       visualTree.CloneTree(rootElement);
 
       return rootElement;
   }
}

この状態でコンパイルし、作成したTestTools.csをコンポーネントとしてアタッチすると、
これだけでUI Elementsでの描画がされます。

image9.png
UI構造データがスクリプト依存していないため、UI Builderなどでの編集もRepaint(画面を切り替えるなど)さえすれば、すぐ反映されます!

ただ、これだけではただ表示しているだけなので、何かしらスクリプトとUIをバインディングをしてみます。

image12.png

数値を入力するFieldを追加しました。

バインドするための名前がオブジェクト名とは別に必要なので、BindingPathに一意に特定できるパス名を入力します。今回は「testfield」としました。

前段のコードにいくつか加筆修正を行います。

実装側の「TestTools.cs」には、先ほど用意したバインディングパス名と同一のSerializeField属性を持つプロパティを用意します。

さらに前段で作成したボタンのシグナル処理も追加したいので、10ずつ値を増やすだけの関数を別途用意しました。

TestTools.cs(実装)

public class TestTools : MonoBehaviour
{
   [SerializeField] private int testfield = 0;
   public void Plus(){
       testfield += 10;
   }
}

そして、カスタムインスペクター側のVisualElementに対して、Bind(seriaiizeObject)のコードを記述します。

TestToolsInspector.cs(UI描画のカスタムインスペクター拡張)

public override VisualElement CreateInspectorGUI()
{
    rootElement = new VisualElement();
    rootElement.Bind(serializedObject);
 
    ~~~~省略~~~~
    rootElement.Q<Button>("TestButton").clickable.clicked += () => _testTools.Plus();
    return rootElement;
}

たったこれだけでスクリプト側とのプロパティバインドが可能になります。

image4.gif

画面を切り替えてもシリアライズが維持できていることがわかります。

非常に便利です!!

ShaderGUIの置き換えをしてみる

続いて、インスペクター拡張の応用例であるShaderGUIに挑戦してみます。
ネット上ではあまり見当たらなかったので、試しに作成してみました。

今回はマテリアルインスペクタを UI Elementsで描画し、ColorFieldをシェーダープロパティの「_BaseColor」にバインディングしてみます。

image15.png

こんな感じのUIをUI Builderで作成しました。
(UI Elementsならではの機能を足してみたかった、という意味でとりあえず用意されているElementを大量に置いてみました。UIとしての意味は全くありません。)

対象のShaderは下記のような、メインテクスチャとカラー情報しかプロパティにない、簡易的なものを用意しました。

URPUnlitShader.shader

Shader "Custom/URP/UnlitTexture"
{
   Properties
   {
       [MainColor] _BaseColor("BaseColor", Color) = (1,1,1,1)
       [MainTexture] _BaseMap("BaseMap", 2D) = "white" {}
   }
 
   SubShader
   {
       Tags { "RenderType"="Opaque" "RenderPipeline"="UniversalRenderPipeline"}
 
       Pass
       {
           Tags { "LightMode"="UniversalForward" }
 
           HLSLPROGRAM
           #pragma vertex vert
           #pragma fragment frag
          
           #include "Packages/com.unity.render-pipelines.universal/ShaderLibrary/Core.hlsl"
 
           struct Attributes
           {
               float4 positionOS   : POSITION;
               float2 uv           : TEXCOORD0;
           };
 
           struct Varyings
           {
               float2 uv           : TEXCOORD0;
               float4 positionHCS  : SV_POSITION;
           };
 
           TEXTURE2D(_BaseMap);
           SAMPLER(sampler_BaseMap);
          
           CBUFFER_START(UnityPerMaterial)
           float4 _BaseMap_ST;
           half4 _BaseColor;
           CBUFFER_END
 
           Varyings vert(Attributes IN)
           {
               Varyings OUT;
               OUT.positionHCS = TransformObjectToHClip(IN.positionOS.xyz);
               OUT.uv = TRANSFORM_TEX(IN.uv, _BaseMap);
               return OUT;
           }
 
           half4 frag(Varyings IN) : SV_Target
           {
               return SAMPLE_TEXTURE2D(_BaseMap, sampler_BaseMap, IN.uv) * _BaseColor;
           }
           ENDHLSL
       }
   }
   CustomEditor "ShaderGUICustomInspector"
}

従来のShaderGUIの実装と同様に、これから作成するカスタムマテリアルインスペクタ用のクラス名を明示した、

CustomEditor "ShaderGUICustomInspector"

を記述します。

ShaderGUIはUI Elementsで描画するための関数が実装されているEditorを継承していないため、Editorを継承しているMaterialEditor側を継承してUI を実装します。

また、MaterialEditorでInspector描画を行う際、従来は「OnInspectorGUI()」でしたが、
こちらを「CreateInspectorGUI()」に変更する必要があります。

先ほどと同様に、ルートのVisualElementを作成し、VisualTreeと(必要であれば)StyleSheetを適用します。

ShaderGUICustomInspector.cs

using UnityEngine;
using UnityEditor;
using UnityEngine.UIElements;
using UnityEditor.UIElements;
 
public class ShaderGUICustomInspector : MaterialEditor
{
    private VisualElement rootElement;
    private VisualTreeAsset visualTree;
 
    // マテリアルへのアクセス
    Material material{
        get{
            return (Material)target;
        }
    }
 
    public override VisualElement CreateInspectorGUI()
    {
        rootElement = new VisualElement();
        rootElement.Bind(serializedObject);
 
        visualTree = AssetDatabase.LoadAssetAtPath("Assets/Editor/Test/TestTools.uxml");
 
        visualTree.CloneTree(rootElement);
 
        return rootElement;
    }
}

マテリアルにシェーダーを適用して表示してみます。

無事、UI Elementsを利用してInspector描画ができました。

image10.png

従来の ShaderGUI、MaterialEditorではできなかった表現も可能です。

image14.png

上記のように、同じUIデータ構成を利用して、それぞれ別のカスタムインスペクター、ShaderGUI間での流用も可能です。

今回の例ではあまり意味がありませんが、各機能ごとにUIを作成して、Treeとしてマージして再利用、なんて使い方もできるかと思います。

現状ではただUIを乗っ取って表示しているだけであり、シェーダー側のプロパティと連動できていない状態なので、シェーダープロパティとバインディングしてみます。

値が変更されたタイミングでコールバックする関数「RegisterValueChangedCallback」があるので、コールバック時に「_BaseColor」プロパティに「colorField」の値を渡します。

var colorField = _RootElement.Q("MainColor");
colorField.RegisterValueChangedCallback(evt =>
{
    material.SetColor ("_BaseColor", colorField.value);
});

バインディングした結果は以下の通りです。先ほどのカスタムインスペクター同様、シリアライズ しないと値が保持されない点には注意が必要です。

image5.gif

まとめ

最近までUnity2018以前のプロジェクトに携わっていたというのもあり、初めて UI Elements と UI Builder を使ってみましたが、Editor拡張の作り方は今後だいぶ変わってくるな、という印象を受けました。

今回は紹介できませんでしたが、パフォーマンス向上を生かして、画像を大量に利用したアニメーションUIや、スタイルシートの適用なども行うことでそれこそUnityのEditor拡張とは思えないほどゴージャスなUIを作ることも可能だと思います。

ただ、業務でも非常に簡易的なツール(それこそボタンを一つおいて実行するだけのツール)を作ることはそれなりにあります。

そういったツールにおいては従来のIMGUIの実装の方が手っ取り早い可能性もあります。

(※UI Elements上でIMGUI描画を行う関数もあります)

また、いくつかのネット記事ですでに UI Elements を触れられた方の感想の中に「UIの表現力が高くなりすぎるあまりにツールの統一感がなくなるのでは」という危惧を受けた感想もありました。

個人で利用する分にはさほど大きな問題にはならないかもしれませんが、 UI Elements へ移行する場合は、エディタ拡張ツールにおいても、ユーザーにとって扱いやすいUI / UXのスタイルガイドラインやcssの共通テンプレート化などを検討する必要が将来的には出てくるのかもしれません。

とはいえ、開発目線では実装とデータ構造が分離できるだけでも大きなメリットを感じました。

最後まで読んでいただきありがとうございました。

かなり基本的な入門記事でしたが、本記事が何かしら手助けになれば幸いです。

このブログについて

KLabのクリエイターがゲームを制作・運営で培った技術やノウハウを発信します。

関連記事

このブログについて

KLabのクリエイターがゲームを制作・運営で培った技術やノウハウを発信します。