【Unityでも】UniRxを使ってみた。マウスクリックの回数を数える【リアクティブプログラミング】


はじめに

 「関数型言語を学ぶことは実務でどう役に立ったか」や、「iOSのSwiftとAndroidのGroovy」を読んで、RxJavaを使ってみたくなりました。その後、リアクティブプログラミング入門を読んで、RxJavaだけでなくリアクティブプログラミングに興味が出てきました。リアクティブプログラミングを正しく理解し、RxJavaやReactiveExtensionsを使いこなしてコーディングすることができれば、より読みやすいコードが書けると思います。

 UnityゲームエンジンはC#でコーディングができます。C#にはもともとReactiveExtensionsがありますね。しかし、UnityではReactiveExtensionsは動かないそうです。そこでneueccさんが、UnityでもReactiveExtensionsが使えるように、移植したのがUniRxだそうです。(作者neueccさんの、こちらのブログより)

 この投稿では、リアクティブプログラミング入門にある「何回クリックされたかを表すカウンタストリーム」の作成と利用を、UnityでUniRxを使ってやってみました。

 この投稿では、「リアクティブプログラミングとは何か」や、「FRPとは」、「ストーリームとは」などについては触れません。また、ObservableやIObservable<T>、Subect<T>、IObserver<T>の役割、メソッドについても触れません。

UniRxを使わず、マウスクリックの回数を数える

 UniRxを使わずに、マウスが左クリックされる度に今までクリックされた回数を表示します。

CountClickSample.cs(Updateでカウンタをインクリメントする)【再掲】
using UnityEngine;

public class ClickCounterSample : MonoBehaviour
{
    int clickCount; // クリック回数のカウンタ

    void Update () // Updateメソッドは、毎フレーム呼ばれる
    {
        if(Input.GetMouseButtonDown (0)) { // マウスが左クリックされた時にtrueなる
            clickCount++; // クリック回数のカウンタをインクリメント
            Debug.Log(clickCount); // クリック回数を表示
        }
    }
} 

 フレームをまたぐ処理ですので、フィールドにクリック回数のカウンタを持たせる必要がありますね。

UniRxを使って、「何回クリックされたかを表すカウンタストリーム」を作る

 今度は、UniRxを使って「何回クリックされたかを表すカウンタストリーム」を作ってみます。これを利用して、マウスが左クリックされる度に今までクリックされた回数を表示するコードを作ってみます。

EveryUpdateでイベントストリームを作る

 UniRxはReactiveExtensionsを移植したアセット(ライブラリ)です。

 しかし、この節で紹介するEveryUpdateはReactiveExtensionsにない、UniRx固有のメソッドのようです。ゲームエンジンであるUnityは、フレームという概念を持っています。MonoBehaviourというクラスのサブクラスのUpdateというメソッドは、毎フレーム呼ばれます。

 UniRx.ObservableクラスのEveryUpdateメソッドは、フレーム更新(Update)を司るストリームを生成します。

EveryUpdateの例
using UnityEngine;
using UniRx;

public class ClickCounterSample : MonoBehaviour
{
    void Awake ()
    {
        IObservable<long> updateStream = Observable.EveryUpdate();
        updateStream.Subscribe (count => Debug.Log (count)).AddTo(gameObject);
    }
}

 上記のコードでは毎フレーム、ログが出力されます。表示内容のcountはフレームごとにインクリメントされていきます。

Whereでフィルタリングする

 次のサンプルではクリックの検知をしています。

マウスの左クリックを検知
bool isLeftMouseClicked = Input.GetMouseButtonDown (0);

 マウスやトラックバッドの左クリックをしたフレームでは、isLeftMouseClickedがtrueになっています。これと先ほどのEveryUpdate、そしてWhereを使ってクリックのストリームを作ります。

 LINQ to ObjectsのWhereは要素のフィルタリングをします。UniLinqやReactiveExtensionsのWhereもストリームのフィルタリングをします。

Whereを用いたクリックストリームの例
using UnityEngine;
using UniRx;

public class ClickCounterSample : MonoBehaviour
{
    void Awake ()
    {
        IObservable<long> clickStream = Observable
            .EveryUpdate()
            .Where (_ => Input.GetMouseButtonDown (0)); // 左クリックしたフレームだけに

        clickStream.Subscribe (_ => Debug.Log ("Clicked!")).AddTo(gameObject);;
    }
}

 上記のコードを実行すると、マウス(トラックパッド)を左クリックするたびに、Clicked!と表示されます。

