A Beginners Guide to Reactive Extensions with UniRx

どうも始めましての人は始めまして、@neueccといいます。この記事はUnity アセット真夏のアドベントカレンダー 2014 Summer!というイベントの23日目です。クリスマスのアレ!真夏に……!しかしクリスマスのアレは比較的脱落も少なくのないのですが、これは見事ーに続いてます。しかも日付が変わった瞬間に公開されることの多いこと多いこと。〆切というのは23:59:59だと思っている私には辛い話です……。さて、前日はnaichiさんの【うに部屋】Unityのゲーム投稿サイトにアセット検索機能を付けてみたでした。便利でいいですねー、UniRxも使ったアセットとして沢山並ぶ日が来ると、いいなぁ。

Reactive Programming

とは。と、ここで7/30に行われた【第1回】UnityアセットまみれのLT大会で使ったスライドが!

Reactive Programming by UniRx for Asynchronous & Event Processing from Yoshifumi Kawai

LTということで制限時間5分だったんですが当然終わるわけなくて凄まじく早口でまくしたてて強引に終わらせたせいで、全くワカラン!という感想を頂きましたありがとうございますごめんなさい。簡単にかいつまみますと、

Reactive Programmingはガートナーのハイプサイクル(記事では2013ですがこないだ出た2014年版のApplication DevelopmentでもOn the Riseに入っています)やThought Works Technology Raderといった有名な技術指標にもラインナップされるほど、注目を浴びている技術です。Scala周辺からもThe Reactive Manifestoといった文章が出ていますし、JavaでReactive Programmingを実現するRxJavaはGitHubのStarが2995、Objective-C用のReactiveCocoaは5341、JavaScriptでもRxJSが1792、bacon.jsが2864と、知名度・注目度、使われている度は非常に大きくなっています。

Reactive Programming自体は別に近年始まったわけでもなく、昔からたまに盛り上がっては消え、って感じなので、「へぇ~この技術2年前くらいに流行ってたよね 2年前くらい前に見たわ」って思う人もいるかもですが、大事なのは、ちゃんと実用に乗った、ということです。実験的なライブラリの段階はとうに超えて、RxJavaやReactiveCocoaの知名度が示す通り、完全に実用レベルに乗りました。

UniRxはReactive Extensions(Rx)というMicrosoftの開発した.NET用のライブラリ(現在はOSS化)を私がUnity用に移植したものです。現在のReactive(Rx)Hogeの源流は、この.NETのRxにあります。ほとんどのライブラリから言及され、原理原則や用語はRx.NETに従っていることが多い。というわけでRx.NETは革命的に素晴らしいわけなのです、が、しかし、Unityでは動きません。それはRx.NETが本来のC#の機能を全面的に使いすぎてUnityのC#では動かせないから……。少なくともiOSのAOT問題を全く突破できない……。

が、どうしてもUnityで使いたいので移植(といってもソースコードレベルではほとんど自前で書いてるのでインターフェイスと挙動を合わせているという感じで割と書き下ろしです、一部は純粋に移植してますが)+UnityはUnityで.NETとは異なるところもあるので、Unityで使って自然になるような改良を施したのがUniRxになります。

ムリョーですよ、ムリョー。FREE!。私はLINQやRxの大ファンなんで、とにかく使ってもらいたい欲求のほうが強くて。そして勿論、自分で使えない状態にも耐えられなくて!気になったらGitHubでStarつけてください(Star乞食)、勿論AssetStoreのほうでもいいですよ:)

UniRxは非同期やイベント処理をReactive Programmingの概念を元に大きく簡単にします。Unityにも非同期ライブラリ、イベントライブラリは沢山あります。それらと比べたUniRxの強みは「Rxであるということ」です。Reactive Programmingは現状かなりブームになっているとおりに、その手法の正しさ、威力に関しては実証済みです。また、手法やメソッド名などが同一であるということは、既に普及しているRx系のライブラリのドキュメントがまんま使えます。そしてUniRxで学んだやり方は他のプラットフォームに移っても同じように使えるでしょう。ネイティブAndroid(RxJava)でもネイティブiOS(ReactiveCocoa)でも.NET(Rx.NET)でもJavaScript(RxJS)でも、そういった先々への応用性もまた、選ぶべき理由になると思います。

