UIElementsの基本構造

UIElementsがいまいち使いにくい。もっと拡張性を上げられないものか…とおもってUIElementsのソースコードをいろいろ読み漁っていたら理解が深まってしまったのでメモとして残します。 読んでも「ふーん」と思うだけできっと役に立ちません。

VisualTreeAssetとは何なのか

var vitualTreeAsset = AssetDatabase.LoadAssetAtPath<VisualTreeAsset>("Assets/Editor/Nantoka.uxml");
var rootElement = vitualTreeAsset .CloneTree();

UIElementsを使う場合はいつも上記のようなコードを書くと思います。拡張子がuxmlなのでuxmlファイルの生テキストそのものを読んでいるような気持ちになりますが、これは厳密には違います。

XMLが記述されたテキストファイルであるところのuxmlファイルは専用のScriptedImporterによって前処理され、ScriptableObjectに変換されます。このScriptableObjectがVisualTreeAssetです。つまりVisualTreeAssetはXMLのパースが完了した結果生まれたバイナリです。

VisualTreeAssetに含まれる要素はいくつかの種類に分類されます。

  • VisualElementAsset
  • inlineStyleSheet
  • UsingEntry
  • TemplateAsset

などです。順にみていきましょう。

VisualElementAsset

VisualElementAssetは以下のような代物です

[Serializable]
class VisualElementAsset
{
    int Id;
    string 要素名;
    string フルネーム(解決済み名前空間付き要素名);
    int 親のId;
    (string attributeName, string attributeValue)[] 要素がもつ全属性;
    string 要素内のテキスト;
}

VisualTreeAssetにはこれがListで入っています。 この時点では、まだButtonやImageなどの具体的なVIsualElementに解決はされていません。解決のための文字列情報をもっている状態です。

真の姿をみたければ公式のソースを。Propertyまわりがちょっとウッとなります。 https://github.com/Unity-Technologies/UnityCsReference/blob/master/Modules/UIElements/UXML/VisualElementAsset.cs

inlineStyleSheet

UXMLのビジュアル要素には以下のようにstyle属性を書けますが、これを抽出してかき集めたものです。

<engine:Button name="btn" text="Click!" style="height: 40px" />

uxmlファイルと同様にussファイルもScriptedImporterを経由してStyleSheetというScriptableObjectになります。inlineStyleSheetもソースがussファイルじゃなくなっただけでできるものは同じでStyleSheetを作ってVisualTreeAssetのSubAssetとして保持されます。

UsingEntry

UsingEntryは外部のテンプレートへの参照です。

<engine:Template path="Assets/Portrait.uxml" name="Portrait" />

上記のように書けばUXMLでは外部テンプレート(UXML)を参照して名前をつけることができますが、この要素それ自体は何もVisualElementを作りませんのでVisualElementAssetとして保持せずUsingEntryという構造体のListで持っています。

[Serializable]
struct UsingEntry
{
    public string alias;
    public string path;
    public VisualTreeAsset asset;
}

実際に外部Assetへの参照になっているのでAssetBundleの依存解決なんかも上手くいきそうな気がします。というかこうしないと文字列だけではUnityの参照解決の仕組みに乗っかれないので不安というか。

TemplateAsset

TemplateAssetは、UsingEntryで保持した外部テンプレートを使うための仕組みです。

<engine:Template path="Assets/Portrait.uxml" name="Portrait" />

<engine:Instance template="Portrait" />
<engine:Instance template="Portrait" />

上記のようにInstance要素をかけばTemplateを実体化できるわけですが、これは単純なVisualElementとは異なる挙動をする必要がるのでVisualElementAssetだけでは保持されません。TemplateContainerというVisualElementとTemplateAssetの合わせ技で処理されます。

その他のVisualTreeAsset内の要素

Slotsなどあるんですが、まだ私がちゃんと理解していないのでこの記事では触れません。
気になる方は公式のソースを読むなりマニュアルからあたりをつけるなりで平に御容赦を。

IUxmlFactory

IUxmlFactoryはVisualElementAssetをLabelやButtonなどの具体的なVisualElementに変換するための仕組みです。

public interface IUxmlFactory
{
    string uxmlName { get; }
    string uxmlNamespace { get; }
    string uxmlQualifiedName { get; }
    bool canHaveAnyAttribute { get; }
    IEnumerable<UxmlAttributeDescription> uxmlAttributesDescription { get; }
    IEnumerable<UxmlChildElementDescription> uxmlChildElementsDescription { get; }
    string substituteForTypeName { get; }
    string substituteForTypeNamespace { get; }
    string substituteForTypeQualifiedName { get; }
    bool AcceptsAttributeBag(IUxmlAttributes bag, CreationContext cc);
    VisualElement Create(IUxmlAttributes bag, CreationContext cc);
}

メンバーは結構ありますが、もっとも大事なのは uxmlQualifiedName とCreateです。
VisualElementAsset内のフルネームとuxmlQualifiedNameが一致したIUxmlFactoryのCreateがVisualElementの生成に使われます。逆にいうとuxmlQualifiedNameが一致するIUxmlFactoryが存在しそのCreateが何らかのVisualElementを返すならば、UXMLにはどんなXML要素を書いても良いのです。これがUXMLの拡張モデルの中核になります。

個人的に胸熱なのが、どうも公式のソースを読む限りUnity2020.1から、IUxmlFactory.Createがnullを返すことが許されるようになる気配があります。これは、UXMLの中に非VisualElement要素を自由に追加できるようになることを意味します。2019.2現在ではUXMLに非VisualElementを混ぜるにはScriptedImporterで特別扱いするしかなく、それためまったくカスタム不可能な領域です。ここに手が入るのは大変好ましいと思います。ただし未来のことなので取り下げられても文句は言えないので鵜呑みにしないでもらえると助かります。

UxmlTraits

IUxmlFactoryは要素とのマッピングを行いますが、UxmlTraitsは属性とのマッピングを行います。VisalElementAsset内の属性のキーと値のペア群を元にしてIUxmlFactory内で生成されるVisualElementのプロパティをセットします。UxmlTraitsは基本的にはIUxmlFactory実装の内部メンバーとして仕事します。

XMLスキーマ生成

Unityのアセットメニューで「Update UIElements Schema」を選択すると、IUxmlFactoryとUxmlTraitsの情報をもとにXMLスキーマが自動生成されます。XMLスキーマはUXMLを書く時のコード補完に使用されますので、ユーザーが追加したカスタムUML要素に対してもコード補完が効くようになります。

なお<engine:Template />や<engine:Instance />にコード補完が効かないのもこの仕組みが理由です。この二つはScriptedImporterで特別扱いされることで機能しているので対応したIUxmlFactoryが存在しないのです。これは2020.1でCreateがnullを返すIUxmlFactoryが作成されることで改善されそうな気配です。

まとめ

UXMLはScriptedImporterによってVisualTreeAssetに変換され、VisualTreeAsset内の各VisualElementAsset要素は対応するIUxmlFactoryによってButtonやLabelなどのVIsualElementに変換されます。 カスタムのIUxmlFactoryを書くことでUXMLを拡張できます。

f:id:enrike3:20191013173439p:plain