Version: Unity 6.0 (6000.0)
言語 : 日本語
C# スクリプトでのランタイムバインディングの作成
バインディングモードと更新トリガーの定義

ランタイムバインディングのデータソースの定義

バインディングオブジェクトを作成するときは、データソースを定義する必要があります。データソースは、バインドするプロパティを含むオブジェクトです。任意の C# オブジェクトをランタイムバインディングのデータソースとして使用できます。

バインディングシステムがデータソースにアクセスできるようにするには、バインディングオブジェクトの dataSource プロパティをデータソースオブジェクトに定義する必要があります。例えば、以下のようなデータソースオブジェクトと UI 要素があるとします。

using UnityEngine;
using UnityEngine.UIElements;
using Unity.Properties;

public class DataSource
{
    public Vector3 vector3 { get; set; } 
}

var element = new VisualElement();

次に、データソースオブジェクトに element.dataSource プロパティを以下のように定義することができます。

element.dataSource = new DataSource();

これにより、要素に適用されたバインディングが DataSource オブジェクトにアクセスできるようになります。

要素に適用されたバインディングが DataSource オブジェクトの vector3 フィールドにアクセスできるようにするには、以下を追加します。

element.dataSourcePath = PropertyPath.FromName(nameof(DataSource.vector3));

子要素に適用されたバインディングが DataSource オブジェクトの vector3 フィールドにアクセスできるようにするには、以下を追加します。

var child = new VisualElement();
child.dataSourcePath = PropertyPath.FromName(nameof(DataSource.vector3));
element.Add(child)

プロパティバッグ

UI Toolkit は Unity.Properties モジュールを使用して、2 つのオブジェクト間でデータをバインドするための プロパティバッグ を作成します。使用可能な C# 型の情報に基づいてプロパティバッグを生成します。ただし、特定のビルトイン Unity 型では、生成されたプロパティバッグに期待されるプロパティが含まれない場合があります。これは、これらの型に必要な属性がない場合に発生します。例えば、Rect 型のように、[SerializeField] 属性を持たない public プロパティと private フィールドがある場合や、ランタイムに決定できないネイティブ側のフィールドを定義する場合などです。

ノート: データソースとして値型を使用する場合、VisualElement.dataSource がオブジェクトプロパティとして定義されるため、ボックス化のコストが発生します。つまり、dataSource プロパティに割り当てる前に値型をボックス化する必要があります。ボックス化操作では、メモリの割り当てとコピーのオーバーヘッドが発生し、結果としてパフォーマンスコストが発生します。このパフォーマンスへの影響は、小規模なデータセットや不定期な使用ではさほど大きくない可能性があります。ただし、パフォーマンスが重要なシナリオや大量のデータを扱う場合は、ボックス化のコストが問題になる可能性があります。

ランタイムバインディングと、オーサリングまたはシリアライズの目的でデータソースを定義するには、以下に示す一般的なパターンを使用します。

using UnityEngine;
using Unity.Properties;

public class MyBehaviour : MonoBehaviour
{
    // Serializations go through the field. 
    [SerializeField, DontCreateProperty] 
    private int m_Value;
    
    // Bindings go through the property rather than the field. 
    // This allows you to do validation, notify changes, and more.
    [CreateProperty] 
    public int value
    {
        get => m_Value;
        set => m_Value = value;
    }
    
    // This is a similar example, but for an auto-property.
    [field: SerializeField, DontCreateProperty]
    [CreateProperty]
    public float floatValue { get; set; }
}

ノート: これらのバインド可能なプロパティは、本質的にポリモーフィック特性を持っています。

バージョン管理と変更追跡の統合

パフォーマンスを向上させるために、バージョン管理と変更追跡をバインディングデータソースに統合することができます。デフォルトでは、バインディングシステムはデータソースを継続的にポーリングし、変更のたびに UI を更新します。最後の更新以降に実際に何かが変更されたかどうかはわかりません。この方法は単純なプロジェクトには便利ですが、多数のバインディングを扱う場合は効率的にスケールできません。

ソースのバージョン管理と変更追跡は、意図的にアクティブ化する必要がある任意の機能です。デフォルトでは、アクティブバインディングオブジェクトはフレームごとに更新されるため、高いリソース負荷がかかる場合があります。処理のオーバーヘッドを最小化するために、ソースに関連付けられたバインディングを更新するタイミングをバインディングシステムに指示するために実装できるインターフェースが 2 つあります。

  • IDataSourceViewHashProvider インターフェースには、ソースにリンクされたすべてのバインディングを更新するタイミングを示すビューハッシュコードがあります。
  • INotifyBindablePropertyChanged インターフェースでは、プロパティごとの変更通知を有効にして、変更されたプロパティに関連する個々のバインディングに対してのみ更新をトリガーできます。

これらのインターフェースは、個別に実装することも、一緒に実装してコントロールを強化することもできます。

