UI Toolkit のバインディングを試す - その1 まずはやってみる編
はじめに
こんにちは。
Unity 6 をはじめて起動したときに UI Builder
がババーンとドヤ感満載で前面に開いた状態で起動してきました。これは長らく Editor 専用と言われてきた UI Toolkit をそろそろ Runtime でも使ってねという Unity さんの熱い想いなんでしょうか?
そんな事を想いながら試しに UI Toolkit を触ってみたら、データバインディングが驚くほど楽 に使えて感動しました。
ってなわけで今回は UI Toolkit のデータバインディングについて書きたいと思います。
ちなみに私自身は MVVM やら WPF などに関する知識はほとんどなく素人同然です。分からないなりに「やってみた」的なノリで書いていきますので間違いなどあるかもしれませんがよろしくお付き合いください。
※ この記事は Unity 6 (6000.0.22f1) の内容を記載しています。
※ 現在 Unity 6 は Preview 版なので今後変更が入る可能性があります。
テスト用の UI を作成
今回はバインディングについての解説なので UI Toolkit の基本的なところについては詳しく書きません。そのうち別記事でまとめるかもしれませんが、そのあたりはネット上で詳しく解説してくださっている方々もいらっしゃるので情報には困らないと思います。
簡単に今回テストで使う UI の構築手順についてを書きます。
Visual Tree Asset ( uxml ) を作成
まず Project ウィンドウで適当なフォルダに Create -> UI Toolkit -> UI Document
で Visual Tree Asset ( .uxml ファイル ) を作ります。ここでは BindTestUI.uxml
という名前にしました。
作成した uxml ファイルをダブルクリックすると UI Builder が開きます。この Builder を使ってボタンやラベルを配置していきます。
ちなみに UI Builder を使わずに uxml ファイルのソースを手書きしても良いです。
UI Builder で Label や Button を作成
バインディングのテスト用に int と string を表示する Label を 1 つずつ、それと表示更新するための Button をそれぞれに配置しました。
このラベルにプロパティをバインドしておいて、ボタンを押したらプロパティの更新、するとバインディングされたラベルが勝手に更新される、という想定です。
ボタンには C# で click 時の動作を仕込むため BtnSetInt
などと適当な名前を付けておきます。名前を付けると Hierarchy 上では # 記号がついて青色表示になります。
またデータソースを設定するための親ノードとして BindTestArea という名前を付けた VisualElement を追加します。(画像の緑矢印のところ)
ラベルについては名前を付けていません。(画像のオレンジ矢印のところ)
もちろん名前を付けていても良いですが、バインディングだけで使う場合は C# スクリプトからこのラベルを触る事がないので名前を付ける必要はありません。
UI Document の GameObject を作成
uxml ファイルが出来たので、これを UI としてシーンに表示するための GameObject を作成します。 Hierarchy の右クリックメニュー -> UI Toolkit -> UI Document
で UI Document コンポーネントがついた GameObject が生成されます。
UI Document コンポーネントの Source Asset
に作成した uxml ファイルをセットします。
Panel Settings には最初から用意してあるものが自動でセットされていると思いますので今回はこれをそのまま使います。もし SceleMode などを変更する場合は PanelSettings を新規作成してここにセットします。
この状態でゲームを再生すれば Game ビューに UI が表示されます。
今回はテストなのでシーンに直接 UI を配置しちゃっていますがこれだと取り回しも悪く Unity の作法的にも非推奨なので、実際にはこの GameObject を Prefab にしてロードする等してつかうようにします。
バインディングするデータクラスを作成
UI が出来たので今度はバインディングするプロパティを持つクラスを作成します。
Unity マニュアル によるとバインディングに使えるのは下記の型のようです。
- ユーザー定義の ScriptableObject クラス
- ユーザー定義の MonoBehaviour クラス
- ネイティブ Unity コンポーネントの種類
- ネイティブ Unity アセットタイプ
- プリミティブ C# の型 ( int, bool, float ..
- ネイティブの Unity 型 ( Vector3, Color, Object ..
一昔前のマニュアルには「SerializedObject データ バインディングはエディター限定」と注意書きがありましたが Unity 6 のマニュアルにはその記載がなくなっています。
私は Unity 2022 以前でバインディングを試した事がないのでよく知りませんが、以前はランタイム UI では動作しなかったのかもしれません。
まずは単純なフィールドでテスト
まずは下記のようなとても簡単なデータクラスを用意しました。
public class BindTestData
{
public int IntValue = 10;
public string StringValue = "初期値";
}
簡単すぎるクラスですね。こんなに簡単で本当にちゃんとバインディングされるんでしょうか?
UI とデータクラスをバインディング
ではいよいよバインディングを設定してみましょう。
バインディングの設定も UI Builder から行います。
( もちろん UI Builder を使わずに uxml に直接書いてもできます。 )
親ノードに Data Type をセット
BindTestArea
と名付けた VisualElement の Inspector で Data Source
に Type を選択して Data Source Type
の Sekect Type をクリックします。ダイアログが出るので先ほど作成した BindTestData クラスを検索して設定します。
こうする事でこの VisualElement 配下のノードすべてに同じ Data Source Type をセットした状態にできます。
ラベルにフィールド名をバインディング
次にラベルとフィールドをバインディングします。
int 型を表示したい名無しのラベルを選択し、Text プロパティの左にある⋮ をクリックして Add binding..
を選択します。
Add binding
ダイアログが出てきます。ここでは既に先ほど親ノードに設定した Data Source Type がセットされています。
そのまま Data Source Path
をクリックするとフィールドが入力候補に出てくるので IntValue
のプロパティ名を選択します。
Add Binding
を押すとバインディングが設定できます。
きちんと設定できていれば Text プロパティの左に紫のデータアイコンが表示されます。
データクラスと UI を接続するクラスを作成
ここまで出来ればあとは UI Document とデータクラスをつなぐだけです。
今回は簡単に MonoBehaviour で用意して UI Document と一緒の GameObject にくっつけます。もちろん UI とは別の場所に置いても良いですし、MonoBehaviour でなくても大丈夫です。
using System;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.UIElements;
public class BindTestUI : MonoBehaviour
{
private BindTestData mTestData = new();
private void Start()
{
// 親 Node の dataSource に データクラスをセット
var rootElement = GetComponent< UIDocument >().rootVisualElement;
var bindArea = rootElement.Q<VisualElement>("BindTestArea");
bindArea.dataSource = mTestData;
// ボタンにイベントをセット
bindArea.Q<Button>( "BtnSetInt" ).clicked += OnClickedBtnSetInt;
bindArea.Q<Button>( "BtnSetString" ).clicked += OnClickedBtnSetString;
}
Q<VisualElement>("BindTestArea");
でデータ型を設定した親ノードクラスを探し、その dataSource
プロパティにデータクラスのインスタンスをセットします。バインドの操作はこれで完了です。配下にあるラベルにはフィールド名が設定してあるので、親にデータを渡すだけでつながってくれます。
あとはテスト用にボタンを押したときにフィールドを更新するメソッドをつなぎます。
private void OnClickedBtnSetInt()
{
mTestData.IntValue++;
}
private void OnClickedBtnSetString()
{
mTestData.StringValue = GetStringValue();
}
private int mStringIdx = -1;
private List<string> mTexts = new(){ "さくらんぼ", "いちご", "ぶどう", "デコポン", "かき", "りんご", "なし", "もも", "パイナップル", "メロン", "スイカ" };
private string GetStringValue()
{
if( mTexts.Count <= ++mStringIdx ){ mStringIdx = 0; }
return mTexts[ mStringIdx ];
}
}
テスト用の適当な処理で int は押すたび ++ する感じで、String は某有名スイカ・・ですね。はい。
ランタイムでバインディングする場合
ちょっと脱線しますが、今回のように uxml 上には直接バインディングを書かず実行中に C# でバインディングする場合は dataSourcePath
プロパティを使って下記のようなコードでバインドする事が出来ます。
var label = rootElement.Q<VisualElement>("MyLabelName");
label.dataSourcePath = PropertyPath.FromName(nameof(BindTestData.IntValue));
当然ながらこの場合は対象のラベル等を Q で検索できるように名前を付けておく必要があります。
実際に動かしてみる
これで必要な実装は終わったので実行してみましょう。
ちゃんと動きました!
再生ボタンを押した直後もきちんとデータクラスの初期値が反映されています。
またボタンを押すたびに Field 更新に合わせて Label の表示が変わっています。
すごい簡単!
すごい簡単!!・・・でも?
すごい簡単で便利なデータバインディング・・なんですが、実はこの実装だとちょっと問題があります。
勘の良い方ならお気づきだと思いますが、この実装だと UI からフィールドの値を取得する処理が毎フレーム走ってしまう 事になります。フィールドが更新されてもいないのに不必要な処理が常に動く状態なので、表示項目が多くなるほど無視できない負荷になりそうな予感がしますね。
せっかく簡単な方法でバインディングが出来たのに、これだと負荷が心配でなかなか使いにくいですね。
というわけで、次回はこのあたりを少し掘り下げてこの負荷を下げる方法を探してみましょう。
Discussion