UniRx学習(1)

参考スライド

こちらのスライドを一通り読んだ.

www.slideshare.net

UniRx の利点

時間の取扱が簡単になる!

(例)

  • イベントの待ち受け
    • マウスクリック
    • ボタン入力のタイミング
  • 非同期処理
    • 別スレッド通信
    • 別スレッドデータロード
  • 時間計測が判定に必要な処理
    • 長押し
    • ダブルクリック
  • 時間変化する値の監視
    • False → True になった瞬間に 1 回だけ処理

Rxを使わない方法とRxを使った場合の違い

前者では,イベントを受け取った後にどうするかを書いていた
後者の場合,イベントを受け取る前に何をしたいかが書ける

Rxの使い方

① ストリームを用意する
② ストリームをオペレータで加工する
③ Subscribeする

クリックされたら画面に表示する

UniRx利用時

using UnityEngine;
using UnityEngine.UI;
using UniRx;

public class ButtonClick : MonoBehaviour
{
    [SerializeField] private Button button;
    [SerializeField] private Text text;

    void Start()
    {
        button.onClick
            .AsObservable()
            .Subscribe(_ => text.text = "Clicked");
    }
}

UniRx.Triggers利用時

UniRxには,uGUI用のObservableやSubscribeが準備されている.

using UnityEngine;
using UnityEngine.UI;
using UniRx;
using UniRx.Triggers;

public class ButtonClick : MonoBehaviour
{
    [SerializeField] private Button button;
    [SerializeField] private Text text;

    void Start()
    {
        button
            .OnClickAsObservable()
            .SubscribeToText(text, _ => "clicked");
    }
}

Buttonが3回押されたらTextに表示

オペレータBuffer(3) または オペレータSkip(2)を加える

button
    .OnClickAsObservable()
    .Buffer(3)
    .SubscribeToText(text, _ => "clicked");

Buttonが2つとも押されたらTextに表示

オペレータZip()を利用する

両方が交互に1回ずつ押された時にTextに表示する
 → 連打されても「1回押された」と判定させる

button1
    .OnClickAsObservable()
    .Zip(button2.OnClickAsObservable(), (b1, b2) => "Clicked!")
    .First() // 1度動作した後に
    .Repeat() // Zip内のバッファをクリアする
    .SubscribeToText(text, x => text.text + x + "\n");

よく使われるオペレータ

Where

条件を満たすメッセージのみ通過させる
Javaでいうfilter

Select

要素の値を射影(変換)する
Javaでいうmap

SelectMany

新たなストリームを生成して,そのストリームが流すメッセージを
本流のストリームのメッセージとして扱う
JavaでいうflatMap

Throttle/ThrottleFrame

落ち着いた後に最後のメッセージを流す

ThrottleFirst/ThrottleFirstFrame

最初にメッセージが来てから一定時間遮断する

Delay/DelayFrame

メッセージの伝達を遅延させる

DistinctUntilChanged

メッセージが変化した時のみ通知する
同じ値が連続している場合は無視する

SkipUntil

指定したストリームにメッセージがくるまでメッセージをSkipする

TakeUntil

指定したストリームにメッセージ来たら,
自身のストリームにOnCompletedを流して終了させる

Repeat

ストリームがOnCompletedで終了した時にもう一度Subscribeを行う

SkipUntil + TakeUntil + Repeat

よく使う組み合わせ

イベントAが来てからイベントBが来るまでの間だけ処理したいような時に利用する

(例)ドラッグでオブジェクトを回転させる

UpdateAsObservable()
    .SkipUntil(OnMouseDownAsObservable()) // マウスがクリックされるまでストリームを無視する
    .Select(_ => new Vector2(Input.GetAxis("Mouse X"), Input.GetAxis("Mouse Y"))) // マウスの移動量をストリームに流す
    .TakeUntil(OnMouseUpAsObservable()) // マウスが離されるまで
    .Repeat() // TakeUntilでストリームが終了するため再Subscribe
    .Subscribe(move =>
    {
        transform.rotation =
            Quaternion.AngleAxis(move.y * _rotationSpeed * Time.deltaTime, Vector3.right) *
            Quaternion.AngleAxis(-move.x * _rotationSpeed * Time.deltaTime, Vector3.up) * transform.rotation;
    });

First

ストリームに最初にきたメッセージのみを流す
OnNextの直後にOnCompleteも流れる

First + Repeat で1回動作する度にストリームを作り直している

実例

ダブルクリック判定

// クリックストリームをキャッシュに入れる
var clickStream = UpdateAsObservable()
                    .Where(_ => Input.GetMouseButtonDown(0));

// ダブルクリック時の講読処理
clickStream.Buffer(clickStream.Throttle(TimeSpan.FromMilliseconds(200))) // 最後にクリックされてから200ミリ秒経過した時
    .Where(x => x.Count >= 2)
    .SubscribeToText(
        _text,
        x => string.Format("DoubleClick detected! \n Count:{0}", x.Count)
    );

値の変動の監視 (着地判定を例に)

通常の方法の一例

① CharacterController.isGroundedを毎フレーム監視
② 現フレームにおける値をフィールド変数に保存
③ 次フレームでFalse → Trueに変わったときにエフェクトを再生

public class OnGroundedWithoutUniRx : MonoBehaviour
{
    CharacterController _characterController;
    ParticleSystem _particleSystem;
    private bool oldFlag;

    private void Start() 
    {
        _characterController = GetComponent<CharacterController>();
        _particleSystem = GetComponentInChildren<ParticleSystem>();

        oldFrag = _characterController.isGrounded;    
    }

    private void Update()
    {
        var currentFlag = _characterController.isGrounded;
        if(currentFlag && !oldFlag)
        {
            _particleSystem.Play();
        }
        oldFlag = currentFlag;
    }
}

UniRxを利用した場合の着地検知

(例1)

f:id:xrdnk:20200314142551p:plain

public class OnGroundedScript : MonoBehaviour
{
    public override void Start()
    {
        var characterController = GetComponent<CharacterController>();
        var particleSystem = GetComponentInChildren<ParticleSystem>();

        UpdateAsObservable()
            .Select(_ => characterController.isGrounded)
            .DistinctUntilChanged()
            .Where(x => x)
            .Subscribe(_ => particleSystem.Play());
    }
}

(例2)ObserveEveryValueChangedを利用する場合
毎フレーム値の変動を監視する場合は「ObserveEveryValueChanged」の方がシンプルにかける

characterController
    .ObserveEveryValueChanged(x => x.isGrounded)
    .Where(x => x)
    .Subscribe(_ => Debug.Log("OnGrounded!"));

値の変動を丸める

isGroundedの変動を丸める

isGroundedは斜面を移動する場合,True/Falseが激しく変動する.
値の変動をUniRxで抑え込み,精度を改善させる

f:id:xrdnk:20200314142535p:plain

UpdateAsObservable()
    .Select(_ => playerCharacterController.isGrounded)
    .DistinctUntilChanged()
    .ThrottleFrame(5)
    .Subscribe(x => throttleIsGround = x);