entry-header-eye-catch.html
entry-title-container.html

entry-header-author-info.html
Article by

uGUI記述ライブラリ『Mux』を公開します

こんにちは。VRoid モバイル/StudioのUIを開発したねこまんまです。

先日VRoid モバイル/Studioで利用しているuGUI記述ライブラリ『Mux』を公開しました🎉

github.com

実は以前、pixiv TECH SALONで少しだけ紹介させていただいたのですが、今回はこのライブラリの魅力をより掘り下げて見たいと思います🔍

uGUI?

uGUIとはUnityのアプリケーションで使うことができるUIツールキットです。他にもUnityには、新しく開発されているUI ToolkitUIWidgets、サードパーティーによるものなど様々なものがあります。uGUIはその中でも成熟しており、Unityに標準的に含まれることからよく利用されています。

uGUIの特徴は、Unityのヒエラルキーに則ってUIが構築されることです。そのため、Unity Editorを用いて編集することができます。また、UI以外の3Dのオブジェクトなどとの組み合わせも可能です。

f:id:pxvpxv:20210219115328g:plain
https://docs.unity3d.com/Packages/com.unity.ugui@1.0/manual/UIBasicLayout.html から引用

強さを活かして弱さを改める

uGUIはUnity Editorによる編集が容易な反面、ちょっと都合が悪いこともあります。

テキストを扱うツールとの親和性が悪い。

例えば、Gitのようなバージョン管理ツールで差分を解決するのは困難です。

Unity Editorによる記述とスクリプティングが分離している。

uGUIでUIを書くとき、Unity Editorを使ってUIを配置していくのですが、もちろん置いて終わりではなく、スクリプトを使ってこれを操作していきます。そのつながりはUnity Editor上からははっきり見えません。

MuxはuGUIの利点はそのままに、こういった点をXamarin.FormsのXAMLによる記述とデータバインディングでもっと便利にします✨

Xamarin.Formsとは?

Xamarin.Formsは、先程上げたような課題をデスクトップアプリ、モバイルアプリで解決するフレームワーク*1です。

Xamarin.Formsは.NETに基づいています。そう、Unityと同じです。同じクラスライブラリを使い、プログラムは同じILにコンパイルされています。それなら、uGUIとXamarin.Formsをくっつけちゃえばいいじゃん💡これがMuxです。

なお、Xamarin.Forms自体にあるクロスプラットフォームUIの機能は使わずに、Unityのヒエラルキーを表現するためだけに使われています。 もちろんUnityが動く環境ならどこでもMuxを使うことができ、そしてUnityはクロスプラットフォームです。

XAML📃

「XAMLはXMLで.NETのインスタンス化を表現したもので〜」という言葉での説明もいいですが、実際に見てしまうのが早いでしょう。次のリンクを開くと、XAMLとそれに対応するUnityの表示を見ることができます。

https://pixiv.github.io/Mux/playground.html

