AizuVR blog

会津大学VR部の部員が持ち回りで投稿していくブログです。特にテーマに縛りを設けずに書いていきます!

「面白法人カヤックVR分室」としても活動しています。詳細はこちら

UniRxでUpdateを好きな場所に書いて使おう

学部2年のユムルです。

今回はAizuVR部で使っていく可能性のあるUniRxについて話します。

UniRxとは

C#にはRx(Reactive Extensions)という標準ライブラリが存在します。
元々Unityでは使えなかったのですが、neueccさんがUnityでもRxを使える様に開発したものがUniRxです。

UniRxが得意なものは、「何か処理が起こった時に別の処理をする」という処理です。
別名をイベントハンドラーと言います。
イベントとは、プログラムにおけるあらゆる動作のことを指します。
このイベント処理を、自分の好きな場所にわかりやすく書くことができるのがUniRxの利点です。
UniRxのイベント処理について具体的にどう便利なのかは下のサイトを参照して欲しいです。
今回は深く触れないので。
UniRxを導入するメリット ~こういう時にUniRxは使えるよ~

また、UniRxを使うことで別の大きな利点があります。
Updateを好きなところに書いたり、数秒後に特定の処理をするとか、数フレーム毎に処理をするという様な処理が好きな場所に簡単に書けることです。
僕は普段からUniRxを利用しますが、その理由としてはこの側面がとても強いです。
他の機能は理解して利用するのが難しいですが、今回はその内容には深く触れず、UniRx初心者でもすぐに使いこなせる機能を紹介します。

UniRxの導入

UniRxはAssetとして公開されています。
使うときは以下のページからインポート assetstore.unity.com

そして、コード上ではUniRxとUniRx.Triggersをusingしてください。

using UniRx;
using UniRx.Triggers;

Updateを操る

UniRx初心者が真っ先に使いこなせる様になるであろうUpdateに関する機能について話します。
まずお試しにcsファイルを作成。namespaceのusingをしっかりと書いてください。Updateメソッドはいりません。消してしまいましょう。

Startメソッドの中にUpdate処理を書きます。
プリミティブのキューブを作成して、右に移動させます。

void Start() {
    var cube = GameObject.CreatePrimitive(PrimitiveType.Cube);
    cube.transform.position = new Vector3(-4f, 0f, 0f);

    this.UpdateAsObservable()
        .Subscribe(_ => {
            cube.transform.Translate(Vector3.right * Time.deltaTime);
        });
}

StartメソッドだけでCubeを動かすスクリプトが書けました。
f:id:aizu-vr:20180602115549g:plain
見やすさの為、Lightingの設定を変えています。
さて、UniRxでは、この様なメソッドを繋げた書き方をします。
このメソッドの流れの中を、 イベントの情報が流れると思ってください。
そして、UpdateAsObservableは空のイベントを毎フレーム発生させるということです(厳密には発行すると言います)
thisから始まるのは、これを書いたクラスを元にUpdate処理をさせているということです。処理はGameObject依存です。
Subscribeでは、イベントが到達した時に行われる処理を登録しています。
Subscribe(=購読)という名前の意味はRxのイベントの取り扱い方から来ています。
イベントを「発行と購読」と、雑誌の様な取り扱いをしていますね。

Subscribeで処理を渡すと言いましたが、C#には関数をオブジェクトとして扱う「デリゲート」というものと、=>の記号から、左辺を引数、右辺を処理(と返り値)という様に関数を生成する「ラムダ式」という書き方があります。
デリゲートでは変数名は指定せずに、引数や返り値の型を指定して関数をオブジェクトとして扱います。
ラムダ式では、左辺に変数名を指定しつつ、右辺に処理を書きます
今回は左辺には一つ空のイベントを表す引数(Unit型)が入って来ますが、変数名は基本_をつけときます。
取り敢えずは_ => 処理という書き方で処理を渡せるということがわかればOKです。
上の例では、=>の右辺に毎フレーム実行させる処理を書きました。


この様に、UniRxを使うとUpdate処理を好きな場所に書くことができます。 上の例でも既にやっていることですが、ローカル変数をまるでクラス変数を扱うかのごとく、使えます。
もっとわかりやすく、実用的な例としては、時間を扱う処理でしょう。

