UniRxを導入するメリット ~こういう時にUniRxは使えるよ~

  • 47
    いいね
  • 2
    コメント

UniRxを導入するメリット

Rxが流行っています。
UnityにもUniRxという移植があります。
こちらかAssetStoreから簡単に入れることができます。
https://github.com/neuecc/UniRx

グラニのCTOの人が作ったのできっと内部ではバリバリ使われているのでしょううらやましい。

最新は5.4.0かな
http://neue.cc/2016/08/03_536.html

Rx自体つかいこなせるなら何も考えることなく脳死で突っ込めばOKなんですけれど、慣れていない場合どこから使い始めればいいの?
という問題があります。

概念も難しいしスキルキャップが高いのでとても便利なのですが反面導入を躊躇してしまうこともあるでしょう。
ただ、やっぱり使いこなせると最高に便利なツールの一つなので是非もなく導入してほしいという気持ちがあります。

というわけですぐ使えるUniRxの使い方をいくつか紹介したいと思います。

この記事の趣旨は
使おうと思ってるけど使い方がわからない人向けに簡単にできる置き換えをいくつか紹介する
ことです。

取り合えず食べてみて!おいしいから!っていう試食みたいな記事です。

すぐに試せるRx

このセクションではすぐに使えるUniRxの使い方を紹介します。

コルーチンをMonoBehaviour以外で使う

コルーチンをIObservable化(Rxの基本のインターフェイス)するといくつかのメリットがあります。
普通コルーチンはこんな感じで使うのかなと思います。

before
    /// <summary>
    /// コルーチン使う側
    /// </summary>
    public void UseCoroutine()
    {
        StartCoroutine(Coroutine());
    }

    /// <summary>
    /// コルーチン
    /// </summary>
    /// <returns></returns>
    public IEnumerator Coroutine()
    {
        Debug.Log("Start");
        yield return new WaitForSeconds(1);
        Debug.Log("End");
    }

この書き方だといくつか気になる部分があります。

  • StartCoroutineがMonoBehaviour以外からしか呼べない…
    • StartCoroutineがMonoBehaviourのメンバだから起こる問題
  • コルーチンが終わった後に何か処理をしたいけど面倒くさい…
    • これをするためにはもう一つIEnumeratorを返すメソッドを作らないといけない
    • 終了後の処理の分IEumeratorを返すメソッドがいる
  • コルーチンにコールバックを持たせる方法はあるけれどインターフェイスがバラバラに…

このような問題に対してUniRxは一つの解決策になります。

after
    /// <summary>
    /// コルーチン使う側
    /// </summary>
    public void UseCoroutine()
    {
        //これはMonoBehaviour以外でも呼べる
        Observable.FromCoroutine(Coroutine) //これでIObasevable化
            .Subscribe(_ => Debug.Log("コルーチン終了後の処理")); //これで終了後の処理
    }


    /// <summary>
    /// コルーチン
    /// これがMonoBehaviourで内に定義されている必要もない
    /// </summary>
    /// <returns></returns>
    public IEnumerator Coroutine()
    {
        Debug.Log("Start");
        yield return new WaitForSeconds(1);
        Debug.Log("End");
    }

もしも使う側が慣れていないのであればObservable.FromCoroutine(Coroutine)をメソッド化してしまうのもよいと思います。
この場合CoroutineAsyncとかが名前としてはお勧めです。

ボタンの動作を状況によって変える

チュートリアルなどで、実装の課題となるポイントの一つにある機能を封印したいなということがあるかと思います。
逆に、機能の開放などで機能を追加したいときもあるでしょう。(そして未開放の時は何章開放したら見れるよ!みたいな)
つまり動的にボタンの機能を変えたいパターンというのがあります。

いくつかの方法はあると思うのですが、一番素直な方法はif文を使って分岐することでしょう。
例えばこんなメソッドを作ってuGUIのボタンに登録するわけです。

before
    private bool IsTutorial;
    public void ClickButton()
    {
        if (IsTutorial)
        {
            Debug.Log("このボタンは使えないよ!");
            return;

        }
        Debug.Log("みんなが使いたがる機能だよ");
    }

これの問題はこんなところでしょうか

  • チュートリアルの処理がいろいろなところに分散する
  • if文の中はチュートリアルという問題だけで実際の機能とは関係ない
    • 実装方法いかんではセリフとか説明とか出さなきゃってなる(もちろんこれはview系のところにやらせるべき)

これの解決策もやはりいくつかあるのですがUniRxでの解決策はこんなところです。
これをStartメソッドか呼び出します。

after
    public void TutorialSetting()
    {
        var button = GetComponent<Button>();
        if (IsTutorial)
        {   
            button.OnClickAsObservable()//ボタンが押されたことを検知する
                  .Subscribe(_ => Debug.Log("このボタンは使えないよ!"));//先ほどと同じように登録
        }
        else
        {
            button.OnClickAsObservable()
                  .Subscribe(_ => Debug.Log("みんなが使いたがる機能だよ"));
        }


    }