Introduction

前置きが長い!さて、この記事を出すちょっと前に【翻訳】あなたが求めていたリアクティブプログラミング入門という素晴らしい記事が翻訳されました!はてブでも500以上集まってましたし、実際めっちゃ良い記事です。この記事はRxJSを用いて具体的な説明を行っていますが、勿論UniRxでも同様のことができます(というわけで、結論んとしては↑の記事読んでもらえればいいんでさー、とか投げてみたい)。ちょっとやってみましょう。最初の例はダブルクリックの検出です。

using System;
using UniRx;
using UnityEngine;

public class Intro : MonoBehaviour
{
    void Start()
    {
        // 左クリックのストリーム
        var clickStream = Observable.EveryUpdate()
            .Where(_ => Input.GetMouseButtonDown(0));

        // Buffer:250ミリ秒以内に連続してクリックされたものをまとめる
        clickStream.Buffer(clickStream.Throttle(TimeSpan.FromMilliseconds(250)))
            .Select(xs => xs.Count) // 250ミリ秒以内検出したクリック数
            .Where(x => x >= 2) // 2個以上のみにフィルタ
            .Subscribe(_ => Debug.Log("ダブルクリックされた!")); // foreachみたいな
    }
}

このスクリプト(Intro.cs)をMainCameraでもなんでも適当な何かに貼り付けてもらえれば、画面のどこでもダブルクリックされればログに流れます。

この例は6行しかない単純なものですが、多くの要素が詰め込まれています!そして、実際、詰め込まれすぎていてかなり難しい!わからん!というわけで真面目に分解していきます。

Rx is LINQ

Rxについてまとめると「時間軸に乗るストリームに対するLINQ」です。時魔法です。先の記事でも「FRPは非同期データストリームを用いるプログラミングである」といってました。(ドヤァするわけですが、私は遥か昔、2011年の時点で同じこと言ってましたからね!)。このことさえピンと来れば割としっくり来るんですが、同時にこれがしっくり来るというところまでが敷居となる。ところが、RxはこのことをLINQとして表現することによりグッと敷居を下げました。なんだ、LINQと一緒じゃん!って。え、LINQがワカラナイ?それは、普通にC#の必須技術なので是非学んでください!(色々種類ありますがLINQ to Objectsだけでいいです。以前に@ITでLINQの仕組み&遅延評価の正しい基礎知識を書いたりAn Internal of LINQ to Objectsというスライドで発表したりしてるんで読んでください)

簡単に同じように見れることを説明すると、LINQ to Objectsでは

new[] { 1, 2, 3, 4, 5 }
    .Where(x => x % 2 == 0)
    .Select(x => x * x); 

のように、配列をフィルタリングして別の形に射影できます。

配列、int[]の横軸は当然ながら長さです。Rxは時間を横軸に取ることができます。どういうことか、というと、例えば何かをタップするというイベントは図にしたらこういう表現ができます。

ということは、同様にWhereしたりSelectしたりできる。

少しピンと来ました?さて、配列はnew[]{}やGetComponentsなどで手に入れることができますが、Rxで扱うためのイベントストリーム(IObservable<T>、ちなみにLINQ to Objectsは配列をIEnumerable<T>として扱う)はどこに転がっているのか。UniRxにおいて一番お手軽なのはObservable.EveryUpdateです。ゲームループは、ループというぐらいに1フレーム毎にUpdateが毎回呼ばれるサイクルなわけですが(参考:Script Lifecycle Flowchart)、つまり60fpsなら1/60秒毎に発生する時間軸にのったイベントとみなせられます。一度、時間軸上に乗るイベントとみなせられれば、それは全て無条件にRxで取り扱えます。