var sTime = Time.time;
this.UpdateAsObservable()
    .Subscribe(_ => {
        var time = Time.time - sTime;
        cube.transform.localScale = Vector3.one * time;
        if (time >= 3f) {
            sTime = Time.time;
        }
    });

この処理は3秒毎に基準となる時間を設定し、そのタイミングからの経過時間をCubeの大きさに反映させたものです。
f:id:aizu-vr:20180602115624g:plain
この映像はgifでループされていますが、実際にもこの様に見えます。

この様にUniRxメソッドの中で宣言した変数に状態を持たせてUpdate処理を行うことができるということが確認できました。
従って、ちょっとした処理でクラス変数に変数名を迷いながらつけたり、処理がほとんどない小さなクラスを作らなくても良くなったでしょう。

Updateの書き方はもう一つありまして、

Observable.EveryUpdate()
    .Subscribe(_ => {

    }).AddTo(gameObject);

で書くことができます。
前の書き方ではそのままで自身のgameObjectに依存していましたが、
こちらではAddToを書く必要があり、gameObjectを引数に渡すことで、そのGameObjectが存在している間だけUpdateが働きます。
AddToを書き忘れると、GameObjectが消えても動き続けます。
こちらの書き方が優れているのは、先ほどの書き方に比べてパフォーマンスがいいということです。
大量の個別のUpdate処理を回す場合はこちらを使うといいでしょう。


また、UniRxでのUpdateは一定時間処理を回すとか、特定の条件を満たすまで処理を回すとか、コントロールが良く効きます。
次はそれらを見てみましょう。
一定時間だけ処理をする例です。

cube.transform.position = Vector3.down * 3f;
this.UpdateAsObservable()
    .Take(System.TimeSpan.FromSeconds(3f))
    .Subscribe(_ => {
        cube.transform.Translate(Vector3.up * 2f * Time.deltaTime);
    });

f:id:aizu-vr:20180602115927g:plain
実際に使う場合は、Systemをusingするとタイプ数が減ります。
Takeは、SystemのTimeSpanで設定された時間だけイベント情報を通します。
TimeSpanにはFromSecondsの他にも、FromMilliseconds、FromMinutes等があります。
int型の数値を入れるとフレーム数指定になります

Takeを使うときは、終了時に別の処理を走らせることができます。

this.UpdateAsObservable()
    .Take(TimeSpan.FromSeconds(時間))
    .Subscribe(_ => {
        // Update処理を入れる
    }, () => {
        // 終了時の処理を入れる
    });



特定の条件を満たすまで処理を回したい場合は、
先ほどのTakeをTakeWhileに変えてやり、中に真偽を返すラムダ式を入れます。

.Take(System.TimeSpan.FromSeconds(3f))

これを

.TakeWhile(_ => {
    return !Input.GetKeyDown(KeyCode.Space))
})

これでスペースキーが押されるまで処理を回す様にできました。
ラムダ式の右辺は1文なら括弧はいりません。

.TakeWhile(_ => !Input.GetKeyDown(KeyCode.Space))



さて、UniRxではUpdateを好きなところに書くことができると言いましたが、その利点は処理がわかりやすくなるだけではありません。
普段、Update内でGetKeyDownなどをトリガーに処理をすることがあると思いますが、そこから新たなUpdate処理も走らせることができるということです。

var sphere = GameObject.CreatePrimitive(PrimitiveType.Sphere);
this.UpdateAsObservable()
    .Where(_ => Input.GetKeyDown(KeyCode.R))
    .Subscribe(_ => {
        var sTime = Time.time;
        var maxTime = 0.5f;
        var stop = false;
        this.UpdateAsObservable()
            .TakeWhile(_2 => !stop)
            .Subscribe(_2 => {
                var t = (Time.time - sTime) / maxTime;
                if (t >= 1) {
                    t = 1;
                    stop = true;
                }
                var rad = t * Mathf.PI * 2f;
                sphere.transform.position = new Vector3(
                    -Mathf.Sin(rad),
                    -Mathf.Cos(rad),
                    0f);
        });
    });

