UniRxについての記事のまとめはこちら
#Reactive Extensionsとは
ReactiveExtensions(Rx)とは、一言で言えば「イベントや非同期処理に対してLINQライクな処理を適用できるようにするC#のライブラリ」です。
リアクティブプログラミングについての説明は、あなたが求めていたリアクティブプログラミング入門を見てもらえるとわかりやすいかと思います。
Rxは本来であればUnityでは動作しないのですが、neueccさんがUniRxというUnityで動くRxを作ってくれました。
今回はそちらを触ってみたので書いたスクリプトを紹介したいと思います。
#値を通知するSubject<T>/画面にオブジェクトが写ったことを通知する
Subject<T>を使うことで、値を発行し通知することができます。
これはEventの上位互換の機能に当たり、EventArgsでパラメータを通知するのと同じように監視している対象に値を通知することができます。
それでは、Subject<T>を使って実際にカメラにオブジェクトが写った時に通知されるようにしてみます。
using UnityEngine;
using UniRx;
/// <summary>
/// GameObjectがカメラに写ったことを通知するスクリプト
/// </summary>
public class OnVisibleScript : MonoBehaviour
{
/// <summary>
/// カメラに写ったGameObjectを流すストリーム
/// </summary>
Subject<GameObject> onVisibleStream = new Subject<GameObject>();
/// <summary>
/// 外部に公開するObservable
/// </summary>
public IObservable<GameObject> OnVisibleObservable
{
get
{
return onVisibleStream.AsObservable();
}
}
/// <summary>
/// カメラに写った時に実行されるUnityのコールバック
/// </summary>
public void OnBecameVisible()
{
//OnNextで自分自身のGameObjectをストリームに流す
onVisibleStream.OnNext(this.gameObject);
}
}
using UnityEngine;
using UniRx;
/// <summary>
/// 対象を監視する側
/// </summary>
public class ObserveScript : MonoBehaviour
{
//観測対象のGameObject
public GameObject targetCube;
void Start()
{
//OnVisibleScriptを取得
var targetOnvisibleScript = targetCube.GetComponent<OnVisibleScript>();
//Subscribeで値を購読する
targetOnvisibleScript.OnVisibleObservable
.Subscribe(x => Debug.Log(x));
}
}
これでOnVisibleScriptが付与されたGameObjectがカメラに写った瞬間に、OnVisibleObservableに値が流れます。
OnVisibleObservableを監視しているObserveScriptがそれを検知し、Debug.Logが実行されます。
OnNext()が値を通知する、つまり「Eventを発火させる」のと同じ処理に当たります。
Subscribe()が従来のEventにおける「EventHandlerの登録」に相当すると思って下さい。
Eventを利用する場合はデリゲートを定義したり、EvetnArgsを定義したりと煩雑な処理が多かったですがUniRxを使えばSubjectを定義するだけととても簡潔に書くことができます。
#イベントを合成してみる/画面に同時に写った数を数えてみる
Subject<T>を使えばイベント通知を簡単に書けると言いましたが、それだけでRxは終わりません。
このイベント通知を合成・フィルタリング・射影など柔軟な処理を行うことができます。
例として、「カメラが移動して画面にオブジェクトが写った時、同時にいくつのオブジェクトがカメラに写ったかを数える」という処理を実装してみたいと思います。
まずは下準備としてOnVisibleScriptを貼り付けたTargetCubeをシーン上にいくつか配置し、CubesというGameObjectの子としてまとめておきます。
次に、先ほどのObserveScript.csを書き換えます。
using UnityEngine;
using System.Collections;
using System.Linq;
using UniRx;
using System;
public class ObserveScript : MonoBehaviour {
/// <summary>
/// TargetCubeを束ねるGameObject
/// UnityEditor上で設定しておく
/// </summary>
public GameObject cubes;
void Start()
{
//OnVisibleScriptをまとめて取得
var onVisibleScripts = cubes.GetComponentsInChildren<OnVisibleScript>();
//Mergeで複数のOnVisibleObservableを1つにまとめる
var allOnVisibleObservable = Observable
.Merge(onVisibleScripts.Select(x => x.OnVisibleObservable));
//250ms以内に画面にまとめて写ったGameObjectを数える
allOnVisibleObservable
.Buffer(allOnVisibleObservable.Throttle(TimeSpan.FromMilliseconds(250)))
.Subscribe(x => Debug.Log(x.Count));
}
}
以上です。
ほんの数行書き換えただけで、「同時に画面に写ったオブジェクトの数を数える」という複雑な処理が実装できました。
1つ1つ何をやっているか解説します。
まずObservable.Merge()を使い、複数のストリームを1本のストリームをallOnVisibleObservableとして合成しています。
//Mergeで複数のOnVisibleObservableを1つにまとめる
var allOnVisibleObservable = Observable
.Merge(onVisibleScripts.Select(x => x.OnVisibleObservable));
続いて、Bufferを使い、値の通知を塞き止めます。
Bufferではイベントがコレクションとしてまとめられていき、解除のイベントが来るまでずっと塞き止め続けます。
Buffer解除のイベントはThrottleを使い生成します。
Throttleは最後に値が来てから一定期間経過すると発火するというものです。
今回は最後に値が来てから250m秒以上経過したらBufferを解除するとしました。
(つまり、250ms以内に画面に写ったものは同時に写ったとカウントされる)
//250ms以内に画面にまとめて写ったGameObjectを数える
allOnVisibleObservable
.Buffer(allOnVisibleObservable.Throttle(TimeSpan.FromMilliseconds(250)))
.Subscribe(x => Debug.Log(x.Count));
この他にも、Whereで特定のイベントのみにフィルタリングをしたり、Selectで流れてくる値そのものを差し替えたりとRxを使うことでLINQライクにイベントを柔軟に扱うことができてしまいます。
学習コストは若干高いですが、使いこなすとメチャクチャ便利なので是非勉強して使いましょう!!