ノート: 現在、いずれかのインターフェースを実装する型は、アセンブリに [assembly: Unity.Properties.GeneratePropertyBagsForAssembly] というタグが付けられると、自動的にコード生成にオプトインします。ただし、この動作は変更される可能性があります。

IDataSourceViewHashProvider の実装

特定のソースにビューハッシュコードを提供するには、IDataSourceViewHashProvider インターフェースを実装します。このインターフェースを使用すると、バインディングシステムは、ソースが最後の更新から変更されていない場合に、特定のバインディングオブジェクトの更新をスキップできます。

以下の例では、変更を即座にレポートするデータソースを作成します。

using UnityEngine.UIElements;

public class DataSource : IDataSourceViewHashProvider
{
    public int intValue;
    public float floatValue;

    // Determines if the data source has changed. If the hash code is different, then the data source
    // has changed and the bindings are updated.
    public long  GetViewHashCode()
    {
        return HashCode.Combine(intValue, floatValue);
    }
}

IDataSourceViewHashProvider インターフェースは変更のバッファも行います。このバッファリング機能は、データが頻繁に変更されるものの、UI にすべての変更を即座に反映する必要がない場合に特に役立ちます。

変更をバッファするには、IDataSourceViewHashProvider インターフェースを実装し、データソースが変更されたことをバインディングシステムに通知するときに CommitChanges メソッドを呼び出します。

デフォルトでは、バインディングシステムは、データソースのバージョンが変更されない場合にバインディングオブジェクトを更新しません。ただし、MarkDirty メソッドを呼び出したり updateTriggerBindingUpdateTrigger.EveryFrame に設定したりして、バージョンが変更されていなくてもバインディングオブジェクトは更新される可能性があります。IDataSourceViewHashProvider を使用して変更をバッファリングする場合は、リストの項目の追加や削除、サブフィールドやサブプロパティの型の変更など、ソースの構造的な変更を避けてください。

以下の例では、変更をバッファするデータソースを作成します。

using UnityEngine.UIElements;

public class DataSource : IDataSourceViewHashProvider
{
    private long m_Version;

    public int intValue;
    
    public void CommitChanges()
    {
        ++m_Version;
    }
    
    // Required by IDataSourceViewHashProvider
    public long  GetViewHashCode()
    {
        return m_Version;
    }
}

INotifyBindablePropertyChanged の実装

特定のプロパティ変更についてバインディングシステムに通知するには、INotifyBindablePropertyChanged インターフェースを実装します。このインターフェースを実装すると、プロパティパスで変更が検出された場合、バインディングシステムは関連するバインディングのみを更新します。例えば、MyAwesomeObject プロパティの変更が通知されると、バインディングシステムは、MyAwesomeObject プレフィックスを持つデータソースパスに関連付けられているすべてのバインディングを更新します。ソースに結び付けられたその他のバインディングオブジェクトは影響を受けません。

このアプローチでは、バインディングシステムが実行する処理が最小限で済むため、UI の更新が非常に効率的になります。

以下の例では、プロパティごとに変更を通知するデータソースを作成します。

using System.Runtime.CompilerServices;
using Unity.Properties;
using UnityEngine.UIElements;

public class DataSource : INotifyBindablePropertyChanged
{
    private int m_Value;
    
    // Required by INotifyBindablePropertyChanged
    public event EventHandler<BindablePropertyChangedEventArgs> propertyChanged;

    [CreateProperty]
    public int value
    {
        get => m_Value;
        set
        {
            if (m_Value == value)
                return;

            m_Value = value;
            Notify();
        }
    }

    void Notify([CallerMemberName] string property = "")
    {
        propertyChanged?.Invoke(this, new BindablePropertyChangedEventArgs(property));
    }
}

ノート: INotifyBindablePropertyChanged インターフェースを実装する場合、バインディングシステムは変更の通知時にチェックを実行しません。変更をレポートしないということは、バインディングシステムはそのプロパティに関連するバインディングを更新しないということです。したがって、変更は必要な場合にのみレポートするようにしてください。

IDataSourceViewHashProviderINotifyBindablePropertyChanged の実装

最適なバインディングパフォーマンスを実現するには、IDataSourceViewHashProvider インターフェースと INotifyBindablePropertyChanged インターフェースの両方を実装します。バインディングシステムは、ビューのハッシュコードが変更されるまでプロパティの変更を追跡します。その時点で、変更されたプロパティに関連付けられた影響を受けるバインディングのみを効率的に更新します。

これには追加の定型コードが必要ですが、柔軟性とパフォーマンスを最大限に高めることができます。

以下の例では、両方のインターフェースを実装するデータソースを作成します。データソースは、変更が発生するとバインディングシステムに通知します。ただし、バインディングは即座に更新されるのではなく、Publish() メソッドが呼び出されるまで更新が保留されます。このアプローチは、毎フレーム UI を更新するとパフォーマンスコストが発生する、非常に変動しやすいデータを扱う場合に特に役立ちます。

