スレート アーキテクチャ

スレート デザインの中心となるアイデア

概要

スレート デザインの中心となるアイデアを説明しています。説明の順番に特に意味はありません。 ここで述べられている内容は、剛健な構造体や大理論などではなく、 UI の手直しから得た経験から得た指針を集めたものです。

スレートを習得しながら、このページを時々読み返すと役に立つでしょう。

動機

当時利用できた市販の UI ソリューションを観察していて、スレートに対する動機を持ちました。 結果として

  • ウィジェットからの UI のビルドは、ほとんどのツールキットで簡単にできます。難しい点は

    • UI デザインとイタレーション

    • フローデータの制御: ウィジェット (表示) と基本データ (モデル) 間の結合を考えるのが一般的です。

    • UI の説明のための外国語の習得。

  • IMGUI : 即時モード グラフィック ユーザ-インターフェース

    • メリット:

      • プログラマーは、データの取得が容易なコードに近い UI 描画を好む。

      • 無効状態は普通は問題にならない。直接データをポーリングすればよい。

      • 手順に沿ったインターフェースのビルドが簡単。

    • デメリット:

      • アニメーションとスタイリングの追加が難しい。

      • UI 描画は命令型コードなのでデータドリブンは不可能。

  • 望ましいスレートの特徴:

    • モデルのコードおよびデータへのアクセスが簡単。

    • プロシージャ UI 生成に対応。

      • UI 描画で失敗しないこと。

    • アニメーションとスタイリングに対応。

核となる信条

できる限りデベロッパーに効率的となるようにします。プログラマーの時間は貴重です。高速で負荷が小さい CPU を使用します。

  • 不透明なキャッシュとステートの重複を避けてください。UI はステートをキャッシュし、明示的な無効状態を要求します。スレートは以下のアプローチを使用します (好ましい順)。

    1. ポーリング

    2. 透明性があるキャッシュ

    3. 低粒度の無効状態の不透明なキャッシュ

  • UI 構造が変更された場合、ポーリングによる通知が望ましいです。(通知が必要な場合、通知は高粒度よりも低粒度が望ましいです。)

  • フィードバックループは避けてください。例: レイアウトは全てプログラマー設定から算出されますので、前のレイアウト ステートを絶対に信頼しないでください。

    • 唯一の例外は、 UI ステートがモデルになる時、例えば ScrollBars が UI ステートを視覚化する場合などです。

    • これはパフォーマンスよりもむしろ正確性とプログラマーの健全性のために行います。

    • ちょっとした多くの作業が必要な面倒な場当たり的な UI の計画を立てます。ユースケースが固まったら、これらを適切なシステムに統合します。

データフローとデリゲートのポーリング

UI はモデルを視覚化し操作します。スレートは、モデルデータの読み書きに必要なウィジェットの柔軟なコンジットとしてデリゲートを使用します。表示する必要がある場合、スレート ウィジェットはモデルのデータを読み取ります。 ユーザーが何かアクションを実行する場合、スレート ウィジェットはデータを修正するために書き出し用デリゲートを呼び出します。

テキストを表示するスレート ウィジェットの STextBlock を考えます。 STextBlock には、表示するテキストの取得場所の指示が必要です。 データは静的に設定することが可能です。ただし、これをもっと柔軟に行うのがデリゲートです (ユーザー指定の関数)。 STextBlock はこの目的で Text という名前のデリゲートを使用します。

Diagram showing relationship between float data and STextBlock

STextBlock は文字列としてフレームレートを読み取ります。

framerate はたいていの場合 float あるいは integer として格納されますが、上記の例では Text は文字列であると考えてみてください。デリゲートの使用により、値を読み取る場合の変換を柔軟に行うことができます。 これにより、下記の「パフォーマンスへの配慮」セクションで取り扱っているような、パフォーマンスへの配慮事項をすぐに思い出せます。

SEditableText は入力と出力の両方に対応するスレート ウィジェットです。STextBlock と同様、 Text デリゲートを使ってデータを視覚化します。 ユーザーが編集可能なテキストボックスにテキストを入れて [Enter] を押すと、 SEditableText が OnTextChanged デリゲートを呼び出します。 入力を有効にし、モデルのデータを OnTextChanged に変化させる正しい機能をプログラマーが適用していることが前提です。

Diagram showing two-way relationship between SEditableText and text data

SEditable テキストは item name を読み取ります。ユーザが [Enter] を押すと、新規テキストが OnTextChanged へ送られ、そこで確認され、適切であれば item name に割り当てられます。

