UniRx.ReactivePropertyの紹介
Viewの更新に関する苦悩
ネイティブの開発で、最も苦悩するジャンルの一つではないでしょうか?
随時書き換えられていくModelに対して、付随するView更新の処理は、ともするとグチャグチャになりがちです。

たとえば、上記のような画面があったとして、各情報の更新方法として、どんなアプローチがあるでしょうか?
パターン1: Modelで頑張る
…って。
だめ、絶対。
パターン2: SendMessage式
Modelが、変更通知を送信するパターンです。
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を利用することで簡単に実装する事が可能です。
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の使い方
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について勉強していきたいですね!