渋谷ほととぎす通信

完全趣味でやってるUnityメモ。説明できないところを説明できるようにするための個人ブログ。昨日の自分より少しでも大きくなれるように。。。 ※所属団体とは一切関係がありません

UnityのUniRx逆引き辞典 (自作)


復習用にUniRxの逆引き辞典なるものを作っています。
※随時更新記事です。更新ログはコメント欄に記載しております

こちらも随時更新しています。

esakun.hateblo.jp

目次

※例が増えてきたらカテゴリ分けします。


■Update関数をUniRxに置き換える

// アップデートストリームを作成
var updateStream = this.UpdateAsObservable ().Select (x => x);
updateStream
    .Subscribe (_ => Debug.Log ("Do Something."));

■Update処理を100フレームに1回ループ実行する

※10フレームだと早すぎて確認しづらかったので、100フレームに変更しました

【改善後 その2】SkipとRepeatUntilDestroyを使用
// アップデートストリームを作成
var updateStream = this.UpdateAsObservable ().Select (x => x);
updateStream
    .Skip (99)
    // Firstオペレータを使用するとこのGameObjectが削除された時に例外が発生します
    .FirstOrDefault ()
    // このオブジェクトが削除されるまでこのストリームはリピートします
    .RepeatUntilDestroy (gameObject)
    .Subscribe (_ => Debug.Log ("Do Something."));

さらにとりすーぷさんのご指摘で、最も無駄がないのは、ThrottleFirstFrameではなくSkipをとのこと(ThrottleFirstFrameは内部的にコルーチンを回すため)。
ということでSkipRepeatUntilDestroyを連携させるの方法が最も無駄のない処理という結論に至りました。

【改善後 その1】ThrottleFirstFrameを使用
// アップデートストリームを作成
var updateStream = this.UpdateAsObservable ().Select (x => x);
updateStream
    // 100回メッセージが届いたら実行する(1〜99回のメッセージは破棄される)
    .ThrottleFirstFrame (100)
    .Subscribe (_ => Debug.Log ("Do Something."));

とりすーぷさんのご指摘で、Bufferは文字通りメッセージをバッファしちゃうため、この場合はThrottleFirstFrameを使うのが無駄がないです。(ThrottleFirstFrameを使うと指定した回数までのメッセージを破棄するからです。)

【改善前】Bufferを使用
// アップデートストリームを作成
var updateStream = this.UpdateAsObservable ().Select (x => x);
updateStream
    // 100回メッセージが届いたら実行する
    .Buffer (100)
    .Subscribe (_ => Debug.Log ("Do Something."));

ポイントはBufferオペレータです。

■タップダウンしたら何かする

// タップダウンストリームを作成
var tapDownStream = this.UpdateAsObservable ()
    .Where (_ => Input.GetMouseButtonDown (0))
tapDownStream
    .Subscribe (_ => {
    Debug.Log ("Do Something.");
});

■タップを離したら何かする(シングルタップ)

// タップアップストリームを作成
var tapUpStream = this.UpdateAsObservable ()
    .Where (_ => Input.GetMouseButtonUp (0))
tapUpStream
    .Subscribe (_ => {
    Debug.Log ("Do Something.");
});

■ダブルタップ

var tapDownStream = this.UpdateAsObservable ()
    .Where (_ => Input.GetMouseButtonDown (0));
tapDownStream
    // 0.2秒以内のメッセージをまとめる
    .Buffer (tapDownStream.Throttle (TimeSpan.FromMilliseconds (200)))
    // タップ回数が2回以上だったら処理する
    .Where (tap => tap.Count >= 2)
    .Subscribe (tap => Debug.Log ("Do Something."));

【ポイント】
Bufferオペレータの引数に0.2秒間にタップされた回数分メッセージを送っている所がポイントです。

未来のプログラミング技術をUnityで -UniRx-
75ページ目辺りが参考になります。

■n回目のタップだけ実行する(実行するのは1回だけ)

int n = 5;
// タップダウンストリームを作成
var tapDownStream = this.UpdateAsObservable ()
    .Where (_ => Input.GetMouseButtonDown (0));
tapDownStream
    // n回メッセージが届いたら実行(n - 1回目まで無視する)
    .Skip (n - 1)
    // 初回実行で終了させる
    .FirstOrDefault ()
    .Subscribe (_ => Debug.Log ("Do Something."));

【補足】
FirstOrDefaultオペレータを削除すれば、n回タップされる度に実行されます。

【追記】
とりすーぷさんからご指摘を頂き、BufferではなくにSkipに変更。メッセージを貯めて何かする場合はBufferオペレータを使うべきですが、そうではなく、指定した回数後に何かする的なものの場合は、Skipを使うと無駄が無いです。