これだけならeventでできるのですが、ほかにもいろいろなタイミングを登録できます。
(毎アップデート事とかボタンがenableになったときとかUnityが用意しているタイミングならほぼ網羅しています)

数秒後に何かをする

初期化の関係メソッドの数秒後に何かをしたいです。
でもコルーチンはちょっとめんどくさい…というときにUniRxだと簡単に数秒後の処理を書くことができます。

timer
    void Start()
    {
        Observable.Timer(TimeSpan.FromSeconds(2)).Subscribe(_=>Debug.Log("2秒後の処理"));
        Observable.TimerFrame(2).Subscribe(_ => Debug.Log("2フレーム後の処理"));
    }

アップデートメソッドの最初の1フレーム目だけ何かをする

アップデートメソッドの最初の1フレーム目だけ何かをする
たまーにタイミングの関係でどうしてもやらなくてはいけなくなるこんな処理
どう頑張ってもStartとかだとうまく動かないどうしてもUpdateがいいんだ!

普通に書くとこんな感じ
やっぱりアップデート事のif文って気持ち悪いし…可読性もよくない気がする…

before

    private bool initialized ;
    void Update()
    {
        if (initialized)
        {
            Debug.Log("初期化");
        }
        //毎回やる処理
    }

UniRxだとStart時にアップデートの最初の一回だけ処理をすることを登録するということもできます

after
    void Start()
    {
        Observable.EveryUpdate()//毎アップデート事にイベントが発火する
            .FirstOrDefault()//最初の一回だけでそのあとは捨てる(というか破棄される)
            .Subscribe(_ => Debug.Log("アップデート後の処理"));
    }

ちなみに
Observable.EveryUpdate().Subscribe(~)だと毎回アップデート事に~が実行されます。

基本的にRxはイベント処理の上位互換にあたるライブラリだと考えてください。
そのうえで上記のように最初の一回だけや、最初のn回はスキップするなどのフィルターをかけることができます。
そしてその記述方法はLINQのよくあるメソッド+Rxならではのいくつかのメソッドになります。

実はこの用途には「Observable.NextFrame」という専用ものがあるみたいです。
とりあえず例ということでおいておきます

書き方が統一される

これ個人的にはすごい利点なんですけれど、Rxではすべて先頭のIObservableを作成するメソッド自体は違ってもそのあとの書き方は一緒です。
基本的にはこの形になります。

IObservableの生成メソッド.[場合によってはオペレータ.]Subscribe(処理);

この形さえ覚えてもらえればいろいろな応用が利きます。

ですので、特に話し合いなどしなくても書き方が統一されるというメリットがあります。

これだけでも使えるScheduler

UniRxのちょっと変わった使い方になりますが地味に便利な機能としてSchedulerというものがあります
 ※内部ではバリバリ使ってるんですよ

これの使い道ですが、例えば入力データなどの状況によって同期的にやるか別スレッドで実行するかを選択したい場面があるかもしれません。
そういったときにこのScheduler は「いつ」「どのように」処理を実行するかを指定できるものです。

schaduler
        Scheduler.ThreadPool.Schedule(() => Debug.Log("別スレッドで実行"));
        Scheduler.Immediate.Schedule(() => Debug.Log("すぐに実行"));
        Scheduler.MainThreadEndOfFrame.Schedule(() => Debug.Log("フレーム終了後に実行"));

Scheduler.ThreadPoolなどはISchedulerのオブジェクトなので引数にもできますし、if文で分岐させることも簡単です。
もちろんIObservableと一緒に使われることが想定されているので組み合わせると強力です。

その他いろいろ

WWWの利用が簡単になる

ObservableWWWというのがあります。(ただしWWWをいまからつかうべきかは)

いろいろな言語で流用できる

細部は違ってもいろいろな言語にRxは移植されていますのでいろいろな言語で同じシステムが使えます。

アイデアスケッチ

使い方によってはこんなことが普通に書くより少し簡単にできます

ゲームのカウントダウン⇒スタートの制御
アイテムを選んだらその説明が説明欄に出てくる
ファイルの読み込み時の定型文(using)とかを省略できるようにする
HPが25%を切ったらHPバーを赤くする

UniRxの注意点

UniRxはとても便利で強力なのですが一つだけ注意しなければいけないことがあります。
それはUniRxが少しだけ重い処理だということです。

1Frameに何度もObservableのインスタンスが生成されるような感じで使うと死にます。
オペレータ一回でインスタンス一つ生成するパターンもあるのでバチバチにチューニングしたいところでは注意が必要かもしれません。

一方で得意な部分はUIなどのメッセージパッシングや1frameに1回などのあまり大きな処理をしない部分。非同期処理をしたい通信やIOなどの部分、などある程度UniRx自身の重さが無視できる部分では強力なツールになりえます。

UniRx参考

UniRx入門 その1
http://qiita.com/toRisouP/items/2f1643e344c741dd94f8

Rx再入門
http://blog.okazuki.jp/entry/20111031/1320072227

UniRxのシンプルなサンプルの取扱説明書(自分が書いたやつだから入れたくなっただけ)
http://qiita.com/Marimoiro/items/b0c66c6c22cf58bde049