Help us understand the problem. What is going on with this article?

UniRx入門 ~ データバインディングとUnityイベント関数の購読 ~

More than 1 year has passed since last update.

UniRx(github)
UniRx(アセットストア)

はじめに

UniRxはRxをUnity用に移植した素晴らしいライブラリ(アセット)ですが、Rx自体の学習コストの高さから導入が見送られるケースが多い気がします。

この記事では学習コストが高く理解が必要な機能については省き、使い方を覚えるだけで簡単かつ便利に使える強力な機能について紹介します。

ReactiveProperty<T>でデータバインディング

ReactiveProperty<T>を使うと値の変化が通知されてきます。これを使うと例えば「HPが減ったらビューを自動更新」みたいなことができます。

Text[] _textsにはInspectorから適当にuGUIのTextコンポーネントをセットしてください。

DataBinding1.cs
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)って書くのめんどくさい」という場合は自分で実装しましょう。

以下コード。

UnityUIComponentExtensions.cs
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で値をセットしておくことができます。

DataBindingSample2.cs
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>を実装しているのでキャストして公開するだけです。

DataBindingSample3.cs
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()を例にその使いづらさについて考えます。

Player.cs
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を忘れずに。

Player.cs
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減らさない!?

できらぁ!

Player.cs
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で幸せになることを祈っています。

Why not register and get more from Qiita?
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away
Comments
No comments
Sign up for free and join this conversation.
If you already have a Qiita account
Why do not you register as a user and use Qiita more conveniently?
You need to log in to use this function. Qiita can be used more conveniently after logging in.
You seem to be reading articles frequently this month. Qiita can be used more conveniently after logging in.
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away
ユーザーは見つかりませんでした