次のフレームの間で、SEditableText はモデルデータから読み取ります。上記の例では、item name は OnTextChanged デリゲートで変化して、 Text デリゲートにより読み取られて視覚化されます。

属性と引数

デリゲートの使用が常に好ましいというわけではありません。ユースケースによっては、スレート ウィジェットへの引数は、定数値あるいは関数でなければならない場合があります。 この概念を TAttribute< T > クラスを使ってカプセル化します。属性は定数あるいはデリゲートに設定することができます。

パフォーマンスへの配慮

データフローとデリゲートのポーリング」セクションを読むと、パフォーマンスに関して重く配慮するようになると思います。

以下の所見を考えてみましょう。

  • UI の複雑度はライブ ウィジェットの数の制約を受けます。

  • 可能な限り、スクロール中のコンテンツを可視化します。これによりライブ ウィジェットがオフスクリーンになるのをほとんどの場合に避けることができます。

    • オフスクリーンのウィジェットが多いと、スレート パフォーマンスが失敗しやすくなります。

  • 前提:画面が大きい場合は、その画面および大量のウィジェットを操作できる剛健なマシンであること。

無効化 vs ポーリング

ポーリングは、効率がよくないか、機能的に正しくないかのいずれかです。簡単に小さな値の組み合わせとして表現できない場合などです。 通常は、モデルの構造体が大幅に変更された場合に無効状態にします。既存の UI をスクラップして再度作成するのが合理的です。 ただし、これによりステートが失われることが想定されるので、必要でない場合は行いません。

原則的に無効状態は、頻度が低く、粒度も低いイベント用に維持されます。

グラフ上にノードが表示されるブループリント エディタの例を考えて見ましょう。 更新が必要になると、すべての Graph Panel ウィジェットはクリアされて、再度作成されます。 簡単で管理しやすいので、粒度の高い無効状態に望ましいです。

子スロット

すべてのスレート ウィジェットは、子スロットに子を格納しています。(プレーンな配列の子ウィジェットを格納するのと反対です。) 子スロットは常に有効なウィジェットを格納しています。デフォルトで格納されているのは SNullWidget で、 このウィジェットには視覚化や相互処理がありません。 ウィジェットのタイプは子スロット独自のタイプを宣言するので、特殊なニーズに応えることができます。 SUniformGridPanel とはかなり異なり、SVerticalSlotSCanvas とは完全に異なる方法で子を調整すると考えましょう。 スロットで、子供の調整に影響する子供ごとの設定を各種パネルで要求できます。

ウィジェットのロール

ウィジェットには 3 種類あります。

  • Leaf Widgets - 子スロットのないウィジェットです。例えば、 STextBlock はテキストの構成要素を表示します。テキストの描画方法に関する固有の情報を持っています。

  • Panels - 動的な数の子スロットを持つウィジェットです。例えば SVerticalBox はレイアウト ルールに従って幾つでも子供を垂直に調整します。

  • Compound Widgets - 明確な名前のついた子スロットを固定数もつウィジェットです。例えば SButton にはボタン内部にウィジェットを含む「コンテンツ」という名前のスロットが 1 つあります。

レイアウト

スレート レイアウトは 2 つのパスで実行します。パスは最適化のために 2 つに分けているので、残念ながら透明性はありません。

  1. Pass 1:Cache Desired Size - 関係する関数は SWidget::CacheDesiredSizeSWidget::ComputeDesiredSize です。

  2. Pass 2:ArrangeChildren - 関係する関数は SWidget::ArrangeChildren です。

それぞれの詳細です。

Pass 1:Cache Desired Size

このパスの目的は、各ウィジェットが占有したい空間を計算することです。 子供を持たないウィジェット (リーフ ウィジェットなど) は固有のプロパティに基いて希望するサイズの計算およびキャッシュするように命令されます。 他のウィジェットと一緒になっているウィジェット (ウィジェットとパネル) は特殊なロジックを使って、それらの子供のサイズの関数として希望するサイズを決定します。 ウィジェットのそれぞれのタイプは ComputeDesiredSize(); の実行のみを要求され、キャッシュとトラバース ロジックはスレートが実行します。 スレートは、 ComputeDesiredSize() がウィジェット上に呼び出された時に、その子供が希望するサイズに既に計算されキャッシュされていることを保証します。 つまり、これはボトムアップのパスです。

次の例で、テキストの構成要素と画像の 2 つの子供を調整する水平のボックスを検討します。

Diagram showing a horizontal box that arranges two children

平行ボックスは、テキスト ブロックと画像という 2 つの子供を配置します。