実際のところ、イベントに限らずあらゆるものがRxに見せかけることができます。非同期だってx秒後に一度だけ発生するイベントと考えれば?配列は0秒で沢山の値が発行されるイベントと考えれば?Unityのコルーチン(IEnumerator)だって乗せられる。

全てのものをRx化(IObservable化)すれば、あとは好きなようにLINQのメソッドで合成してしまえる。あらゆる素材(イベント・非同期・配列・コルーチン)を鍋(IObservable)に突っ込んで、料理(Where, Select, etc...)して食べる(Subscribe)。というのがRxの基本の基です。料理方法は色々あるので、そこが次のキモですね。

Composable

というわけでclickStreamがマウスの左クリックのストリームになったということは分かったでしょうか!?

var clickStream = Observable.EveryUpdate()
    .Where(_ => Input.GetMouseButtonDown(0));

左クリックがあったフレームだけにフィルタリングしているということですねー。では次は?

clickStream.Buffer(clickStream.Throttle(TimeSpan.FromMilliseconds(250)))

Rxの利点として、イベントが変数として扱えることです。どういうことか、戻り値にすることもできるし、フィールドやプロパティとして公開することもできるし、自分自身と結合することもできる。というわけで、clickStreamが二個出てるのは、自分との合成なんですね(わっかりづらい!)

そこまではいいとしてThrottleが分かりづらい!Throttleは一定時間毎(この場合は250ミリ秒)に値が観測できなかったら値を流す、という挙動です。

スロットル、流れてくる値を絞り込んでいるんですね。Rxは時間軸上に乗っているので、こうした時間関係を扱うメソッドが豊富です。Sample(一定時間毎のもののみを流す)、Delay(一定時間後に流す)、Timeout(一定時間たっても値がなければエラーにする)、Buffer(一定時間の間値を溜めてから流す)などなど。一見分かりづらいですが、そもそも普通にも書きづらい、そういったものがメソッド一発で書けるというのもRxの魅力です。

じゃあどういうことかというと、もうまだるっこしいので全部のせますが

ThrottleしたものをBufferしているのは、値が流れてくるまで値を溜める(そして配列にして後続に流す)、ということです。あとはSelectで配列の個数、つまり250ミリ秒の間にクリックされた回数に変形して、Whereでフィルタリング(ダブルクリック、つまり2個以上ならばOK)。なるほどねー?

ちなみにコレは(250ミリ秒以内に)クリックされ続けてるとずっと実行されません。最後にクリックされてから250ミリ秒後に、その間に2回以上クリックされてると実行される。が正しい表現でしょうか。この動作が望ましい場合もあれば望ましくない場合もある、ダブルクリックと一口でいっても定義は案外複雑なので、その辺はチューンしてみてください。その辺もclickStream.Timestamp()や.TimeInterval()でその時刻や前との差分なんかが簡単に取れます。

Subscribeって?

Subscribeはforeachです。配列をforeachで消費するように、RxではSubscribeで消費する。イベントストリームなのだからeventのSubscribe(購読)。foreachしなければ配列は動かないように、RxもSubscribeしなければ始まりません!(というのは正確には語弊があり、ObservableにはHotとColdという性質があり、HotはSubscribeしなくても動いている、とかかんとか、とかがありますが今は無視します)。RxにおけるSubscribeは、値・エラー・完了を一手に受け取ります。

Observable.EveryUpdate() // 0, 1, 2, 3, 4, 5, 6, 7, 8,....
    .Take(5)
    // .Do(x => { if(x == 3) throw new Exception(); } )
    .Subscribe(
        x => Debug.Log(x + "Frame"), // OnNext
        ex => Debug.Log("Exception!" + ex), // OnError
        () => Debug.Log("Complete!")); // OnCompleted

