UnityのUI Toolkitで、SeializedProperty
の値とUIをデータバインディングする方法についてまとめました。
Unity 2021.3.16f1
やりたいこと
いま、SerizliedObject
にシリアライズされているSerializedProperty
の値をUI ToolkitのUIに表示することを考えます。
従来のIMGUIベースの実装方法では、SerializedObject.FindProperty
でSerializedProperty
を取得して、自身でUIの表示を更新する必要がありました。
UI ToolkitではVisualElement
にSerializedProperty
のパスを事前に指定しておくことで、対象の値が変わったときに自動的に表示を更新する仕組みがあります。
本記事ではこのデータバインディングの仕組みについてまとめます。
SerializedObjectとバインドする
SerializedObject
とバインドするには以下を行います。
- 対象のUI要素の
bindingPath
にプロパティのパスを入力 - ルートの
VisualElement
のBind()
に対象のオブジェクトを渡す
以下は選択中のオブジェクトの名前を表示・変更するテキストフィールドを持つエディタウィンドウの実装例です。
using System; using UnityEditor; using UnityEditor.UIElements; using UnityEngine.UIElements; using Object = UnityEngine.Object; namespace BindExample { public sealed class Example : EditorWindow { private TextField _textField; [MenuItem("Window/Example")] public static void ShowWindow() { GetWindow<Example>(); } public void CreateGUI() { _textField = new TextField("Object Name") { // 対象のオブジェクトにシリアライズされているm_Nameとバインドする bindingPath = "m_Name" }; rootVisualElement.Add(_textField); Bind(Selection.activeObject); } private void OnSelectionChange() { Bind(Selection.activeObject); } private void Bind(Object obj) { if (obj != null) { var so = new SerializedObject(obj); rootVisualElement.Bind(so); // バインド // rootVisualElementじゃなくTextFieldに対してバインドしてもOK //_textField.Bind(so); } else { rootVisualElement.Unbind(); // バインド解除 _textField.value = ""; } } } }
これを実行すると下図のように、選択中のオブジェクトの名前がバインディングされてTextField
に表示されます。
またバインディングは双方向で行われるため、TextField
の値を変更するとオブジェクトの名前も変わります(変更可能なもののみ)。
SerializedPropertyとバインドする
以下のように、自身でSerializedProperty
を取得してIBindable.BindProperty()
に渡すことで、特定のUI要素と直接バインドすることもできます。
using UnityEditor; using UnityEditor.UIElements; using UnityEngine.UIElements; using Object = UnityEngine.Object; namespace BindExample { public sealed class Example : EditorWindow { private TextField _textField; [MenuItem("Window/Example")] public static void ShowWindow() { GetWindow<Example>(); } public void CreateGUI() { _textField = new TextField("Object Name"); // 自分でバインド処理をするのでbindingPathには何も入れない rootVisualElement.Add(_textField); Bind(Selection.activeObject); } private void OnSelectionChange() { Bind(Selection.activeObject); } private void Bind(Object obj) { if (obj != null) { var so = new SerializedObject(obj); // 自身でSerializedPropertyを取得 var nameProp = so.FindProperty("m_Name"); // BindPropertyでTextFieldとバインド _textField.BindProperty(nameProp); } else { rootVisualElement.Unbind(); _textField.value = ""; } } } }
UXMLにバインドパスを指定する
UXMLファイルを使ってレイアウトを組む場合には、バインドするためのパスをUXML側に記述することもできます。
これを行うには以下のように、TextField
のbinding-path
にパスを入力しておききます。
<UXML xmlns:ui="UnityEngine.UIElements"> <ui:VisualElement> <ui:TextField name="exampleText" label="Name" text="" binding-path="m_Name"/> </ui:VisualElement> </UXML>
エディタウィンドウは以下のように、VisualTreeAssetからレイアウトを生成するように修正しておきます。
using UnityEditor; using UnityEditor.UIElements; using UnityEngine; using UnityEngine.UIElements; namespace BindExample { public sealed class Example : EditorWindow { [SerializeField] private VisualTreeAsset visualTree; public void CreateGUI() { visualTree.CloneTree(rootVisualElement); Bind(Selection.activeObject); } private void OnSelectionChange() { Bind(Selection.activeObject); } [MenuItem("Window/Example")] public static void ShowWindow() { GetWindow<Example>(); } private void Bind(Object obj) { if (obj != null) { var so = new SerializedObject(obj); rootVisualElement.Bind(so); } else { rootVisualElement.Unbind(); var textField = rootVisualElement.Q<TextField>("exampleText"); textField.value = string.Empty; } } } }
あとはこのEditorWindowのスクリプトアセットのInspectorから、先ほど作ったUXMLファイルをアサインすれば完成です。
動作は変わらないので割愛します。