UniRx


最近になって、Reactive Extensions通称Rxというのが気になってたのですがUnityにもUniRxというのがあるので試してみた。
C#で実装されている機構なんだけどこの機能はLINQとの関連が深いのでまずはLINQをある程度理解していないといけない。
そもそもC#でデータの問い合わせをするにはLINQ以前は

IEnumerable<Character> q =
	from c in Characters
	where c.hp >= 100
	select c;

こんな感じでプログラムとは分離されたSQL的な書き方をしていたらしい。
それがLINQを使うと

IEnumerable<Character> q = this.Customers
				.Where(c => c.City == "London")
				.Select(c => c);

こんな感じで、見やすいメソッドになる。
データベースに限らずデータの集合(XMLやJSON)なども同じ機構で扱えるようにしたのがLINQ to XMLやらLINQ to JSONというものらしい。
C#のIEnumerable<T>の型に対してLINQが適応できて、例えばListのようなデータに対して偶数(Where)のものを3つ(Take)取得するとすると。

List<int> list = new List<int>(){1, 2, 3, 4, 5, 6, 7, 8, 9, 10};
IEnumerable<int> numsEnumerable = list.Where(_ => _ % 2 == 0).Take(3);
foreach (int num in numsEnumerable) {
	Debug.Log(num); // 2, 4, 6
}

こんな感じになる。
データの最大値を取得したり、平均を算出したりなどデータ群に対して抽出や操作ができる。
参考)
地平線に行く | LINQの拡張メソッド一覧と、ほぼ全部のサンプルを作ってみました。

C#の強みはLINQと言われているくらいなんだけど、どうもUnityではiOSに書き出した際に落ちることがあるらしい。詳細はこちら
Qiita | やっぱりUnityでもC#なんだからLINQが使いたい!

LINQはデータ群を扱う為のものですが、「イベントもタイムライン上で順番に並べるとそれも一つの配列と見なせ、LINQのような機構をつかってイベントをフィルタリングやデータを操作できますよね。」っていう変態的な考えを誰かが考えた!それがReactive Extensions。
JavaScriptですがこちらの記事がわかりやすい。
LIG Inc | 「RxJS」初心者入門 – JavaScriptの非同期処理の常識を変えるライブラリ

最初にMicrosoft DevLabsに2009年に登場したようでまだ新しい技術ですね。これがだんだんとjsやphpなどでも広がっていき言語を超えた機構として広まっているみたいです。

LINQもまったく使ったことがなかったので、LINQも含め幾つかのサンプルを作ってUniRxを試してみた。
(1つ落ちるサンプルがあるけど、なにか間違っているのかな…?)
あとは開発者さんのところで開催された勉強会の資料なんかも読むと良さそうです。

第一回UniRx勉強会を開催しました+スライドまとめ