通常、イベントに終わりはありませんが、Rxのイベントストリームは終わりを持たせることができます。Takeは値をx個取得したら強制的に終点ということにするもの。長さ5のイベントストリーム。というわけで↑のコードはOnNextが5回呼ばれた後にOnCompletedが呼ばれます。もしDo(値が通った時にメソッドを実行する)のコメントアウトを外すと、OnNextが3回呼ばれた後にOnErrorが呼ばれます。この場合はOnCompletedは呼ばれません。なお、OnNext/OnError/OnCompletedはどれも書いても書かなくてもいいです(その場合はOnErrorはグローバルに例外をそのままthrow、OnNextとOnCompletedは何もしない、ということになる)

Rxのイベントストリームは以下の原則に必ず従います。これは他のRx系列のReactive Programmingライブラリも同じものを採用しています。

OnNext* (OnError | Oncompleted)?

OnNextが0回以上呼ばれた後に、OnErrorもしくはOnCompletedが1回または0回よばれます。

購読ということは解除(Unsubscribe)はあるのか、というと、あります!Subscribeの戻り値は必ずIDisposeableで、それをDisposeすることが解除になります。

var subscription = Observable.Interval(TimeSpan.FromSeconds(1))
    .Subscribe(_ => Debug.Log("hogehoge!"));

Observable.EveryUpdate()
    .Where(_ => Input.GetMouseButtonDown(0))
    .Take(1)
    .Subscribe(_ => subscription.Dispose());

Intervalはx秒毎に値を発行するというもの、↑の例では1秒おきにhogehoge!と表示されます。もし左クリックがあったら、その戻り値をDisposeしているので、これで値の発行は止まります。Take(1)なので、左クリックを監視するストリームもTake(1)が終わったら自動的にEveryUpdateの監視を解除しています。

var subscription = Observable.Interval(TimeSpan.FromSeconds(1))
    .Subscribe(_ => Debug.Log("hogehoge!"));

Observable.EveryUpdate()
    .Where(_ => Input.GetMouseButtonDown(0))
    .Take(1)
    .Subscribe(_ => subscription.Dispose());

この形式の何がいいか、というと、入れ物に入れてまとめて購読解除できます。

CompositeDisposable eventResources = new CompositeDisposable();

void Start()
{
    Observable.Interval(TimeSpan.FromSeconds(1))
        .Subscribe(_ => Debug.Log("hogehoge!"))
        .AddTo(eventResources);

    Observable.EveryUpdate()
        .Where(_ => Input.GetMouseButtonDown(0))
        .Subscribe(_ => Debug.Log("click!"))
        .AddTo(eventResources);
}

void OnDestroy()
{
    eventResources.Dispose();
}

CompositeDisposableはIList[IDisposable]みたいなもので、IDisposableをまとめて突っ込めます。AddToはメソッドチェーンのまま突っ込めるようにするUniRxの定義している拡張メソッド。こうしてためておいて、Destroyでまとめて解除、ができます。こうした変数で扱えるという性質により、イベントの管理がかなり容易になっています。勿論、文字列で止めて、などもない完全なタイプセーフですしね。

余談:片方のストリームが発動したら止める、という処理は割と定形なので明示的にsubscriptionをDisposeするようなコードを書かなくても、TakeUntilが使える。

Observable.Interval(TimeSpan.FromSeconds(1))
    .TakeUntil(Observable.EveryUpdate().Where(_ => Input.GetMouseButtonDown(0)))
    .Subscribe(_ => Debug.Log("hogehoge!"));

色々あるってことです!メソッド一覧を眺めて使い道を考えよう!

非同期について

Unityにある素敵な素敵なコルーチンはあまり素敵ではない。WWWでyieldできて非同期扱えてサイコー、ではない。なんで?C#のyieldが非同期を扱うためのものじゃないから。try-catchできないからyield return StartCoroutineしたら例外はあの世に飛んでいく。それを避けるためにWWWのようにwww.textとwww.errorを持つようにあらゆる非同期はどうでもいいコンテナを持たなければならなくて?そしてそもそもIEnumeratorは戻り値を持てない。だから戻り値を持たせたかったらコールバックの形に返るしかない。酷い、酷い、醜悪な話だ。