STextBlock ウィジェットは、表示されている文字列を計算して望ましいサイズを出します。SImage ウィジェットは 表示中の画像データに基いてサイズを判断します。テキストブロック内のテキストは 14 スレート ユニット、 画像は 8 ユニットの空間がそれぞれ必要だと過程します。平行パネルはウィジェットを平行に配置するので、 14 + 8 = 22 ユニットの空間が必要となります。

Pass 2:ArrangeChildren

ArrangeChildren はトップダウンのパスです。スレートは上位のウィンドウで始まり、プログラマーが提供したコンストレイントに基いて 子供を配置するよう各ウィンドウに要求します。各子供に割り当てる空間が分かると、スレートが再度起こり、その子供の子供を配置することができます。 全ての子供が配置されるまで繰り返されます。

Diagram showing a horizontal panel arranging two children

平行パネルは、テキスト ブロックと画像の 2 つの子を配置します。

上の例で、パネルには親から 25 ユニット割り当てられています。 最初のスロットでは、子に希望するサイズを使いたいので 14 ユニットの空間が割り当てられたことを示します。 2 つ目のスロットでは、利用できるすべての幅を使いたいので、残りの空間 (11 ユニットの空間) が割り当てられたことを示します。 実際の SHorizontalBox ウィジェットでは、スロット内の SImage のアラインメントは、 左、中央、右、両端揃えの HAlign プロパティで操作されます。

実際にスレートが ArrangeChildren パスをまるごと実行することはありません。そうではなくて、この機能は他の機能を実行するために使用されます。 主要なレイは探知とペイントです。

スレートの描画OnPaint

ペイントパスの間、スレートは表示されているすべてのウィジェット上でイタレートし、レンダリング システムで消費されることになるドロー エレメントのリストを生成します。 このリストはフレームごとに新規に作成されます。

上位のウィンドウで開始し、各ウィジェットのドロー エレメントをドローリストに加えながら、下の階層で繰り返されます。 ウィジェットはペイント中に 2 つのことを行う傾向があります。 それは、実際のドロー エレメントの出力あるいは子ウィジェットが存在すべき場所を計算し、子ウィジェットにそれ自体をペイントするように要求することです。従って、単純化された一般的なケースの OnPaint 関数は以下のように考えることができます。

    // An arranged child is a widget and its allotted geometry (配置された子は、ウィジェットとその配置されたジオメトリです)
    struct ArrangedChild
    {
        Widget;
        Geometry;
    };

    OutputElements OnPaint( AllottedGeometry )
    {
        // 割り当てられたジオメトリを指定してすべての子を配置します
        Array<ArrangedChild> ArrangedChildren = ArrangeChildrenGiven( AllottedGeometry );

        // Paint the children for each
        (子供をペイントします)
        {
            OutputElements.Append( Child.Widget.OnPaint( Child.Geometry ) );
        }

        // Paint a border (境界をペイントします)
        OutputElements.Append( DrawBorder() );
    }

SWidget のアナトミー

スレート内の Swidget の動作を定義する主要な関数は、以下の通りです。

  • ComputeDesiredSize() - 希望するサイズにします。

  • ArrangeChildren() - 親が割り当てた領域内に子供を配置します。

  • OnPaint() - 表示をします。

  • Event handlers - OnSomething 形式です。これらの関数は、様々な場合にスレートによってウィジェット上に呼び出すことができます。

構成

構成とは、スロットが任意のウィジェット コンテンツを含むことができるという概念です。 これにより、スレートを扱う柔軟性が大幅に広がります。構成は、中心となるスレート ウィジェット内で 出来る限り使用します。

ウィジェットのラベルに文字列引数を使用してみようかと考えたことがあるなら、SWidget の使用を検討してもいいかもしれません。

特定のタイプの子供をウィジェットが含まなければならない特殊なケースでは、 構成の要件は適用されません。これらは決してスレートのコアでウィジェットにはならず、 むしろドメイン以外での再利用は意図されていないドメイン固有のウィジェットです。

宣言記法

スレートへのアクセスはコードから直接行えることが望ましいです。 経験上 UI 記述用の宣言記法が必要なことは明らかですが、 C++ 関数との結合を確認するコンパイル時であってほしいとも思っていました。

その解決策が、宣言的な UI 記述言語を C++ のサブセットとしてビルドすることでした。

コードベースにサンプルが沢山あります。

タグ
Unreal Engine のドキュメントを改善するために協力をお願いします!どのような改善を望んでいるかご意見をお聞かせください。
調査に参加する
キャンセル