LINQのSelectManyは理解に時間がかかったけど、こちらを参考に勉強した。
[C#・LINQ]九九だけじゃない!アプリ開発にもゲーム開発にも使える、SelectMany!
Rxの場合はSelectManyはストリームの流れを変えるようなときに使われるって感じでしょうか。

サンプルDL

[unitycsharp]
using UnityEngine;
using UnityEngine.UI;
using UniRx;
using UniRx.Triggers;
using System;
using System.Linq;
using System.Collections;
using System.Collections.Generic;

namespace unirxtest {

public class UniRxTest : MonoBehaviour {
/// <summary>
/// キューブ。
/// </summary>
[SerializeField] GameObject cubeGO;

/// <summary>
/// 小さなキューブ。
/// </summary>
[SerializeField] List<GameObject> smallCubes;

/// <summary>
/// 数字のリスト。
/// </summary>
List<int> list = new List<int>() {1, 2, 3, 4, 5, 6, 7, 8, 9, 10};

/// <summary>
/// データクラス。名前とスコア。
/// </summary>
class Data {
public string name;
public int score;
}

/// <summary>
/// データのリスト。
/// </summary>
List<Data> dataList = new List<Data>() {
new Data(){ name = "AAA", score = 100},
new Data(){ name = "BBB", score = 200},
new Data(){ name = "CCC", score = 300}
};

/// <summary>
/// Start.
/// </summary>
void Start () {
this.LinqSample01();
this.LinqSample02();
this.LinqSample03();
this.LinqSample04();
this.LinqSample05();
this.LinqSample06();
this.LinqSample07();

this.RxSample01();
this.RxSample02();
this.RxSample03();
this.RxSample04();
this.RxSample05();

// ここからはボタンを押したら発動
this.RxSample06();
this.RxSample07();
this.RxSample08();
this.RxSample09();
this.RxSample10();
this.RxSample11();
this.RxSample12(); // なぜか落ちる
this.RxSample13();
this.RxSample14();
this.RxSample15();
this.RxSample16();
}

/// <summary>
/// LINQのサンプル1。
/// Whereにて条件に合致する要素のみ抽出。
/// </summary>
void LinqSample01() {
foreach (int num in this.list.Where(_ => _ % 2 == 0)) {
print(num); // 2, 4, 6, 8, 10
}

// 2番目の要素(インデクサ[2]でのアクセスは出来ない)
print(this.list.Where(_ => _ % 2 == 0).ElementAt(2)); // 6
}

/// <summary>
/// LINQのサンプル2。
/// Selectにて要素を2倍にする写像を行った要素をList化。
/// </summary>
void LinqSample02() {
IEnumerable<int> numsEnumerable = this.list.Select(_ => _ * 2); // Selectで写像を行う
List<int> nums = numsEnumerable.ToList(); // ToList()にてListを作成

foreach (int num in nums) {
print(num); // 2, 4, 6, 8, 10, 12, 14, 16, 18, 20
}
}

/// <summary>
/// LINQのサンプル3。
/// 所定の条件の最初の要素のみ抽出。
/// </summary>
void LinqSample03() {
print(this.list.First(_ => _ > 3)); // 4
}

/// <summary>
/// LINQのサンプル4。
/// 3つスキップして、そこから8よりも小さい間続いている要素を抽出。
/// </summary>
void LinqSample04() {
foreach (int num in this.list.Skip(3).TakeWhile(_ => _ < 8)) {
print(num); // 4, 5, 6, 7
}
}

/// <summary>
/// LINQのサンプル5。
/// SelectManyを使って、射影を行った後の階層化されたシーケンスを直列に並べる。
/// </summary>
void LinqSample05() {
List<List<int>> lists = new List<List<int>>(){ this.list, this.list, this.list };

foreach (int num in lists.SelectMany(_ => _)) { // this.listに入っている数字が直列化され、1〜10が3回並ぶ
print(num); // 1〜10を3回出力
}
}

/// <summary>
/// LINQのサンプル6。
/// SelectManyの第2引数を使って、配列の子とその孫を使った写像の結果を直列に並べる。
/// </summary>
void LinqSample06() {
List<int> list2 = new List<int>() {11, 12, 13, 14, 15, 16, 17, 18, 19, 20};
List<int> list3 = new List<int>() {21, 22, 23, 24, 25, 26, 27, 28, 29, 30};
List<List<int>> lists = new List<List<int>>(){ this.list, list2, list3 };

foreach (string str in lists.SelectMany(
_ => _,
(l, num) => lists.IndexOf(l) + " => " + num.ToString()
)) {
print(str); // 0 => 1, 0 => 2, 〜 2 => 29, 2 => 30
}
}

/// <summary>
/// LINQのサンプル7。
/// x座標が最大のCubeを選択。
/// </summary>
void LinqSample07() {
GameObject maxXCube = this.smallCubes.OrderByDescending(go => go.transform.position.x)
.ThenBy(go => go.transform.position.y).First(); // 同じ値があればY座標で比較

print(maxXCube); // Cube3
}

/// <summary>
/// Rxサンプル01。
/// 1を出力。
/// </summary>
void RxSample01() {
Observable.Return<int>(1)
.Subscribe(_ => print(_)); // 1
}

/// <summary>
/// サンプル02。
/// 1-10の偶数を出力。
/// </summary>
void RxSample02() {
Observable.Range(1, 10)
.Where(_ => _ % 2 == 0)
.Subscribe(_ => print(_)); // 2, 4, 6, 8 10
}

/// <summary>
/// サンプル03。
/// 1-10の最初の2つのみを流し、1秒遅延させ出力。
/// </summary>
void RxSample03() {
Observable.Range(1, 10)
.Take(2)
.Delay(TimeSpan.FromSeconds(1))
.Subscribe(_ => print(_)); // 2, 4, 6, 8, 10
}

/// <summary>
/// サンプル04。
/// Updateを5回だけ受け入れる。
/// </summary>
void RxSample04() {
Observable.EveryUpdate()
.Take(5)
.Subscribe(_ => print("OK"));
}

/// <summary>
/// サンプル05。
/// Subscribeの第2引数(OnError)、第3引数(OnCompleted)のテスト。
/// </summary>
void RxSample05() {
// int[] nums = new int[]{100, 200, 300}; // onErrorテスト
int[] nums = new int[]{100, 200, 300, 400, 500}; // onCompletedテスト

Observable.Range(0, 5)
.Select(num => nums[num]) // 他の言語であればMap
.Subscribe(
_ => print(_), // OnNext
_ => {
Debug.LogWarning("Warning : " + _); // OnError
},
() => { // OnCompleted
print("OnCompleted");
}
);
}

/// <summary>
/// サンプル06。
/// ボタンを押した時のテスト。
/// </summary>
[SerializeField] Button sample06Button;
void RxSample06() {
sample06Button.OnClickAsObservable()
.Subscribe(_ => print("RxSample06"));
}

/// <summary>
/// サンプル07。
/// Observableを変数として保持し、ラムダ式とメソッドとの両方をSubscribeさせる。
/// また、クリックしたらラムダ式の方はイベントの購読の解除。
/// </summary>
[SerializeField] Button sample07Button;
void RxSample07() {
IObservable<Unit> button07Stream = sample07Button.OnClickAsObservable();
button07Stream.Subscribe(this.RxSample07Test); // メソッドのSubscribe

IDisposable subscription = Disposable.Empty; // Subscribe()の戻り値IDisposableの宣言。下の代入に書くとラムダ式内で見つからないと怒られる。
subscription = button07Stream.Subscribe(_ => { // ラムダ式のSubscribe
print("RxSample07");
subscription.Dispose(); // 購読の解除
});
}
void RxSample07Test(Unit _) {
print("RxSample07Test");
}

/// <summary>
/// サンプル08。
/// 2回クリックされたら出力させる。
/// </summary>
[SerializeField] Button sample08Button;
void RxSample08() {
this.sample08Button.OnClickAsObservable()
.Buffer(2)
.Subscribe(_ => print("2 Clicked!"));
}

/// <summary>
/// サンプル09。
/// Observableを変数として保持し、ラムダ式とメソッドとの両方をSubscribeさせる。
/// また、クリックしたらラムダ式の方はイベントの購読の解除。
/// </summary>
[SerializeField] Button sample09_01Button;
[SerializeField] Button sample09_02Button;
void RxSample09() {
this.sample09_01Button.OnClickAsObservable()
.Zip(this.sample09_02Button.OnClickAsObservable(), (button01Param, button02Param) => { print("Clicked!"); return 1; })
.First() // 1度メッセージがきたらOnCompleteを呼びストリームを終える
.Repeat() // OnCompletedで終了した際にもう1度Subscribeを行う
.Subscribe(_ => print("Subscribe Again"));
}

/// <summary>
/// サンプル10。
/// ダブルクリック(200msで2回以上のクリック)の検出。
/// </summary>
[SerializeField] Button sample10Button;
void RxSample10() {
IObservable<Unit> button10Stream = this.sample10Button.OnClickAsObservable();

button10Stream.Buffer( // 引数の条件までイベントを貯める
button10Stream.Throttle(TimeSpan.FromMilliseconds(200)) // 最後のクリックから200ミリ秒経過した時
)
.Where(_ => _.Count >= 2) // _ にはクリックの回数だけUnitが作られ、List<Unit> が入っている
.Subscribe(_ => print("Double (or more) Clicked!"));
}

/// <summary>
/// サンプル11。
/// クリック数のカウント。
/// </summary>
[SerializeField] Button sample11Button;
[SerializeField] Text sample11ButtonText;
void RxSample11() {
this.sample11Button.OnClickAsObservable()
.Select(_ => 1) // ストリームの値を1に変換
.Scan((total, one) => total + one) // 変換した1をScanにより、totalへ集計していく
.SubscribeToText(this.sample11ButtonText, _ => _.ToString()); // Textへ表示
}

/// <summary>
/// サンプル12。
/// * なぜか落ちる!
/// 押している間ログを出力。ドラッグなどに応用可。
/// </summary>
[SerializeField] Button sample12Button;
void RxSample12() {
this.sample12Button.OnPointerDownAsObservable() // マウスダウンのストレームを生成
.SelectMany(evt => this.sample12Button.OnUpdateSelectedAsObservable()) // SelectManyにて流すストリームを変える(厳密には違う)
.TakeUntil(this.sample12Button.OnPointerUpAsObservable()) // マウスアップが起こるまで
.Repeat() // OnCompleted時に再度ストリームを購読
.Subscribe(_ => print("Press"));
}

/// <summary>
/// サンプル13。
/// Web上からテキストデータをタイムアウト付きで取得して取得したデータの100文字を出力。
/// </summary>
[SerializeField] Button sample13Button;
void RxSample13() {
this.sample13Button.OnClickAsObservable()
.First() // 最初のクリックのみ
.SelectMany(ObservableWWW.GetWWW("http://google.co.jp/").Timeout(TimeSpan.FromSeconds(3)))
.Select(www => www.text.Substring(0, 100))
.Repeat() // onCompleted後再度ストリームを購読
.Subscribe(
_ => print(_), // OnNext
_ => Debug.LogError("Error Occurerd : " + _) // OnError
);
}

/// <summary>
/// サンプル14。
/// Web上からテキストデータをパラレルでダウンロードし揃ったら取得したデータの100文字を出力。
/// </summary>
[SerializeField] Button sample14Button;
void RxSample14() {
this.sample14Button.OnClickAsObservable().
Subscribe(_ => {
Observable.WhenAll(
ObservableWWW.Get("http://google.co.jp/"),
ObservableWWW.Get("http://yahoo.co.jp/"),
ObservableWWW.Get("http://japan.unity3d.com/")
)
.Subscribe(
texts => { // OnNext
foreach (string s in texts) {
print(s.Substring(0, 100));
}
},
e => Debug.LogError("Error Occurerd : " + e) // OnError
);
}
);
}

/// <summary>
/// サンプル15。
/// 使用しているオブジェクトがDestroy()された場合にnullエラーにならないようAddToを使って
/// Destroy()と同時にDispose()させる。
/// </summary>
[SerializeField] Button sample15Button;
void RxSample15() {
this.sample15Button.OnClickAsObservable()
.Subscribe(_ => {
Observable.EveryUpdate()
.Scan((total, __) => total + 1)
// .TakeUntilDestroy(this.cubeGO) // AddToではなくこちらだとOnCompletedが呼ばれる
.Subscribe(total => {
if (total >= 60) Destroy(this.cubeGO);
this.cubeGO.transform.Rotate(new Vector3(0f, 1f, 0f));
})
.AddTo(this.cubeGO);
}
);
}

/// <summary>
/// サンプル16。
/// プロパティの変更を購読して指定のTextに値を入れる。
/// </summary>
[SerializeField] Button sample16Button;
[SerializeField] Text sample16ButtonText;
ReactiveProperty<int> sample16Count = new ReactiveProperty<int>(0); // 変更をストリームとして流せるプロパティ。
void RxSample16() {
this.sample16Button.OnClickAsObservable() // ボタンをクリックでカウントをアップ
.Subscribe(_ => {
this.sample16Count.Value += 1;
});

this.sample16Count.Select(_ => "Count : " + _.ToString()) // 写像にてカウントに「Count : 」を付け加える
.SubscribeToText(this.sample16ButtonText); // カウントの変更を受けてTextを更新
}
}

}
[/unitycsharp]

コメントを残す

メールアドレスが公開されることはありません。 * が付いている欄は必須項目です