(このページのXAML、実は書き換えられるんです。実行中に書き換えられることは、C#とも違うXAMLの魅力です。試しにXAML内のテキストを書き換えてみてください✍️)

f:id:pxvpxv:20210219120848p:plain

しかし、ぱっと見ただけではこれのどこがいいのか分からないかもしれません。確かに、テキストで書かれているので、Gitとの親和性は良さそうですし、複数のコンポーネントのプロパティが一度に見えるのは便利です。でも、それってテキストで書いたらみんな同じですよね。ただのXMLでも、あるいは (シーンファイルより読みやすくした) YAMLでもいいかもしれません。UXML/USSでも同様の機能を実現できるでしょう。

しかし、XAMLの強さは他にあります。

Markup Extension🧙

Markup extensionは、XAMLで値を生成する機能を提供します。スクリプトでIMarkupExtension<T>を実装することでつくることができます。

例えば、色を暗くしたかったらどうでしょう?それなら次のようなmarkup extensionを実装します。

[ContentProperty((nameof(Darken.Original))]
internal sealed class Darken : IMarkupExtension<Color>
{
    public Color Original { get; set; }
    public float T { get; set; }
    public Color ProvideValue(IServiceProvider provider) => Color.Lerp(Original, Color.black, T);
    object IMarkupExtension.ProvideValue(IServiceProvider provider) => Color.Lerp(Original, Color.black, T);
}

一度実装すれば、XAMLの中では{Darken Color.red, T=0.1}といったふうに何回も使えます。自分のアプリケーションでは、UIの記述でどんな値の変換を行っているでしょうか? Markup extensionを使えば、スクリプティングの表現力であらゆる変換を実装して、それをUIから直接使うことができます。スクリプティングの力を気軽にUIの記述につなげることができるのが、Muxの強さです。

既存のmarkup extensionで重要なものには静的なフィールドの値を取り出すx:Staticx:Name属性で名前をつけたXAML内の別のオブジェクトを参照するx:Referenceがあり、これらも有効に使えばUIの記述をより効率的に行なえます。

データバインディング⛓

データバインディングは実は既にもう現れています。先程の例には、フォントを指定するのに{Binding Path=Resources.Font}としていますが、これがデータバインディングです。がっつりBindingって書いてありますしね。

これは次のような.NETのクラスとバインドされています。

internal sealed class PlaygroundViewModel : NotifyPropertyChanged
{
    private Resources _resources;

    public Resources Resources
    {
        get
        {
            return _resources;
        }

        set
        {
            _resources = value;
            OnPropertyChanged();
        }
    }
}

PlaygroundViewModel.Resourcesに値を代入すると、その変更がXAMLで記述されたUIに伝わり、フォントが変更されます。この機能を使うことで、スクリプティングで動的に変更した値をUIに反映させることができます。

そのとき、仮の値がUnity Editorに入力されるのではなく、{Binding Player.Name}{Binding Character.HP}{Binding CurrentLocaleMessage[StartGame]}というように、意味のある名前がXAMLに書かれ、そしてそれは型で検証されます。こうすることで、見て分かるだけでなく、読んで分かる、正しいUIを記述できます。

コンポーネント化による無限の拡張性∞

uGUIのコンポーネントとXAML、データバインディングを組み合わせれば、より複雑な表現が可能です。これをMuxコンポーネントとしてまとめることで、無限の拡張性を得ることができます。

例えば、ボタンのMuxコンポーネントを考えてみます。ボタンはuGUIにあるじゃないかと思うかもしれませんが、uGUIのボタンは押すことができる機能を提供するだけで、その見た目は提供しません。アプリケーション開発者が見た目をつけることで、自由な見た目に変えることができるわけですが、その代わり記述が多くなります。そこで、Muxコンポーネント化の出番となるわけです。

ボタンMuxコンポーネントのXAMLは、例えば次のようになります。

<m:RectTransform
    xmlns="http://xamarin.com/schemas/2014/forms"
    xmlns:m="clr-namespace:Mux.Markup;assembly=Mux.Markup"
    xmlns:mu="clr-namespace:Mux.Markup;assembly=Mux.Markup.UI"
    xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
    xmlns:playground="using:Mux.Playground"
    xmlns:playgroundMarkup="using:Mux.Playground.Markup"
    x:Class="ButtonTransform"
    x:DataType="playground:ViewModel"
    x:Name=_transform
    X="{m:Sized SizeDelta=160}"
    Y="{m:Sized SizeDelta=30}">
    <mu:Image
        Body="{Binding TargetGraphic, Source={x:Reference _button}}"
        Sprite="{Binding Resources.UISprite}"
        Type="Sliced" />
    <mu:Button x:Name="_button" />
    <playgroundMarkup:TextTransform
        Alignment="MiddleCenter"
        Text="{Binding Text, Source={x:Reference _transform}}"
        X="{m:Stretch}"
        Y="{m:Stretch}" />
</m:RectTransform>

C#のコードは次のようになります。

internal sealed class ButtonTransform : RectTransform
{
    [XamlImport] private extern void InitializeComponent();

    public static readonly BindableProperty TextProperty = BindableProperty.Create(nameof(Text), typeof(string), typeof(ButtonTransform));

    public Text
    {
        get => GetValue(TextProperty);
        set => SetValue(TextProperty, value);
    }

    public ButtonTransform()
    {
        InitializeComponent();
    }
}

一度Muxコンポーネントを作ってしまえば、あとは<ButtonTransform Text=”New Button” />といったふうにすればいくらでもボタンを作ることができます。

ここで注意したいのが、指定できるプロパティ (Text) をスクリプトで作っているところです

Unityのプレハブを使うとプレハブのあらゆるコンポーネントのプロパティを変更できますが、このために、注意しないとプレハブを利用する場面によって様々なコンポーネントのプロパティが変更されることになります。

一度そうなってしまえば、プレハブを変更しようと思ってもどのコンポーネントのどのプロパティを残さなければいけないか分かりません。

Muxなら、スクリプトで作っているのでC#のオブジェクト指向の機能を用いて、見せるプロパティを決め、アクセス修飾子でその範囲を決めることができます。

また、このプロパティはバインディングできます。つまり、Muxコンポーネントの1つのプロパティを変更したら、そのコンポーネントに含まれる複数のUnityコンポーネントの値を同時に変更する、といったことも、Bindingを複数回書くだけで簡単にできます。

VRoidで活躍するMux

Muxは、VRoidチームで満足できるユーザー体験を提供するには、比較的小規模なチームだからこそ開発体験でも挑戦する必要があるとして開発されました。既に公開されていたVRoid StudioでMuxへの既存のUIの大胆な移行を行い、その後のVRoid モバイルの開発では初期の時点からMuxが使われています。

今ではほとんど全てのUIがMuxで記述されています

VRoidでは、何度も使うUIをMuxコンポーネント化して再利用することで複雑なUIを実現しています。

また、markup extensionも活用されています。

x:Staticにより、静的なフィールドに記録した、アプリケーション内で共有される色などの定数をまとめています。

少し特殊な例としては、アルファブレンディングがあります。VRoid モバイルでは色空間をリニアにしていますが、uGUIはリニアでのアルファブレンディングに問題を抱えています。

そこで、markup extensionを使ってアプリケーション内でアルファブレンディングを行っています。例えば、{vc:TextHighEmphasis {m:Color R=1, G=1, B=1}}とすると強調文字の色 (R=0, G=0, B=0, A=0.88) と背景色 (R=1 G=1 B=1) を合成することができます。

こういったmarkup extensionを多数実装されています。この機能をUnity Editorとスクリプティングで実現したり、UXML/USSで実現しようとするとかなり困難ですが、XAMLを使えば容易に実装できます。

特にVRoid モバイルではこれらのMuxコンポーネントやmarkup extensionはピクシブが提供するさまざまなサービス群に共通するデザインシステムに則ったコンポーネントが作られており、ピクシブの一貫した世界観を提供することを可能にしています

データバインディングはアプリケーションの状態を扱うのはもちろんのこと、セーフエリアの対応といったものにも使われています。

こうして記述されたXAMLやC#は、GitLabにアップロードされ、開発者同士で意見を交わしながら改善され、ユーザーに届けられています。

f:id:pxvpxv:20210219121834p:plain

VRoidでは、先に挙げたようなuGUI、XAML、データバインディングの機能を余すところなく使い込むことで、開発効率を最大限に高めています📈

公開されたMuxを試す

公開されたMuxは誰でも試用して、その機能を体験できます。対応しているUnityは2020.2です。

Muxはpackagesとなっています。Git URLをプロジェクトに追加してください。

それから初期化スクリプトを追加します。

using Mux;
using UnityEngine;

internal sealed class Initializer
{
    [RuntimeInitializeOnLoadMethod(RuntimeInitializeLoadType.BeforeSceneLoad)]
    private static void OnBeforeSceneLoadRuntimeMethod()
    {
        Forms.mainThread = System.Threading.SynchronizationContext.Current;
        Forms.Init();
        System.Diagnostics.Debug.Listeners.Add(new TraceListener());
    }
}

これでMuxを使えるようになりました。さっそくMuxを使ったコードを書いてみます。

using Mux;
using Mux.Markup;
using System.ComponentModel;

namespace Main
{
    [UnityEngine.RequireComponent(typeof(UnityEngine.Canvas))]
    internal sealed class Main : UnityEngine.MonoBehaviour
    {
        private MainViewModel _viewModel;

#pragma warning disable CS0649
        [UnityEngine.SerializeField]
        private UnityEngine.Font _font;
#pragma warning restore CS0649

        private void Awake()
        {
            _viewModel = new MainViewModel { Font = _font };
            var transform = new View. MainTransform();
            transform.BindingContext = _viewModel;
            transform.X = new Stretch();
            transform.Y = new Stretch();
            transform.AddTo(gameObject);
        }

        private void OnValidate()
        {
             if (_viewModel != null)
            {
                _viewModel.Font = _font;
            }
        }
    }

    internal sealed class MainViewModel : INotifyPropertyChanged
    {
        private UnityEngine.Font _font;

        public UnityEngine.Font Font
        {
            get => _font;
            set
            {
                _font = value;
                PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(nameof(Font));
            }
        }

        public event PropertyChangedEventHandler PropertyChanged;
    }

    internal sealed class MainTransform : RectTransform, IReloadable
    {
        [XamlImport] private extern void InitializeComponent();

        public MainTransform()
        {
            InitializeComponent();
        }

        public void Reload()
        {
            InitializeComponent();
        }
    }
}
<m:RectTransform
    xmlns="http://xamarin.com/schemas/2014/forms"
    xmlns:m="clr-namespace:Mux.Markup;assembly=Mux.Markup"
    xmlns:mu="clr-namespace:Mux.Markup;assembly=Mux.Markup.UI"
    xmlns:main="using:Main"
    xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
    x:Class="Main.MainTransform"
    x:DataType="main:MainViewModel">
    <mu:Text Content="hello, world" Font="{Binding Font}" />
</m:RectTransform>

あとは、Main.MainをuGUIのヒエラルキーに追加すれば、Muxが動き出します。 GameObject Galleryとして、uGUIのGameObject (“New Text”とかあるアレです) の実装例を公開しています。これを参考にMuxでUIを作成してみましょう。

f:id:pxvpxv:20210219122303p:plain

VRoidプロジェクトでの開発を体験する

3Dモデルの制作を扱っているVRoidプロジェクトには、ゲーム開発とも、その他のWebサイトやアプリの開発とも、ちょっと違うけどどこか似ている、不思議で魅力的な開発体験があります。Muxはそういった中から生まれた挑戦の成果です。

VRoidプロジェクトでは開発だけでなく、あらゆる局面で未来の形を目指しています。その様々な局面でも挑戦できる方を募集しています。

*1:もっとも、デスクトップアプリやモバイルアプリではこれらの課題を解決する仕組みがUWPやJetpack Compose、SwiftUIとしてプラットフォーム側から提供されているため、Xamarin.Formsはもっぱらクロスプラットフォームの開発ができる点を利点として宣伝されています。

20191219020351
nekomanma
これまでにPawoo、VRoid Studio/Mobile、そしてWebRTCの.NETバインディングを開発しました。現在はpixiv Sketchのアプリの開発をしています。Web、Unity、C++、ネイティブアプリなど、さまざまな技術スタックを幅広く扱います。