// こんなこるーちんを用意して
IEnumerator GetGoogle(Action<string> onCompleted, Action<Exception> onError)
{
    var www = new WWW("http://google.com/");
    yield return www;

    if (!www.error) onError(new Exception(www.error));
    else onCompleted(www.text);
}

// なんかダラダラしてる
IEnumerator OnMouseDown()
{
    string result;
    Exception error;
    yield return StartCoroutine(GetGoogle(x => result = x, x => error = x));
    if(error != null) { /* なんかする */ }

    string result2;
    Exception error2;
    yield return StartCoroutine(GetGoogle(x => result2 = x, x => error2 = x));
    if(error2 != null) { /* なんかする */ }
}

基本的に破綻している。別に分離しないで単純に単純なWWWを延々と連鎖している限りは、少しはまともに綺麗になるかもしれないけれど、それは処理を分離できないという問題を産む。一つの巨大な無駄にデカいCoroutineと重複コードを避けられない。何れにせよコルーチンは非同期を扱うためのベストソリューションでは全くない(ところでNode.jsはyieldで立派に上手く非同期を扱っているじゃないか!と思う方もいるかもしれませんが、アレはyieldが戻り値を返しているから可能であって、どちらかといえばC# 5.0のasync/awaitに近い。C#のyield returnとは少し別物)

Rxならどう書くか?全てをObservableの連鎖フローに変換する。

// xが完了したらそれでy、完了したらzのダウンロードの連鎖のフローをLINQクエリ式で
var query = from x in ObservableWWW.Get("http://google.co.jp/")
            from y in ObservableWWW.Get(x)
            from z in ObservableWWW.Get(y)
            select new { x, y, z };

// Subscribe = "最後に全部まとまったあとの"コールバック(ネストしないから処理が楽)
query.Subscribe(x => Debug.Log(x), ex => Debug.LogException(ex));

メソッドチェーン(もしくはクエリ式)で、コールバックのネスト数を最小に抑えてフロー化、一体となって処理する。

また、並列ダウンロードなども簡単に行えるのは大きな利点かもしれない。

var parallel = Observable.WhenAll(
    ObservableWWW.Get("http://google.com/"),
    ObservableWWW.Get("http://bing.com/"),
    ObservableWWW.Get("http://unity3d.com/"));

var cancel = parallel.Subscribe(xs =>
{
    Debug.Log(xs[0]); // google
    Debug.Log(xs[1]); // bing
    Debug.Log(xs[2]); // unity
});

// これでキャンセルできる
cancel.Dispose();

こんな風に並べて簡単に並列処理できます。また、全てのRxのSubscribeは戻り値をDisposeすることでキャンセルできる。文字列でStopCoroutineする時代はさようなら。

コルーチンについて

と、コルーチンを腐しましたが、しかし実際コルーチンは素晴らしいツールだと思っています。フレームワークネイティブの機構であり、それはやはり強いのです。無理にRxで全部書くことはまったくもって完全に"可能"なのですが、それよりは素直にコルーチンで書いて、Rxはそれをまとめることに徹してみればいい。コルーチンもイベントと同じく一つの素材。と考えればイイ。

public IObservable<string> AsyncA(string msg)
{
    return Observable.FromCoroutine(observer => AsyncA(msg, observer));
}

// 戻り値のあるコルーチン(IObserverがコールバックを扱うコンテナとして考える、値通知とエラー通知、両方を内包する)
private IEnumerator AsyncACore(string msg, IObserver<string> observer)
{
    Debug.Log("a start");
    yield return new WaitForSeconds(1);
    observer.OnNext(msg); // 値を通知
    observer.OnCompleted();
    Debug.Log("a end");
}

public IObservable<string> AsyncB()
{
    return Observable.FromCoroutine(AsyncBCore);
}

// 戻り値のないコルーチン
private IEnumerator AsyncBCore()
{
    Debug.Log("b start");
    yield return new WaitForEndOfFrame();
    Debug.Log("b end");
}

// こんな使い方
var cancel = AsyncA()
    .SelectMany(_ => AsyncB())
    .Subscribe();

// 例によってコルーチンを止めたければ戻り値をDisposeする
cancel.Dispose();

Observable.FromCoroutineによって変換できて、あとは好きなように合成できる。FromCoroutineは戻り値があってもなくてもOK。作る時にお薦めなのはIEnumeratorはprivateにして、変換後のIObservableのほうだけをpublicにすること。

uGUI

iOS用のReactiveCocoaが非常に受け入れられているように、リアクティブプログラミングはGUIとの相性が非常に良いです。そこでUniRx ver.4.5では既にUnityの新GUIシステムに対応!.AsObservable()と書くだけで変換できます。例えば

GetComponentInChildren<Button>().AsObservable()
    .Subscribe(x => Debug.Log("クリックされた!"));

のように書けます。詳しい話はいつか!

そのうち書く何か(予告!)

「Pull vs Push」RxはPush型。Pull型のアーキテクチャはFindGameObjectsで探してきて何かする、もしくはなんとかManagerでオブジェクトを維持して、何れにせよそれらで蓄えて配列上のものに対して処理をグルッと書く。RxはPush型、何かして欲しい何かを各GameObject自身が通知する。ようするにイベント。イベントと違うのはRxならばイベントの集合体を圧倒的にコントロールしやすいこと(沢山のイベントストリームを処理するためのメソッドがある!)。本来、必要な時に必要な情報だけを送ってくるPush型のほうが、必要かどうかを取得してから考える必要のあるPull型よりもパフォーマンスも良くなる可能性が高いし。しかし生のイベントはコントロールが難しすぎて中々使えなかった。それをRxが解き放つ。とかうんたらかんたら。

学習リソース

UniRxの特徴として、Reactive Extensions系の学習リソースを流用できることが上げられますが、実際RxJavaのWikiは非常にお薦めです。沢山あるメソッドの説明が図入りで説明されていて非常に分かりやすい、例えばFiltering Observablesとか。

また、Introduction to Rxは非常に充実したチュートリアルを提供し、RxMarbles: Interactive diagrams of Rx Observables ではインタラクティブにメソッドの挙動を確認できます。

こうしたリソースにひたすらタダ乗り出来るのが強い!

更新履歴。

さて、いまさらですが、UniRxの最初の発表日は2014/04/19に開催されたすまべん特別編「Xamarin 2.0であそぼう!」@関東での発表でした。

UniRx - Reactive Extensions for Unity from Yoshifumi Kawai

この時にGitHubにリポジトリを公開して、アセットストアに審査出し。そこから審査に3回ほどこけて、一月後に2014年05月28日に公開されました、わーぱちぱち。ってしかしブログに解説書く書く詐欺で解説を書かないでいました、ほげえ。だからこの記事が最初の解説記事なんですねー、えー……。なんとなく書かないでいたのは、次のバージョンではもっと良くなってるから!を延々と繰り返してたから説。特に大きく変わったのがver.4.3(ちなみにUniRxのバージョンが4始まりなのは、審査にこける度に間違ってメジャーバージョンを上げちゃってたからです、気付いた時には戻せず……)

ver 4.3 - 2014/7/2

Fix iOS AOT Safe totally
MainThreadSchedule's schedule(dueTime) acquired time accuracy
MainThreadDispatcher avoid deadlock at recursive call
Add Observable.Buffer(count, skip)
Change OfType, Cast definition
Change IScheduler definition
Add AotSafe Utilities(AsSafeEnumerable, WrapValueToClass)
Change Unit, TimeInterval and Timestamped to class(for iOS AOT)
Add Examples/Sample7_OrchestratIEnumerator.cs

Unity + iOSのAOTでの例外の発生パターンと対処法という記事を書いて、そこそこ反響あったのですが、その成果を突っ込んでます。というわけで、このバージョンでiOSのAOT問題を大きく解決しました。そしてver4.4。

ver 4.4 - 2014/7/30

Add : Observable.FromEvent
Add : Observable.Merge Overload(params IObservable[TSource][] / IEnumerable[IObserable[TSource]])
Add : Observable.Buffer Overload(timeSpan, timeShift)
Add : IDisposable.AddTo
Add : ObservableLogger(UniRx.Diagnostics)
Add : Observable.StartAsCoroutine
Add : MainThreadDispatcher.RegisterUnhandledExceptionCallback
Add : Examples/Sample08, Sample09, Sample10, Sample11
Performance Improvment : Subject[T], OnNext avoids copy and lock
Performance Improvment : MainThreadDispatcher, avoids copy on every update
Change : Observable.ToCoroutine -> ToAwaitableEnumerator
Fix : ObservableMonoBehaviour's OnTriggerStay2D doesn't pass Collider2D

機能的にはObservableLoggerを入れたのとIDisposable.AddToでリソース管理のガイドを示したのが大きいんですが、一番大きいのはパフォーマンス改善かなあ、と。ガチで使うと大量に出てくるSubject[T]や、絶対経由することになるMainThreadScheduler/Disptacherの性能を限界まで向上させたので、あまりネックになることはないのではかな、と。で、公開まだなver4.5。

ver 4.5 - 2014/8/19(アセットストアへは審査中)

Add : ObservableWWW Overload(byte[] postData)
Add : Observable.Buffer Overload(windowBoundaries)
Add : LazyTask - yieldable value container like Task
Add : Observable.StartWith
Add : Observable.Distinct
Add : Observable.DelaySubscription
Add : UnityEvent.AsObservable - only for Unity 4.6 uGUI
Add : UniRx.UI.ObserveEveryValueChanged(Extension Method)
Add : RefCountDisposable
Add : Scheduler.MainThreadIgnoreTimeScale - difference with MainThreadScheduler, not follow Unity Timescale
Add : Scheduler.DefaultSchedulers - can configure default scheduler for any operation
Fix : DistinctUntilChanged iOS AOT issue.
Fix : Remove IObservable/IObserver/ISubject's covariance/contravariance(Unity is not support)
Fix : UnityDebugSink throws exception when called from other thread
Fix : Remove compiler error for Windows Phone 8/Windows Store App
Breaking Change : MainThreadSchduler follow Unity Timescale
Breaking Change : All Timebased operator's default scheduler changed to MainThreadScheduler
Breaking Change : Remove TypedMonoBehaviour.OnGUI for performance improvment
Performance Improvment : AsyncSubject[T]
Performance Improvment : CurrentThreadScheduler
Performance Improvment : MainThreadScheduler

本当はこの記事と同時にアセットストアでも公開!と行きたかったんですが審査がまだー……。uGUI対応のイベントハンドリングを足したり、UIで使うの見越したUniRx.UI.ObserveEveryValueChangedの追加とか、uGUIへの対応を見越した基礎部分を足してっていってる感じですね。こうしたUIでの利用法はuGUIのノウハウと共に貯めていきたい/公開していきたいと思っています。

あと凄く大きいのが時間ベースのメソッドで使われるデフォルトのスケジューラをScheduler.MainThreadに変えたことで、ふつーに使う分には全てがUnityのTimescaleの影響下にあるシングルスレッドで動く状態になるので、違和感というかハマりどころ(ObserverOnMainThreadしなかったから死んだ!オマジナイにObserveOnMainThreadって書きまくったせいで性能が!)を消せたのかなー、と思います。ここは本家Rxとは当然デフォルトが違うことになりますが、Unityネイティブに寄せるべきだろう、という判断です。

あと地味にWindows Phone 8やWindows Store Appにも対応しました。いや、最初のバージョンでは対応してたんですが機能足してるうちに、どうやらコンパイル通らなくなってしまっていて……。Platform切り替えないと気づけないのが辛いですねえ、Unity Cloud BuildのWindows Phone対応はよ!いちおう「We’ll be adding more platforms as the service matures.」ってあるので、適当に待ちましょう。そもそもBetaの現状は重すぎてそういう次元ですらないですしね。

次回更新では、現状ExecuteInEditModeでは動かないので、それに対応したものを出す予定です。

最後に

と、いうわけでどうでしょう?使ってみたくなってもらえれば幸いです。怒涛の更新のとおりにやる気はかなりあります、というか私はグラニという会社のCTOをしているんですが(CM放送などをした「神獄のヴァルハラゲート」が代表作です)、開発中の次のプロダクトに投下していて、実プロダクトで使う気満々というかドッグフーディングというか、ともあれ現状「枯れてない」というのは否定出来ないのですが、基本的なバグは既に概ね殺せているのではかなぁ、と、そこは信頼してくれると嬉しいですね。今回はUniRxの初めての記事だったので基礎の基礎的な話になりましたが(そうか?)、今後は応用的な記事などもどんどん出していきます。

Reactive Extensions自体は、私は2011年には@ITに連載:Reactive Extensions(Rx)入門という記事を書いていたり、そもそも2009年に最初のベータ版が出た時から追っかけて記事を書き続けていたりとneue.cc/category/programming/rx、5年間延々とRxを触っているので、さすがにかなり詳しいのではないかと自負するところです(ちなみに最初の記事は.NET Reactive Framework メソッド探訪第一回:FromEventでした。そう、当初はReactive Frameworkって名前だったんですね、更にもっと源流はMicrosoft Live Labs Voltaになります)。

繰り返しますが日本ではReactive Programmingのブームは定期的に起こっては消え(最初のほうでバズったのは2010年のやさしいFunctional reactive programming(概要編)でしょうか)、って感じですが、Microsoftは理論やプロトタイプに留まらず延々と改良を続け、完全に実用ベースに載せ、本物のブームを作り上げたことには本当に感嘆します。勿論、ブーム自体はMicrosoftよりは、そこから波及していったRxJavaやReactiveCocoaの貢献が非常に大きいです。私もUniRxで、Reactive Programingの強力さをUnityでも示し、大きなブームが巻き起こせればなあ、なんて野望は抱いていますね!

UniRxへの質問があれば、GitHubのIssuesUnityのForumに立ててあるスレッドでもぜひぜひですが、そこでは英語でお願いしたいのでちょっと敷居がー、ということであれば、普通にTwitterの@neuecc宛てに気楽に言ってください。またはTwitterで「UniRx」で常時検索してますんで独り言みたいな感じでポストしてもらえればチェックします。

あと会社単位でも、会社間交流ということで共催の社内勉強会などできれば嬉しいかなー、と思ってますので、是非グラニと勉強会やりたいという方いらっしゃいましたらお声がけください。今までもKLabさん - 2014年3月度ALMレポート(株式会社グラニ様との合同開催)やドリコムさん等と行ってきています。グラニのUnity以外の技術、というか現状はそちらがメインなのですが、というのはgihyo.jpのグラニがC#にこだわる理由という記事を見て頂ければなのですが、サーバーサイドをPHPやPython、RubyじゃなくてC#(Windows Server + ASP.NET)でやる、というのが強みです。勿論、今後クライアントサイドもC#(Unity)でやっていきます。

さて、明日は野生の男さんの「無料アセットで簡単IBL!」です。楽しみ楽しみー、ではでは!

Profile

Yoshifumi Kawai

Cysharp, Inc
CEO/CTO

Microsoft MVP for Developer Technologies(C#)
April 2011
|
July 2024

Twitter:@neuecc GitHub:neuecc

Archive