見出し画像

Unityの新UIシステムUI ToolkitをREALITYアプリの配信画面に導入した話 Now in REALITY Tech #85


はじめに

はじめまして、執筆者の wakapippi と申します。社内では、主に蟹の魅力について布教を行う仕事をしている Unityエンジニアです。今回の「Now in REALITY Tech」では、蟹の魅力をみなさんにも紹介しつつ、UI Toolkitを導入した話をしていきます。

REALITYでは、この夏に配信・視聴画面のUIを大幅に更新しました。
夏といえば毛蟹ですね。蟹というと冬が旬なイメージがありますが、実は北海道産で有名な毛蟹は初夏頃が旬です!

毛蟹は初夏頃が旬です


今回のUI刷新で新規開発することになった画面のうち、Unityで実装されるのは配信のUIですが、今回は従来のuGUIではなく、Unityの新UIシステムであるUI Toolkitを使ってみました。

割安で食べることができる「水ガニ」

蟹といえば、高くてなかなか食べる機会がない、というイメージを抱きがちですが、安く入手して食べる方法もいくつか存在します。今回は、美味しくて価格も安い「水ガニ」について紹介します。
水ガニという種類の名前があるわけではなく、脱皮したてで殻の柔らかいズワイガニを水ガニ(地域によってはズボガニ)と呼びます。

水ガニは、見た目が悪い、身入りがやや悪い、味噌が食べられないという点から、普通のズワイガニの3分の1以下の値段で食べられますが、味は全く普通のカニに劣りません。また、殻が柔らかく身入りがやや悪い分剥くのも簡単で食べやすいです。

3月に、水ガニを食べる蟹パーティを開催しました。一人3,4杯食べれる実質食べ放題状態で、お酒もついて実費で一人3000円以下で済みました。

会社で開催した蟹パーティ

そんな水ガニなんですが、実は漁業資源保護のため、2月後半〜3月前半の約一ヶ月間、福井・兵庫・鳥取・島根の4県でのみしか水揚げしていないので、入手したい方はぜひそのタイミングに4県でチェックしてみてください!

UI Toolkitの特徴

UI Toolkitは、Webで使用されている HTML/CSS と似たようなワークフローでUIの開発ができる仕組みです。以前は「UI Elements」という名前で、エディタ拡張のUIを作るのに使われていましたが、現在ではエディタだけではなくランタイムで動くUIも作成できます。

特徴や基本的な導入方法については、こちらのUnity公式のチュートリアルを参考にするとわかりやすいと思います。https://forpro.unity3d.jp/unity_pro_tips/2022/04/21/3629/

ここからは、今回のUI実装で利用した幾らかの機能について触れていきます。

視覚的にUIを作成できる「UI Builder」

UI Builderの画面

UI Toolkitの要素は、uGUIのようにGameObjectとしてHierarchy上に配置するわけではないため、基本的にはテキストベースのUXML/USSを編集して配置することになりますが、視覚的にUIを作成可能な「UI Builder」というツールを使う手もあります。このツール上でUIを編集すると、対応するUXML/USSが出力されます。
左側のHierachyビューでは配置する要素のツリーを管理でき、中央のViewportビューでは見た目を確認でき、右側のInspectorビューではスタイルの値を設定、確認ができます。
要素のスタイルのみならずクラスに設定したスタイルも触れますし、スタイルをクラスに展開するのもワンクリックでできるなど、かなり高機能です。

実行時にUIの描画状態を確認できる「UI Toolkit Debugger」

上でも述べたように、UI Toolkitで描画されている要素はGameObjectではないので、実行時に通常のHierarchyを見てもどのような状態で描画されているのかがわかりません。
要素の描画状態などを確認したい場合は、「UI Toolkit Debugger」を使います。
Webの要素検証ツールと同じ使い勝手で、要素の描画領域を確認したり、プロパティの値を変更したりすることができます。

UI Toolkit Debuggerの画面
Webの要素検証ツールと似ている

Queryで要素を操作する

UI Toolkitでは、UQuery と呼ばれる、jQueryに似たAPIで要素にアクセスできます。要素に対して見た目を変えたりイベントを取得したりといった操作を行うときには、UQueryを使います。
以下のコードのように、かなり直感的に使えます。また、Linqとの相性も良いです。