Selectで変換する

 LINQ to ObjectsのSelectは要素を変換しました。UniLinqやReactiveExtensionsのWhereもストリームの要素の変換をします。

ClickCounterSample.cs(UniRx版)
using UnityEngine;
using UniRx;

public class ClickCounterSample : MonoBehaviour
{
    void Awake ()
    {
        IObservable<int> clickStream = Observable
            .EveryUpdate()
            .Where (_ => Input.GetMouseButtonDown (0))
            .Select (_ => 1);

        clickStream.Subscribe (num => Debug.Log (num)).AddTo(gameObject);;
    }
}

 この投稿の目標は、「何回クリックされたかを表すカウンタストリーム」を作るでした。ここでやった1への変換は、あまり意味は無いように見えますが、次ぎのScanでクリックカウントストリームを作成するのための下準備です。

Scanでクリック回数のストリームに!

 LINQにAggregateというメソッドがあります。Aggregateは集約・集計するためのメソッドです。(個人的にはSumやMin、Maxに比べてあまり活躍する機会が多くない印象です。Java 8のminByやmaxByを、IEnumerable用に自作する際にAggregate使えそうだなーと思いましたが、実際に使ったことはないです。)

 繰り返しになりますが、Aggregateは集約・集計して、最後の結果の値を返すメソッドです。ところでAggregateでは集約・集計の途中の値はわかりません。

 ここで使うScanメソッドは集約・集計の途中の結果も利用できるメソッドです。

using UnityEngine;
using UniRx;

public class ClickCounterSample : MonoBehaviour
{
    void Awake ()
    {
        IObservable<int> clickCountStream = Observable.EveryUpdate()
            .Where(_ => Input.GetMouseButtonDown(0))
            .Select ( _ => 1)
            .Scan ((acc, current) => acc + current); // ここでは、accには前までのクリック数の累計が、currentには1が入っている

        clickCountStream.Subscribe (clickCount => Debug.Log (clickCount)).AddTo(gameObject);
    }
}

 Scanメソッドの引数のデリゲートに注目します。上記のコードでは、最初にクリックされたら、accは0でcurrentは1でデリゲートが呼び出されます。次にクリックされた時には、accは1でcurrentは1で呼び出されます。次はaccは2でcurrentは1で、その次はaccは3でcurrentは1で呼び出されます。コード中のコメントにもありますが、accには前までのクリック数の累計が、currentは1で呼び出されます。

 accには前回のデリゲートの結果が来ます。(前までの集約・集計途中結果)

 curretが1なのは、クリックストリームの要素をSelectで1に変換したものをScanしているからです。

 上記のコードでは「クリックのストリームはここで終わりにして打ち切る」という処理をしていません。そのためAggregateだといつまでたっても値の集約・集計結果が出てきませんね。しかしScanならば、「ここで終わり」とする前の集約・集計の途中でも処理をすることが可能です。

まとめ

 Updateでカウンタをインクリメントする元のコードと、UniRxを使って書いたコードを再掲します。

CountClickSample.cs(Updateでカウンタをインクリメントする)【再掲】
using UnityEngine;

public class ClickCounterSample : MonoBehaviour
{
    int clickCount;

    void Update ()
    {
        if(Input.GetMouseButtonDown (0)) {
            clickCount++;
            Debug.Log(clickCount);
        }
    }
} 
ClickCounterSample.cs(UniRx版)【再掲】
using UnityEngine;
using UniRx;

public class ClickCounterSample : MonoBehaviour
{
    void Awake ()
    {
        IObservable<int> clickCountStream = Observable.EveryUpdate()
            .Where(_ => Input.GetMouseButtonDown(0))
            .Select ( _ => 1)
            .Scan ((acc, current) => acc + current);

        clickCountStream
            .Subscribe (clickCount => Debug.Log (clickCount))
            .AddTo(gameObject);
    }
}

 元のコードと比べて、コード量的にはUniRxを使ったコードの方が(文字数的には)多く見えます。

 しかし長くはなりましたが、UniRxを使ったコードではint型のclickCountフィールドが必要なくなりました。このような単純で小さなコードでは、フィールドが無くなったメリッドは小さいかもしれませんが、もっと複雑な場合フィールドが無くなるメリットは大きいと思います。このフィールドはどのタイミングで更新されるとか、どこのコードで変わるかとかを、クラス全体を見ながら考える必要が無くなりますからね。

 もっと複雑なものも、UniRxを使って使ってみたいと思いました!

関連投稿