using System;
using System.Runtime.CompilerServices;
using Unity.Properties;
using UnityEngine.UIElements;

public class DataSource : IDataSourceViewHashProvider, INotifyBindablePropertyChanged
{
    private long m_ViewVersion;
    private int m_Value;
    private int m_OtherValue;
    public event EventHandler<BindablePropertyChangedEventArgs> propertyChanged;
    [CreateProperty]
    public int value
    {
        get => m_Value;
        set
        {
            if (m_Value == value)
                return;
            m_Value = value;
            Notify();
        }
    }
    [CreateProperty]
    public int otherValue
    {
        get => m_OtherValue;
        set
        {
            if (m_OtherValue == value)
                return;
            m_OtherValue = value;
            Notify();
        }
    }
    public void Publish()
    {
        ++m_ViewVersion;
    }
    public long GetViewHashCode()
    {
        return m_ViewVersion;
    }
    void Notify([CallerMemberName] string property = "")
    {
        propertyChanged?.Invoke(this, new BindablePropertyChangedEventArgs(property));
    }
}

推奨ガイド

パフォーマンスを最適化するには、以下のヒントとベストプラクティスに従ってください。

  • バインド可能なプロパティには C# プロパティを使用する: バインド可能なプロパティを定義する場合は、フィールドの代わりに C# プロパティを使用します。これにより、検証、通知、またはカスタム動作を柔軟に組み込むことができ、より堅牢で保守性の高いコードを実現できます。

  • C# プロパティでの大規模な計算を避ける: プロパティに大量の処理が必要な場合は、必要な場合にのみ計算を実行し、後続のバインディングにはキャッシュされた値を使用します。

  • 不要な通知の回避: 値に実際の変更がない場合、変更の通知には注意が必要です。値が同じ場合は通知を送信する必要はありません。

  • バージョン管理と変更追跡の実装: データソースでバージョン管理を使用します。最適なパフォーマンスを得るには、バージョン管理と変更追跡の両方を使用します。

  • データと UI 間のバッファとしてのデータソースの使用: 可能な場合は常に、データを直接使用するのではなく、データと UI の間の仲介者としてデータソースを実装します。このアプローチにはいくつかの利点があります。

  • データフローをより適切にコントロールし、UI からの変更の追跡を容易にします。データの更新タイミングと方法を管理できます。

  • すべての UI データを 1 か所に一元化するため、データアクセスが合理化され、アプリケーション全体の複雑さが軽減されます。

  • 元データの明瞭さと効率性を維持するため、型に関する追加のインストルメンテーションが不要になり、データ整合性が確保されます。

既知の制限事項

以下のセクションでは、ランタイムバインディングのデータソースに関する既知の制限事項の概要を説明します。

静的な型

静的な型はデータソースとして使用できません。システムを機能させるには、型のインスタンスを作成する必要があります。

メソッド

型に対して生成されるプロパティバッグは、フィールドとプロパティのみを考慮します。したがって、メソッドやビルトインイベントにバインドすることはできません。

ただし、ActionFunc などのデリゲート型にバインドすることは可能です。デリゲートフィールドまたはプロパティにバインドするには、+= または -= の代わりに = 演算子を使用します。デリゲートを割り当てるのではなく追加または削除する必要がある場合は、カスタムバインディング型を実装する必要がある場合があります。

インターフェース

静的な型 のセクションで説明したように、データソースのオブジェクトインスタンスを作成する必要があります。バインディングシステムはインターフェースで動作しますが、[CreateProperty] のタグが付いたプロパティを持つインターフェースを実装する型には、自動的に生成されるバインド可能なプロパティがありません。各型に対して、フィールドとプロパティにそれぞれタグを付け、バインド可能にする必要があります。この制限は、今後のリリースで対処される予定です。

ビルトインコンポーネントおよびオブジェクト

C# のプロパティバッグ生成プロセスは、主にユーザー定義型で動作するように設計されています。その結果、現在、Unity のビルトインコンポーネントおよびオブジェクトのサポートは限られています。これは、ビルトイン型のフィールドがネイティブコードで定義されていること、エンジンによる明示的なシリアル化処理、[SerializeField] 属性がないことなど、さまざまな要因によるものです。ただし、ユーザー定義コンポーネントやスクリプタブルオブジェクトのフィールドやプロパティは期待通りに動作します。

この制限は、今後のリリースで対処される予定です。それまでは 2 つの回避策があります。

  • ビルトイン基本クラスのフィールドまたはプロパティを公開するには、独自のクラスに private プロパティを追加してバインディングシステムに公開します。
  • Transform などのビルトイン型のフィールドまたはプロパティを使用するには、必要なプロパティを公開するラッパー型を作成します。

追加リソース

C# スクリプトでのランタイムバインディングの作成
バインディングモードと更新トリガーの定義