// 名前が Title であるUI要素を1つ取得する
var titleElement = document.rootVisualElement.Q("Title");

// 型が ScrollView であるUI要素を1つ取得する
var scrollElement = document.rootVisualElement.Q<ScrollView>();

// クラスが SampleClassNameである要素を1つ取得する
var elementWithSampleClass = document.rootVisualElement.Q(className: "SampleClassName");

// 型が Button であるUI要素を全て取得する
var buttonList = document.rootVisualElement.Query<Button>();

機能が足りない場合は、拡張した独自のUI Elementを作成できる

UI Toolkitでは、「ボタン」「リストビュー」「プログレスバー」など、さまざまなビルトインのUI要素が存在しますが、機能が不足する場合は、自分でVisualElementsクラスを拡張した要素を作ることができます。

REALITYでは、コメントの背景などに線型グラデーションを使っています。Webの場合、線型グラデーションはCSSの linear-gradient を使用することで簡単に実現できますが、UnityのUSSには対応する機能がないので、独自に作成しました。

public class GradientVisualElement : VisualElement
{
    private Color BottomColor { get; set; }
    private Color TopColor { get; set; }
    private bool Rotate { get; set; }

    public new class UxmlFactory : UxmlFactory<GradientVisualElement, GradientUxmlTraits>
    {
    }

    public class GradientUxmlTraits : UxmlTraits
    {
        public UxmlColorAttributeDescription TopColor = new UxmlColorAttributeDescription
            { name = "top-color" };

        public UxmlColorAttributeDescription BottomColor = new UxmlColorAttributeDescription
            { name = "bottom-color" };

        public UxmlBoolAttributeDescription Rotate = new UxmlBoolAttributeDescription { name = "rotate" };

        public override void Init(VisualElement ve, IUxmlAttributes bag, CreationContext cc)
        {
            base.Init(ve, bag, cc);
            var gradVe = ve as GradientVisualElement;
            gradVe.TopColor = TopColor.GetValueFromBag(bag, cc);
            gradVe.BottomColor = BottomColor.GetValueFromBag(bag, cc);
            gradVe.Rotate = Rotate.GetValueFromBag(bag, cc);
        }
    }    

    public GradientVisualElement()
    {
        generateVisualContent += GenerateVisualContent;
    }

    private void GenerateVisualContent(MeshGenerationContext mgc)
    {
        // ... 実装は省略
    }
}

上記は、基本的な独自UI要素を作成するときのコード雛形です。
VisualElement、UxmlTraits、UxmlFactoryを継承したクラスを作成します。

  • VisualElement : UI要素そのもののクラス。generateVisualContentコールバックを登録することで、要素が再描画されるときにメッシュに対して様々な処理を行えます。描画関係は基本ここで実装します。

  • UxmlTraits : 独自要素のAttributeなどを定義するクラス。指定されいくつかの方でプロパティで定義すること、対応するAttributeを宣言できます。このAttributeはUI BuilderのInspectorにも表示されます。例えばUxmlColorAttributeDescription型のプロパティを定義すると、色を指定するAttributeになります。要素作成時にInitメソッドが呼ばれるので、このメソッドないで要素への情報引渡しなどを行います。


詳しくは、Unityのドキュメントをご参照ください

UI Toolkitのメリットと、UI Toolkitで作るのに向いている画面

REALITYのUnity部にUI Toolkitを入れると言っても、今後作る全ての画面をUI Toolkitで作るとは決めていません。UI Toolkitにより受けられる恩恵が特に多い画面として、配信画面が当てはまるということで、今回はまず配信画面が対象になりました。具体的には次のようなUI Toolkitの特徴が、配信画面と相性が良いと考え、導入しました。

同じ画面で表示パターンが複数ある場合、レイアウト(uss)の変更だけで完結する

例えば、配信画面にあるコメント表示エリアは、同じ画面でも「ブース配信」「ルーム配信・ワールド配信」では表示パターンが異なります。ブース配信ではコメントが画面下部に幅広く表示される一方で、ルーム配信やワールド配信ではアバターの操作がしやすいように上側のやや狭いエリアに表示されています。

