UniRx.ReactivePropertyの紹介

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

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について勉強していきたいですね!