■m回目とn回目のタップだけを実行する

とりすーぷさんのご指摘で2つのストリームを合成して実行する形に変更しました。Mergeオペレータを使用。

【改善後】2つのストリームを合成 Mergeを使用
int m = 5;
int n = 10;
// タップm回目に何かするストリーム
var tapMstream = this.UpdateAsObservable ()
    .Where (_ => Input.GetMouseButtonDown (0))
    .Skip (m - 1)
    .FirstOrDefault ()
    .Do (_ => Debug.Log ("m回目固有の処理"));
// タップn回目に何かするストリーム
var tapNstream = this.UpdateAsObservable ()
    .Where (_ => Input.GetMouseButtonDown (0))
    .Skip (n - 1)
    .FirstOrDefault ()
    .Do (_ => Debug.Log ("n回目固有の処理"));

// 2つのストリームを合成して実行
var MNStream = Observable
    .Merge (tapMstream, tapNstream)
    .Subscribe (_ => Debug.Log ("m回目、n回目共通処理"));

※注意 : MNStreamのSubscribe内の処理は、m回目、n回目どちらの場合も実行されます。

【改善前】2つのストリームをそれぞれ使用
int m = 5;
int n = 10;
// タップダウンストリームを作成
var tapDownStream = this.UpdateAsObservable ()
    .Where (_ => Input.GetMouseButtonDown (0));
tapDownStream
    // m回実行された後に実行するので指定数値から1引く
    .Skip (m - 1)
    .FirstOrDefault ()
    .Subscribe (_ => Debug.Log ("Execute Count : " + m));

tapDownStream
    // n回実行された後に実行するので指定数値から1引く
    .Skip (n - 1)
    .FirstOrDefault ()
    .Subscribe (_ => Debug.Log ("Execute Count : " + n));

【補足】
Skipオペレータを使用しましたが、Bufferオペレータでも同じことが実装可能です。

とりすーぷさんからご指摘を頂き、n回目だけ実行する場合は、BufferよりSkipの方が意味合いとして正しいため補足説明を削除しました。

【間違った例】1つのストリームで実装トライして撃沈
int m = 5;
int n = 10;
// タップダウンストリームを作成
var tapDownStream = this.UpdateAsObservable ()
    .Where (_ => Input.GetMouseButtonDown (0));
tapDownStream
    .Buffer (m)
    .Do (_ => Debug.Log ("Execute Count : " + m))
    .Buffer (n)
    .FirstOrDefault ()
    .Subscribe (_ => Debug.Log ("Execute Count : " + n));

【補足】
この場合、OnCompleteするまでn*m回タップしないといけない。

【緩募】1つのストリームで実装する方法が知りたいです。
上記の【改善後】で2つのストリームを1つのストリームに合成した形で実装しています。

■タップ回数を数える

// タップダウンストリームを作成
var tapDownStream = this.UpdateAsObservable ()
    .Where (_ => Input.GetMouseButtonDown (0));
tapDownStream
    .Select (_ => 1)
        /**
        * sum・・合計値
        * addCount・・加算される数値(Selectから渡ってきた数値)
        */ 
        .Scan ((sum, addCount) => {
        Debug.Log (sum + " : " + addCount);
        return sum + addCount;
    })
    .Subscribe (totalCount => Debug.Log (totalCount));

■タップ後1秒遅れて実行する その1

// タップダウンストリームを作成
var tapDownStream = this.UpdateAsObservable ()
    .Where (_ => Input.GetMouseButtonDown (0));
tapDownStream
    // タップ後1秒遅れて実行させる
    .Delay (TimeSpan.FromMilliseconds (1000))
    // ストリームを実行
    .Subscribe (_ => Debug.Log ("Do Something."));

■タップ後1秒遅れて実行する その2

// タップダウンストリームを作成
var tapDownStream = this.UpdateAsObservable ()
    .Where (_ => Input.GetMouseButtonDown (0));
tapDownStream
    // タップ後1秒遅れて実行させる
    .Throttle (TimeSpan.FromMilliseconds (1000))
    // ストリームを実行
    .Subscribe (_ => Debug.Log ("Do Something."));

『タップ後1秒遅れて実行する その1』と違い、最初のタップ後1秒間何度タップしても、最後の一回しか実行されません。これが、ThrottleオペレータDelayオペレータの大きな違いです。

■タップしている最中、毎フレーム何かする

var tapStream = this.UpdateAsObservable ()
    .Where (_ => Input.GetMouseButton (0));
tapStream
    .Subscribe (_ => Debug.Log ("Execute Tap"));