ブース配信では、画面下部にコメントが表示されている
ルーム配信では、画面上部にコメントが表示されている


以前の実装では、uGUIで実装していましたので、それぞれの表示パターンで異なるPrefabを用意し、ユーザーがブースとワールドを切り替える操作を行なったタイミングで都度Instantiateしていました。
UI Toolkitで実装する場合、コメント欄に指定するclassをブースとワールド・ルームで使い分け、レイアウトについてはussで記述することで容易に切り替えられます。このように、同じ画面で表示パターンが複数ある場合はUI Toolkitを使うと生産性が上がるかもしれません。

public void SwitchToWorldComment()
{
    var commentContainer = document.rootVisualElement.Q(CommentContainerName);
    commentContainer.RemoveFromClassList(NormalCommentClassName);
    commentContainer.AddToClassList(WorldCommentClassName);
}

コメントの表示切り替えロジックは、これだけで完結します。

複数人で同時作業しがちな画面でもコンフリクトで苦しみにくい

uGUIでは、画面はシーンやPrefabで作られており、これらは複雑な構造になってファイルとして保存されているため、複数人で同時に同じ画面を編集してしまいファイルがコンフリクトした場合、解消させるのが非常に大変です。一方で、UI Toolkitでは画面構造に関する情報はすべてテキストベースのUXMLとUSSに保存されるため、コンフリクトしたとしても容易に解消することができます。将来、複数のメンバーが開発で同時に触る可能性の高い画面であるREALITYの配信画面は、UI Toolkitと親和性が高いと考えました。

線ボーダーや角丸マスクなど、UIデザインでよく使う加工が簡単にできる

これは大変限定されたケースで恩恵と呼べるのか微妙なところですが、UI ToolkitではUSSの記述だけで角丸のマスクや線ボーダーが適用できます。配信画面では、「ユーザーのアイコン」「イベント順位」など至る所に角丸ボーダーがあるのでここも恩恵が大きいです。

角丸が多用されている配信UI

やっぱり高級な蟹も食べたい

先ほどは、安く食べられるということで「水ガニ」を紹介しましたが、やはり高級な蟹も食べたいですよね!
冬の風物詩ともいえる蟹ですが、冬にやっぱり食べたいのが日本海産のズワイガニ。越前蟹、加能蟹、松葉蟹などのブランド名がついている高級品は、刺身でも加熱しても絶品です。蟹味噌も味わえてお酒に大変よく合います。

松葉蟹の刺身
味噌甲羅焼き


UI Toolkit導入で遭遇した問題など

新しい仕組みを入れると当然、特有の制約、癖やバグがあります。ここでは、UI Toolkitで画面を実装する際に遭遇した問題をいくつか紹介します。一般的な問題もあれば、REALITY特有の問題もあります。

一部のuGUIとの共存が場合によっては難しい

UI Toolkitには、uGUIと共存させる仕組みが一応存在します。CanvasがOverlayで描画されているケースでしか動作しませんが、Sort Orderを設定することで、uGUIのCanvasとの前後関係もいい感じになります。
しかし、これ以外のケース、例えばuGUIのRender ModeがCameraになっている場合では、前後関係がそのままでは制御できません。RenderTextureに描画してあげる必要があります。REALITYでは、ワールドでアバターを操作する部分や、ダイアログがOverlayではないuGUIで構成されているので、RenderTextureを使って制御しました。

構造や実装が複雑化しやすいので、基本的にはuGUIとの共存は避けたほうが良いです。


ダイナミックなテキストの表示が難しい

REALITYのUnity部では、ユーザーのコメントにおける名前や本文を表示する箇所があります。あらかじめ決まった文字列ではなく、いろんな言語の文字や絵文字まで表示できるようにする必要があります。ここが一筋縄ではうまくいかず、複数の課題に直面します。
Legacy Textであれば、描画できない文字があった際に自動でOSのフォントを選んでフォールバックしてくれるのですが、UIToolkitはTextMesh Proベースで、自動ではその辺をやってくれません。なので、コード側でOSのフォントをリストアップし、正しくフォールバックさせる必要があります。