f:id:aizu-vr:20180602160615g:plain
Where句はラムダ式の右辺の真偽値が真の時のみイベント情報を流します。
Rキーが押された時に一度Subscribeで登録された処理が走ります。
このタイミングで別のUpdate処理を走らせて、0.5秒間球を回しています。

ちなみに、上で経過時間と指定された実行時間の割合を取って処理を回していますが、 僕はこの処理を沢山するので、普段は下のようなメソッドを使っています。

// 一定時間のアニメーション処理。(addToをつけないオーバーロードも存在)
IDisposable PlayAnimation(float actTime, Action<float> action, GameObject addTo) {
    var sTime = Time.time;
    var stop = false;
    return Observable.EveryUpdate()
        .TakeWhile(_ => !stop)
        .Subscribe(_ => {
            var t = (Time.time - sTime) / actTime;
            if (t >= 1) {
                t = 1;
                stop = true;
            }
            action(t);
        }).AddTo(addTo);
}

先ほどのアニメーションの例を書き直すと、

this.UpdateAsObservable()
    .Where(_ => Input.GetKeyDown(KeyCode.R))
    .Subscribe(_ => {
        PlayAnimation(0.5f, t => { // ここで't'はfloat, (経過時間 / 処理して欲しい時間)
            var rad = t * Math.PI * 2f;
            sphere.transform.position = new Vector3(
                -Mathf.Sin(rad),
                -Mathf.Cos(rad),
                0f);
        }, sphere);
    });


一定時間後に処理&一定時間の繰り返し

一定時間後に処理をしたい場合は、Timerというものが使えます。

var renderer = cube.GetComponent<Renderer>();
Observable
    .Timer(TimeSpan.FromSeconds(3f))
    .Subscribe(_ => {
        renderer.material.color = Color.blue;
    });

3秒後に色が青に変わります。
f:id:aizu-vr:20180602164420g:plain

処理を繰り返したい場合は、Timerの
引数1に最初に実行するまでの時間
引数2に間隔
を入れることで、処理を繰り返すことができます。

Observable
    .Timer(TimeSpan.FromSeconds(1f), TimeSpan.FromSeconds(1f))
    .Subscribe(_ => {
        // ここでusing Systemしていると、Randomが短く打てません。
        renderer.material.color = new Color(
                UnityEngine.Random.value,
                UnityEngine.Random.value,
                UnityEngine.Random.value);
    }):

もしくは、Intervalを使うことでも、処理を繰り返すことができます。

.Interval(TimeSpan.FromSeconds(1f))

これで1秒毎にランダムに色が変わります。
f:id:aizu-vr:20180602164911g:plain

UniRxの勉強として

ます最初に、これからUniRxを勉強する人のために少し補足をします。
今回の記事では、イベント情報という単語を用いて説明していましたが、
イベント情報の発行から購読(=Subscribe)までの流れを「ストリーム」と呼びます。
また、メソッドを繋げて書く書き方は、「メソッドチェーン」と言います。
WhereとかTakeとか、イベント処理をするメソッドを「オペレータ」と言います。

次に、僕がUniRxを勉強するのに使ったサイトを紹介します
UniRxについて書いた記事をまとめてみた
まあ、UniRxと検索して一番上にでてくるページの人のところなのですが

あと逆引き便利
UniRx オペレータ逆引き


最後に、僕がUniRxを勉強するときにはまった点を紹介します。
今回は毎フレーム流れてた空のイベントを利用していましたが、UniRxでは自分で自由にストリームを流すことが出来ます。
僕がはまったのは、その流し方を間違えてしまった所です。
UpdateAsObservableを元に、条件を満たしている間ストリームを発行するようにしたところ、逆に条件を満たしていない時の動作を設定できなくなってしまいました。
そもそも、ストリームを使う必要がありませんでした。
bool型で保持しておけば良かったのです。
ストリームを発行するのは、トリガー的(ある動作を起こしたい状態になった等)なタイミングで使うのがやりやすいかと思います。

最後

今回、UniRxの機能の一部を取り上げました。
Updateをうまく操れる様になったでしょうか。
なっていたら幸いです。

ここまで読んでくださってありがとうございました。

会津大学VR部の部員が持ち回りで投稿していくブログです。特にテーマに縛りを設けずに書いていきます!