はじめに
UniRxはRxをUnity用に移植した素晴らしいライブラリ(アセット)ですが、Rx自体の学習コストの高さから導入が見送られるケースが多い気がします。
この記事では学習コストが高く理解が必要な機能については省き、使い方を覚えるだけで簡単かつ便利に使える強力な機能について紹介します。
ReactiveProperty<T>
でデータバインディング
ReactiveProperty<T>
を使うと値の変化が通知されてきます。これを使うと例えば「HPが減ったらビューを自動更新」みたいなことができます。
Text[] _texts
にはInspectorから適当にuGUIのTextコンポーネントをセットしてください。
using System;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.UI;
using UniRx;
public class DataBindingSample1 : MonoBehaviour
{
// NOTE: newするときに初期値を与えることが可能
public ReactiveProperty<int> _intProperty = new ReactiveProperty<int>(1);
public ReactiveProperty<float> _floatProperty = new ReactiveProperty<float>(0.1f);
public ReactiveProperty<bool> _boolProperty = new ReactiveProperty<bool>(true);
public ReactiveProperty<string> _stringProperty = new ReactiveProperty<string>("ABC");
[SerializeField]
private Text[] _texts;
void Start()
{
// バインド(購読)する
_intProperty.SubscribeToText(_texts[0]);
_floatProperty.SubscribeToText(_texts[1]);
_boolProperty.SubscribeToText(_texts[2]);
_stringProperty.SubscribeToText(_texts[3]);
}
void Update()
{
// それぞれの値を適当に更新してみる
_intProperty.Value++;
_floatProperty.Value += 0.1f;
_boolProperty.Value = !_boolProperty.Value;
_stringProperty.Value += "a";
}
}
使い方は簡単で、ReactiveProperty<T>
をnewしてValueに値をセットすることで実際の値を更新、値が更新されるとSubscribeToText(Text text)
で与えたtextが更新されます。
SubscribeToText(Text text)
はIDisposable
を返すので、好きなタイミングでDispose()
を呼び出してバインドを解除します。
番外編:SubscribeToText(TextMeshProUGUI text)
ないの?
「UnityのTextコンポーネントならSubscribeToText(text)
が使えるけどTextMeshProUGUIだとSubscribe(value => t.text)
って書くのめんどくさい」という場合は自分で実装しましょう。
以下コード。
using System;
using UnityEngine;
using UnityEngine.UI;
using TMPro;
namespace UniRx
{
public static partial class UnityUIComponentExtensions
{
public static IDisposable SubscribeToText(this IObservable<string> source, TextMeshProUGUI text)
{
return source.SubscribeWithState(text, (x, t) => t.text = x);
}
public static IDisposable SubscribeToText<T>(this IObservable<T> source, TextMeshProUGUI text)
{
return source.SubscribeWithState(text, (x, t) => t.text = x.ToString());
}
public static IDisposable SubscribeToText<T>(this IObservable<T> source, TextMeshProUGUI text, Func<T, string> selector)
{
return source.SubscribeWithState2(text, selector, (x, t, s) => t.text = s(x));
}
}
}
TextMeshProUGUI以外で使っているTextがあればクラスの記述を置き換えてもらえればOKです。
余談ですが自分は無料になる1週間前くらいに知らずにTextMeshPro買って悔しい思いしました。。。
Inspectorから値をセット可能なIntReactiveProperty
など
ReactiveProperty<T>
だとジェネリックが使われているのでInspectorで値をセットしておけないのですが、int,float,bool,stringのそれぞれに特殊化されたReactiveProperty IntReactiveProperty
,FloatReactiveProperty
,BoolReactiveProperty
,StringReactiveProperty
を使えばInspectorで値をセットしておくことができます。
using System;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.UI;
using UniRx;
public class DataBindingSample2 : MonoBehaviour
{
// NOTE: ジェネリックを使っていないので値をInspectorからセットできる
public IntReactiveProperty _intProperty = new IntReactiveProperty();
public FloatReactiveProperty _floatProperty = new FloatReactiveProperty();
public BoolReactiveProperty _boolProperty = new BoolReactiveProperty();
public StringReactiveProperty _stringProperty = new StringReactiveProperty();
[SerializeField]
private Text[] _texts;
[SerializeField]
private Button _button;
void Start()
{
// バインド(購読)する
_intProperty.Subscribe(value => _texts[0].text = value.ToString("D5"));
_floatProperty.Subscribe(value => _texts[1].text = (value * 10).ToString("F1"));
// NOTE: ButtonのInteractableにもバインドできる!
_boolProperty.SubscribeToInteractable(_button);
_stringProperty.Subscribe(value => _texts[3].text = value.Replace('a', 'z'));
}
void Update()
{
// それぞれの値を適当に更新してみる
_intProperty.Value++;
_floatProperty.Value += 0.1f;
_boolProperty.Value = !_boolProperty.Value;
_stringProperty.Value += "a";
}
}
今度はSubscribeToText(Text text)
ではなくSubscribe(Action<T> OnNext)
を使ってtextにセットする前に値を加工してみました。引数がTextでないのでText以外にも値をバインドできます。
コード中にコメントで書いてありますが、SubscribeToInteractable(Selectable selectable)
を使えばボタンのInteractableにバインドしたりできます。
IReadOnlyReactiveProperty<T>
インターフェース(2017/6/15修正)
これまでのコードだとReactiveProperty自体がpublicで公開されていてValueが外から更新される可能性がある状態でした。
今度はIReadOnlyReactiveProperty
インターフェースを使って外からは値を更新できないようにしてみます。ReactiveProperty<T>
はIReadOnlyReactiveProperty<T>
を実装しているのでキャストして公開するだけです。
using System;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.UI;
using UniRx;
public class DataBindingSample3 : MonoBehaviour
{
#region private ReactiveProperty
[SerializeField]
private IntReactiveProperty _intProperty = new IntReactiveProperty();
[SerializeField]
private FloatReactiveProperty _floatProperty = new FloatReactiveProperty();
[SerializeField]
private BoolReactiveProperty _boolProperty = new BoolReactiveProperty();
[SerializeField]
private StringReactiveProperty _stringProperty = new StringReactiveProperty();
#endregion
#region public IReadOnlyReactiveProperty
public IReadOnlyReactiveProperty<int> IntProperty {
get { return _intProperty; }
}
public IReadOnlyReactiveProperty<float> FloatProperty {
get { return _floatProperty; }
}
public IReadOnlyReactiveProperty<bool> BoolProperty {
get { return _boolProperty; }
}
public IReadOnlyReactiveProperty<string> StringProperty {
get { return _stringProperty; }
}
#endregion
[SerializeField]
private Text[] _texts;
[SerializeField]
private Button _button;
void Update()
{
// それぞれの値を適当に更新してみる
_intProperty.Value++;
_floatProperty.Value += 0.1f;
_boolProperty.Value = !_boolProperty.Value;
_stringProperty.Value += "a";
}
}
IReadOnlyReactiveProperty<T>
は以下のようにValueのセッターを持っていないのでpublicにしても外部から値を更新されません。
public interface IReadOnlyReactiveProperty<T> : IObservable<T>
{
T Value { get; }
bool HasValue { get; }
}
public interface IReactiveProperty<T> : IReadOnlyReactiveProperty<T>
{
new T Value { get; set; }
}
public class ReactiveProperty<T> : IReactiveProperty<T>, IDisposable, IOptimizedObservable<T>
{
// 実装
}
基本的にはモデルがこんな風にIReadOnlyReactiveProperty<T>
を公開して、ビュー側でSubscribe()
する設計になると思います。
(2017/6/15追記)
ReadOnlyReactiveProperty<T>
「外部への公開はToReadOnlyReactiveProperty()
を呼んでReadOnlyReactiveProperty
として公開する(キリッ」と書いてましたが、ToReadOnlyReactiveProperty()
を呼ぶと内部でnew ReadOnlyReactiveProperty()
が走るので
- 同じものを
Subscribe()
しているつもりが挙動が変になる - 参照のたびに
new
されるのでメモリとGC的に良くない
という良くないことが起こります(@toRisouP さんにtwitterで教えていただきました)。
(2017/6/15追記 ここまで)
ObserveEveryValueChanged
を使ってReactivePropertyでない値を監視
「いまさら全部ReactiveProperty<T>
に書き換えられないけど値の変更がPushされてくる機構はほしいなぁ」という場合はObserveEveryValueChanged
が使えます。
UniRxの作者さんの解説を貼っておきます。
最後に、ObserveEveryValueChangedを紹介します。これは、ラムダ式で指定した値を変更のあった時にだけ通知するという、つまり変更通知のない値を変更通知付きに変換するという魔法のような(実際ベンリ!)機能です(実際は毎フレーム監視してるんで、ポーリングによる擬似的なPull→Push変換)
// watch position change this.transform.ObserveEveryValueChanged(x => x.position).Subscribe(x => Debug.Log(x));
これは監視対象がGameObjectの場合はDestroy時にOnCompletedを発行して監視を止めます。通常のC#クラス(POCO)の場合は、GCされた時に、同様にOnCompletedを発行して監視を止めるようになっています(内部的にはWeakReferenceを用いて実装されています)。ただのポーリングなので多用すぎるとアレですが、お手軽でベンリには違いないので適宜どうぞ。
Unityが自動で呼び出すメソッドのイベント化
MonoBehaviourには決まったタイミングでUnityから呼び出されるイベント関数というものがたくさんあります。
一番よく使われていると思われるUpdate()
を例にその使いづらさについて考えます。
using UnityEngine;
public class Player : MonoBehaviour
{
public bool isPoisoned;
public bool isRegenerating;
public int hp {
get;
private set;
}
void Start()
{
hp = 100;
}
void Update()
{
if (isPoisoned) {
hp -= 1;
}
if (isRegenerating) {
hp += 2;
}
}
}
上記のコードはクラスが肥大化したときにその複雑さに耐えきれなくなります。
というのは、1フレーム毎に呼び出してほしい処理がUpdate()
内に何種類も書いてあるため、
-
Update()
が肥大化する - 変数のスコープが大きくなる
からです。
UpdateAsObservable()
で毎フレーム起きるイベントを購読する
UpdateAsObservable()
を使うと毎フレームイベントが飛んでくるので、購読することで処理をカプセル化してスコープを限定できます。
先の例だとLINQのようにWhere()
でフィルタを挟むことでやりたいことが実現できます。using UniRx.Triggers
を忘れずに。
using UnityEngine;
using UniRx;
using UniRx.Triggers;
public class Player : MonoBehaviour
{
public bool isPoisoned;
public bool isRegenerating;
public int hp {
get;
private set;
}
void Start()
{
hp = 100;
this.UpdateAsObservable()
.Where(_ => isPoisoned)
.Subscribe(_ => hp -= 1);
this.UpdateAsObservable()
.Where(_ => isRegenerating)
.Subscribe(_ => hp += 2);
}
}
そもそもUpdate()
というメソッドは消え去り、処理は個々の無名関数に閉じ込められました。
購読をやめたくなったときもSubscribe()
の戻り値のIDisposable
を受け取ってDispose()
を呼べばOKです。
え?毒によるダメージは毎フレームじゃなくて1秒おきに10回!?しかも死んでたら毒解除してHP減らさない!?
できらぁ!
using System;
using UnityEngine;
using UniRx;
using UniRx.Triggers;
public class Player : MonoBehaviour
{
public BoolReactiveProperty IsPoisoned = new BoolReactiveProperty();
public IntReactiveProperty Hp = new IntReactiveProperty();
public ReadOnlyReactiveProperty<bool> IsDead;
void Awake()
{
Hp.Value = 100;
// getterを作るような感じで新しいReactivePropertyを作れる
IsDead = Hp.Select(x => x <= 0).ToReadOnlyReactiveProperty();
}
void Start()
{
this.IsPoisoned
.Where(value => value) // trueに変化したときだけ通知
.Take(1)
.SelectMany(_ => Observable.Interval(TimeSpan.FromSeconds(1))) // 1秒ごとのタイマーに差し替え
.Take(10) // 10個来たら終わり
.TakeWhile(_ => IsPoisoned.Value && !IsDead.Value) // どちらかがtrueで終わり
.DoOnCompleted(() => IsPoisoned.Value = false)
.RepeatUntilDestroy(this) // 1回の毒が終わったら最初から購読し直し
.Subscribe(_ => Hp.Value -= 1);
}
}
だいぶごちゃごちゃしてきましたが、「理解できればいろいろできるんだな」くらいの認識で大丈夫です。
Rxを使いこなせるようになってくるといろんな処理がメソッドチェーンで書けてスコープが小さく保てるのですが、短く書ける分オペレータの挙動を正しく理解していないとハマりやすいです。
最初のうちはとりあえずLINQを知っていれば使えるSelect()
とWhere()
以外のオペレータは使わないようにしましょう。
MonoBehaviour
の購読可能なイベント
MonoBehaviour
の自動で呼ばれる関数については全部購読可能になると思ってもらっていいです。
使い方は先のUpdateAsObservable()
と同じで、using UniRx.Triggers
することでMonoBehaviour
のインスタンスにメソッドが生えるのでSubscribe()
で購読します。
- OnAnimatorIKAsObservable
- OnAnimatorMoveAsObservable
- OnCollisionEnter2DAsObservable
- OnCollisionExit2DAsObservable
- OnCollisionStay2DAsObservable
- OnCollisionEnterAsObservable
- OnCollisionExitAsObservable
- OnCollisionStayAsObservable
- OnDestroyAsObservable
- OnEnableAsObservable
- OnDisableAsObservable
- FixedUpdateAsObservable
- LateUpdateAsObservable
- OnMouseDownAsObservable
- OnMouseDragAsObservable
- OnMouseEnterAsObservable
- OnMouseExitAsObservable
- OnMouseOverAsObservable
- OnMouseUpAsObservable
- OnMouseUpAsButtonAsObservable
- OnTriggerEnter2DAsObservable
- OnTriggerExit2DAsObservable
- OnTriggerStay2DAsObservable
- OnTriggerEnterAsObservable
- OnTriggerExitAsObservable
- OnTriggerStayAsObservable
- UpdateAsObservable
- OnBecameInvisibleAsObservable
- OnBecameVisibleAsObservable
- OnBeforeTransformParentChangedAsObservable
- OnTransformParentChangedAsObservable
- OnTransformChildrenChangedAsObservable
- OnCanvasGroupChangedAsObservable
- OnRectTransformDimensionsChangeAsObservable
- OnRectTransformRemovedAsObservable
- OnParticleCollisionAsObservable
- OnParticleTriggerAsObservable
uGUI系の購読可能なイベント
こちらはObservableEventTrigger
コンポーネントをAddComponent()
して使います。
var trigger = gameObject.AddComponent<ObservableEventTrigger>();
trigger.OnPointerClickAsObservable()
.Subscribe(e => Debug.Log("OnPointerClick"));
- OnDeselectAsObservable
- OnMoveAsObservable
- OnPointerDownAsObservable
- OnPointerEnterAsObservable
- OnPointerExitAsObservable
- OnPointerUpAsObservable
- OnSelectAsObservable
- OnPointerClickAsObservable
- OnSubmitAsObservable
- OnDragAsObservable
- OnBeginDragAsObservable
- OnEndDragAsObservable
- OnDropAsObservable
- OnUpdateSelectedAsObservable
- OnInitializePotentialDragAsObservable
- OnCancelAsObservable
- OnScrollAsObservable
UnityEventの購読可能なイベント
UnityEventはAsObservable()
を呼ぶことで購読可能になります。
「AddListener()
でいいじゃん」と思われるかもしれませんが、Rxへの世界への変換を挟むといろいろといいことがあるので覚えておきましょう。
using UniRx;
// AsObservable()での変換だと初期値は購読時に送られてこない
button.OnClick.AsObservable()
toggle.OnValueChanged.AsObservable()
scrollbar.OnValueChanged.AsObservable()
scrollRect.OnValueChanged.AsObservable()
slider.OnValueChanged.AsObservable()
inputField.OnEndEdit.AsObservable()
inputField.OnValueChanged.AsObservable()
// 初期値が購読時に送られてくるver.
button.OnClickAsObservable()
toggle.OnValueChangedAsObservable()
scrollbar.OnValueChangedAsObservable()
scrollRect.OnValueChangedAsObservable()
slider.OnValueChangedAsObservable()
inputField.OnEndEditAsObservable()
inputField.OnValueChangedAsObservable()
OnApplicationXXX
の購読可能なイベント
Observable
というクラスのstaticメソッドとして生えています。
MonoBehaviour
が必要ないので通常のクラスでも使えますが、購読の解除には気をつけるようにしましょう。
using UniRx;
Observable.EveryApplicationPause()
.Subscribe(pause => Debug.Log("pause:" + pause))
.AddTo(this) // MonoBehaviourで使う時はこれを書いておくと良い
- EveryApplicationPause
- EveryApplicationFocus
最後に
UniRxは確かに学習コストが高いです。
というかできることが多い&すご過ぎて、だんだん使えるようになってくると「素のUnityってなんて貧弱なんだ!」と思うくらいになります。
とりあえず最初のうちは難しいことは一旦置いといて、「なんでもかんでもイベント(Observable)化してくれるんだな」くらいで使い始めてみてはどうでしょうか。
一人でも多くのUnity使いがUniRxで幸せになることを祈っています。