Input.GetMouseButtonDownではないところに注意。

■Y座標が0未満になった時に座標を取得しつつ実行する

// 座標が0未満になったら実行するストリームを作成
var positionStream = this.UpdateAsObservable ()
    .Where (_ => transform.localPosition.y < 0);
positionStream
    // 現在のY座標を引き渡す
    .Select (_ => transform.localPosition.y)
    // 現在のY座標を取得する
    .Subscribe (posY => Debug.Log (posY));

■uGUIのボタンがクリックされた時に何かする

【その1】 AsObservableを使ってストリームに変換
_button.onClick.AsObservable ()
    .Subscribe (_ => Debug.Log ("Do Something"));

【ポイント】 AsObservable関数でクリックの処理をストリームに変換している所。

※_button変数はUnityEngine.UI.Buttonです(uGUIのボタン)。

【その2】OnPointerClickAsObservableを使ってストリームに変換
_button.OnPointerClickAsObservable ()
    .Subscribe (_ => Debug.Log ("Do Something"));

【その1】より【その2】の方をおすすめします。理由としては、引き渡される値がPointerEventDataなので、クリックした位置を取得など簡単に取得することが出来ます(以下のサンプル参照)。

_button.OnPointerClickAsObservable ()
    // クリックしたポジションを取得
    .Subscribe (_ => Debug.Log (_.position));

■何秒後に何かする

Observable.Timer (TimeSpan.FromSeconds (2)).Subscribe (_ => {
    // Do Something.
});

上記の例は、2秒後に何かします。Timerオペレータを使う所がポイント。

■「何秒後に何かする」を繰り返す

Observable.Timer (TimeSpan.FromSeconds (2), TimeSpan.FromSeconds (1))
    .Subscribe (_ => {
    // Do Something.
});

こちらの例は、「2秒後に何かする」を1秒インターバルを置いてから繰り返し実行します。

「何秒後に何かする」を繰り返すの落とし穴

using UnityEngine;
using System;
using System.Collections;
using UniRx;


public class UniRxTimer : MonoBehaviour
{
    void Start ()
    {
        Observable.Timer (TimeSpan.FromSeconds (2), TimeSpan.FromSeconds (1))
            .Subscribe (_ => {
            // Do Something.
        });
    }
}

この処理は、アプリが起動している間止まること無く実行されます。本コンポーネントがアタッチされたGameObjectが削除されても止まりません。

理由は、Timerオペレータを使うとstaticオブジェクトとして生成されるからです。

【対策 その1】GameObjectと紐付ける

Observable.Timer (TimeSpan.FromSeconds (2), TimeSpan.FromSeconds (1)).Subscribe (_ => {
    // Do something.
}).AddTo (gameObject);

AddTo関数の引数に紐付けるGameObjectを指定します。すると、指定したGameObjectがDestroyされたタイミングで処理は停止します。(OnCompleteは実行されません)

【対策 その2】明示的にDisposeメソッドを実行する

【対策 その1】は、MonoBehaviourクラス上で記述することをを前提とした対策でしたが、TimerオペレータをMonoBehaviour外で書きたい場合もあります。その場合は、使用後必ずDisposeを呼ぶようにします。

永久的に実行しちゃう間違った例

MonoBehaviourクラスを継承していないTimerSampleクラス上でTimerオペレータを使ってタイマー処理を実行しています。Disposeが呼ばれていないので、UniRxTimerがDestroyされても処理は止まりません。

using UnityEngine;
using System.Collections;
using UniRx;
using System;

public class UniRxTimer : MonoBehaviour
{
    void Start ()
    {
        new TimerSample ();
    }
}

public class TimerSample
{
    public TimerSample ()
    {
        Observable.Timer (TimeSpan.FromSeconds (2), TimeSpan.FromSeconds (1))
        .Subscribe (_ => {
            // Do something.
            Debug.Log (_);
        });
    }
}
明示的にDisposeメソッドを実行して処理を中断
using UnityEngine;
using System.Collections;
using UniRx;
using System;

public class UniRxTimer : MonoBehaviour
{
    private TimerSample _timer;

    void Start ()
    {
        _timer = new TimerSample ();
    }

    void OnDestroy ()
    {
        _timer.Dispose ();
    }
}

public class TimerSample
{
    private IDisposable _disposable;

    public TimerSample ()
    {
        _disposable = Observable.Timer (TimeSpan.FromSeconds (2), TimeSpan.FromSeconds (1))
        .Subscribe (_ => {
            // Do something.
            Debug.Log (_);
        });
    }

    public void Dispose ()
    {
        // Disposeメソッドを実行して処理を中断させる
        _disposable.Dispose ();
    }
}