var fontPaths = Font.GetPathsToOSFonts(); 
// iOSの場合、同じのパスのフォントが複数格納されるのでDistinctにする
fontPaths = fontPaths.Distinct(StringComparer.Ordinal).ToArray();

上のコードで、OSのフォントリストを取得することができます。
REALITYでは12言語に対応しているので、これらの言語で使う両OSに収録されているフォント名をあらかじめ調べて、フォールバック先として登録するようにします。抜粋したコードの例を示しておきます。

private static readonly List<FontAsset> _fontAssets = new();
private static readonly string[] FontList = new[]
{
    // ラテン系文字
    "Roboto",
    // アラビア語,
    "NotoNaskhArabic",
    // 他にもいろいろありますがここでは割愛
}
private static void SetupFont(){
    // ... 
    // フォントリストにあるフォントと一致する物を抽出
    var otherFonts = fontPaths.Where(x => FontList.Any(s => x.IndexOf(s, StringComparison.Ordinal) >= 0));
    
    // フォントを作成してフォールバック先に指定
    foreach (var fontPath in FontList)
    { 
        var osFont = new Font(fontPath);
        var atlasPopulationMode = AtlasPopulationMode.DynamicOS;
        var fontAsset = CreateDynamicFontAsset(osFont, atlasPopulationMode);
        _fontAssets.Add(fontAsset);
    }
    
    PanelTextSettings set = ScriptableObject.CreateInstance<PanelTextSettings>();
    set.fallbackFontAssets = _fontAssets;
    
    // ...
}

また、これはUIToolkitに限らずUnity全般で言える話ですが、絵文字を表示させるのも苦労しました。今回の実装では、Text要素にspriteを埋め込めるリッチテキストの機能を使って、表示させています。

ビルトインのUI要素の実装にそこそこバグがある

UI Toolkitは割と新めの機能で、Unityのアプデとともに機能もどんどん追加されている状態なので、ビルトインのUI要素にバグがあったりします。今回の実装では、例えば次なようなものがありました。

  • ScrollViewのTouch Scroll TypeをElasticにした場合、ScrollView内の要素が多いと、要素の高さ計算がズレて一番下までスクロールできなくなる問題

  • テキストのシャドウが見切れてしまう問題

  • overflow: hiddenを設定した要素の座標が大きい値の時に、誤った領域がマスクされてしまう問題

他にもありますが、使わないようにしたり、リフレクションを用いて実装を若干変更したりといったワークアラウンドによって問題を回避しました。

最後に

UI Toolkitは割と新しめの機能ということもあって、採用実績を探してもなかなか見つからなく、採用するのを踏みとどまりたくなってしまいますが、今回はメリットが大きいと考えて思い切って導入してみました。

問題に遭遇するケースも多くはありましたが、総合的に見るとuGUIと比べてより快適に開発できるようになった部分が多いと考えています。実際に他のUnityエンジニアのチームメンバーからもUI構築が楽になったという声は聞いています。

既存UIとの兼ね合いなどの問題で、すぐにUIを全てUI Toolkitに切り替えたり、新規開発は全部UI Toolkitにしたりということはなかなか難しいですが、メリットが大きく出そうな画面やUIでは積極的に採用していきたいと思っています。

また、「Webやネイティブ開発では宣言的UIが普及している中、今更Query操作でのUI開発…?」という感想もあるんですが、Unityで開発する物はゲームがメインで、シーケンスの複雑なUIエフェクトやアニメーションなどが使われるので宣言的UIはやや相性が悪く、今のUI Toolkitに着地したのかな、と考えています。(REALITYはかなり特殊で、普通のアプリをUnityで作っているので宣言的UIで記述したいなとは思いました。)

UIToolkitでの開発は梅雨〜夏の間にずっとやっていたんですが、この時期といえば、冒頭で紹介した毛蟹以外にもワタリガニも美味しいです。蒸して食べても美味しいですし、特に韓国料理のカンジャンケジャンなんかは絶品ですよね。

ワタリガニで作ったカンジャンケジャン

これからは秋ということで、上海蟹が食べたくなってくる季節ですね!

REALITYは、これからも継続的にUI体験の向上に努めてまいります。今後ともよろしくお願いいたします。