tonari note

オンラインゲームエンジニアの雑記

UniRxとカスタムバインディング

お久しぶりです。
最近UniVM更新しつつもどうなってるかは全くな感じだったんで、現状どうなってるかだけメモがてら書いておきます…

github.com

まずUniVMの方向性を、MVRPパターンを拡張する形で、バインディングの部分を担う感じにしようと思いまして、UniRx必須にしました。

speakerdeck.com

UniRxを必須にして何が嬉しいかというと、ReactivePropertyが使える点です。
UniVMはUnityEventへのバインディングもでき、またUniRxはUnityEventをIObservableに変換してくれるので、UIからの入力からUIへの出力までを全てUniVM/UniRxを介して行うことができるようになってます。
結局のところ、UniRxが提供してくれているOnClickAsObservableやSubscribeToTextの部分をもっと柔軟にし、MVRPのRPの部分から、UnityEngine.Componentを排除できるものがUniVMと思ってもらえると。
あと、当然ですがUniRxと違ってこちらはオレオレライブラリです。

まずContextによってMonoBehaviourへの依存を切る方法です。

using UniVM;
using UniRx;

// ContextはMonoBehaviourに影響を受けない
public class SampleContext : IContext
{
    public ReactiveProperty<int> Value { get; private set; }

    public void Initialize()
    {
        Value = new ReactiveProperty<int>();
    }

    public void Dispose()
    {
        Value.Dispose();
    }
}

// ViewModelはMonoBehaviourなので、インスペクタから外部の値を設定できる
public class SampleViewModel : ViewModel
{
    // 外部から何かしらのComponentを持ってくる
    public AnotherSampleBehaviour Another;

    public SampleContext Child { get; private set; }

    public ReactiveProperty<int> Value { get; private set; }

    public override void Initialize()
    {
        Child = new SampleContext();
        Child.Initialize();

        // Anotherの値で初期化みたいなことができる
        Value = new ReactiveProperty<int>(Another.Value);
    }

    public override void Dispose()
    {
        Child.Dispose();
        Value.Dispose();
    }
}

SampleContextのValueへのバインディングですが、今回はViewModel内でChildという名前で定義されているので、バインディングのPathに"Child.Value"を入れることでアクセスできます。

余談ですが、ReactivePropertyの配列やContextの配列なども定義でき、それらの配列要素に関しては、"Child[0].Value[0]"みたいにインデクサを指定することによってアクセスが出来ます。

また、カスタムバインディングを用意しました。
たとえば、以下の様な感じ。

using UnityEngine;
using UnityEngine.UI;
using UniVM;

enum Type { Light, Dark }

[RequireComponent(typeof(Image))]
class MyCustomBinding : CustomPropertyBinding<Type>
{
    private Type current;

    // 各タイプに合ったスプライト(インスペクタで設定)
    public Sprite LightSprite;
    public Sprite DarkSprite;
    // スプライトを表示するImage
    private Image image;

    protected override Type GetComponentValue()
    {
        return current;
    }

    protected override void InitializeComponent()
    {
        // Componentの初期化
        image = GetComponent<Image>();
    }

    protected override void UpdateComponent(Type value)
    {
        current = value;

        // バインドされたTypeに応じてImageにSpriteを適応
        switch (value)
        {
            case Type.Light:
                image.sprite = LightSprite;
                break;
            case Type.Dark:
                image.sprite = DarkSprite;
                break;
            default:
                break;
        }
    }
}
//=================
// Context内
public ReactiveProperty<Type> CharaType { get; private set; }

このスクリプトによって、ContextのReactivePropertyからImageへのバインディングを定義することができます。
こうすることによって、よりコンポーネントに依存し、特定の場合に特化されている物を外に出してやることによって、RP層の肥大化を防ぎ、ContextをUIから遠ざけることによって再利用性を高めることができます。

またMVRPで必要なコンポーネントをインスペクター上でD&Dする作業を極力減らしながらも、他のシーン上のComponentとの繋ぎ(汚れ仕事)をViewModelが担ってくれるので、下手にいろんな所が抜け落ちてる、みたいな状態を回避できるかとも思います。