UniRx.ReactivePropertyの紹介

木内智史之介(シャッチョー)
ミンカさんけっこんしてくださいおねがいします(ズザー
SEGAさん、DIVAの筐体ください(ズザー

Viewの更新に関する苦悩

ネイティブの開発で、最も苦悩するジャンルの一つではないでしょうか?

随時書き換えられていくModelに対して、付随するView更新の処理は、ともするとグチャグチャになりがちです。

よくあるゲーム画面の例

たとえば、上記のような画面があったとして、各情報の更新方法として、どんなアプローチがあるでしょうか?

パターン1: Modelで頑張る

…って。

だめ、絶対。

パターン2: SendMessage式

Modelが、変更通知を送信するパターンです。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
public class CharacterModel : BaseModel
{
    public int hp;
    public int mp;

    // Modelの変更通知を受け取りたいリスナーをあらかじめ登録しておく
    List<GameObject> onChangeListeners;

    public void GotDamage(int amount)
    {
        hp -= amount;

        onChangeListeners.ForEach(x =>
            ExecuteEvents.Execute<IPlayerModelEventHandler>(
                target: x,
                eventData: null,
                functor: (y,z) => y.OnHPChanged(hp)
            )
        );
    }
}

public interface IPlayerModelEventHandler
{
    void OnHPChanged(int hp);
}

public class CharacterConditionView : IPlayerModelEventHandler
{
    [SerializeField]
    Text hpText;

    public void OnHPChanged(int hp)
    {
        hpText.text = hp.ToString();
    }
}

うん、悪くないな。
ただ、これにも問題があり、インスペクター上からモデルの情報を修正した際に、Viewが連動しない、という事です。

パターン3: event式

C#には、「event」と呼ばれる便利な仕組みがあって、コールバック系の処理はeventを利用することで簡単に実装する事が可能です。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
public class CharacterModel : BaseModel
{
    public int hp;
    public int mp;

    // HPの変更イベント
    public event Action<int> OnHPChanged;

    public void GotDamage(int amount)
    {
        hp -= amount;

        OnHPChanged(hp);
    }
}

public class SomeView : MonoBehaviour
{
    [SerializeField]
    Text hpText;

    public void SetCharacter(CharacterModel character)
    {
        character.OnHPChanged += OnHPChanged;
    }

    public void OnHPChanged(int hp)
    {
        hpText.text = hp.ToString();
    }
}

これも、悪くない。

ただ、やはり、パターン2と同様、インスペクター上から値を変更してもViewは連動してくれません。

インスペクター上では、メソッドを通さず、メンバーを直接書き換えるため、当たり前と言えば当たり前なんですが、「モデルの状態を表現するView」としては、もっと強く関連づけたいところですよね?

それを解決してくれるのが「ReactiveProperty」です。

ReactivePropertyについて

ReactivePropertyとは?

Rxの概念として、ReactivePropertyから入った方が、むしろわかりがいいかもしれません。

Rxの概念は、ざっくり大きく分けてしまうと、以下の二つで成り立っています。

  • ストリームに値が流れる
  • ストリームに流れた値を購読する

これはつまり、とあるクラスのメンバーの状態変更を、Viewまで通知するための仕組みにそのまま流用が可能です。

ですので、下記のように言い換える事が可能です。

  • クラスのメンバーの状態が変化する(ストリームに値が流れる)
  • それをView側で検知する(ストリームに流れた値を購読する)

イメージがつかめますでしょうか?

ReactivePropertyの使い方

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
public class CharacterModel : BaseModel
{
    // ReactivePropertyとして定義する
    public ReactiveProperty<int> hp;

    // ジェネリックなメンバーはインスペクターから表示できない制限をうけるので、
    // インスペクター上から編集したい値は別途用意されているクラスを使用
    public IntReactiveProperty mp;

    public void GotDamage(int amount)
    {
        hp.Value -= amount;
    }
}

public class SomeView : MonoBehaviour
{
    [SerializeField]
    Text hpText;

    public void SetCharacter(CharacterModel character)
    {
        // hpが変更された際に流れるストリームを購読
        character.hp.Subscribe(x => SetHP(x));
    }

    public void SetHP(int hp)
    {
        hpText.text = hp.ToString();
    }
}

いかがでしょうか?

非常に、すっきり、ハッキリしていて、好感の持てるコードかと思います。

Model側に通知先を保持する必要性もない(非常に疎結合)ですし、インスペクター上からModelの状態を変更してもそれがしっかりView側に届きます(ある意味で密結合)。

これから、もっとRxについて勉強していきたいですね!