Archive - Programming

Unityでのボクシングの殺し方、或いはラムダ式における見えないnewの見極め方

Happy boxing! UniRxの前回リリース(UniRx 5.0.0)でパフォーマンス向上を果たしたと書きましたが、まだやり残したことがありました。それがボックス化(boxing)の殺害です。ボックス化は単純に言うと、せっかくの値型が箱に入っちゃってGCゴミが発生してGCがーーー、というもの。避けれるなら避けるべし。あ、ちなみに今回の内容は特に別にUnityに限らないふつーのC#の話です。

それと、というわけかでUniRx 5.1.0リリースしました、アセットストアから落とせます。基本的な内容は以下に解説するボックス化を徹底的に殺害したことによるパフォーマンス向上です。

ボックス化とジェネリクス

GCって、別に見えてるnewだけで発生するわけでもありません。見えてるものを警戒するのは大事ですが、見えないものを見てないのは片手落ち感が否めない。そんな見えないものの代表例がボックス化です。実際どういう時に発生するのかというと

var x = (object)10;

みんな大好きint(ValueType)がobject(ReferenceType)に!これがボックス化の害です。なるほど、避けたほうが良さそうだ。とはいえこんなのやらないって?ですよね。ではこれは?

void Hoge(object o)
{
}
 
Hoge(10);

まぁまぁやらないかもしれませんが、まぁまぁやるといえなくもないです。というかやる時はあります。ではこれは?

bool IsSame<T>(T t1, T t2)
{
    return t1.Equals(t2);
}

一見何も悪くないのですが、実は悪いです。どこが?

public virtual bool Equals(Object obj);

ここが。ようするにEqualsはobjectになった後に比較されてしまうのです。というわけでボックス化が発生します。ジェネリクスは基本的にボックス化を避けれるのですが、一部のObjectに生えてるメソッド、というかようするにEqualsですが、を触る場合、気をつけないとうっかりしがちです。他に t1.GetType() と書いてもボックス化が発生します。その場合、 typeof(T) と書くことで避けられます。

EqualityComparer<T>を使う

ボックス化を避けた比較を行うインターフェイスにIEquatable<T>があります。

public interface IEquatable<T>
{
    bool Equals(T other);
}

これを使い、つまり

bool IsSame<T>(T t1, T t2) where T : IEquatable<T>
{
    return t1.Equals(t2);
}

にすればボックス化は避けれる問題なし。ではあるんですが、これでは不便すぎます(さすがにintとかはIEquatable<T>を実装してはいますが、普通の参照型はほとんど実装していないでしょう)。同じなのかどうかとりあえずチェックしたい、Equalsを普通に呼びたいケースは沢山あります。そこでEqualsを外部から渡せるIEqualityComparer<T>インターフェイスと、デフォルト実装を取得するEqualityComparer<T>.Defaultが使えます。

bool IsSame<T>(T t1, T t2)
{
    return EqualityComparer<T>.Default.Equals(t1, t2);
}

EqualityComparer<T>.Defaultは、TがIEquatable<T>を実装していればTがIEquatable<T>のEquals(T other)を、実装してなければEquals(object other)を呼んで比較します。これによりめでたく値型のボックス化が避けれました!UniRxでもDistinct、DistinctUntilChanged、ObserveEveryValueChanged、そしてReactivePropertyのSetValueでボックス化が発生していたのですが、UniRx 5.1.0からは発生しなくなっています。なんで今まで発生していたのかというと、EqualityComparer<T>がiOS/AOTで怪しくてあえて避けてたんですが、5.0.0からAOTサポートはきってIL2CPPのみにしたので無事性能向上を果たせました。

UnityとIEquatable<T>

Unityにおいては、それだけでメデタシではなく、もう少し話に続きがあります。Unityにおける代表的な値型であるVector2やRectなどは、全て、IEquatable<T>を実装して、いません。へー。==はオーバーライドされているので、素のままで扱って比較している限りは問題ないのですが、ジェネリックの要素として、また、DictionaryのKeyとして使った場合などでもボックス化が発生しています。

これが地味に困る話で、UniRxにおいてもObserveEveryValueChangedなどでVector2などが流れてくるたびにボックス化が発生したらちょっとよろしくない。

そこで、その対策として今回のUniRx 5.1.0では UnityEqualityComparer.Vector2/Vector3/Vector4/Color/Rect/Bounds/Quaternion というものを用意しました。これら代表的なUnityの値型に関しては、専用のEquals/GetHashCodeを実装してあります。また、 UnityEqualityComparer.GetDefault[T] により、それらが型から取り出せます。普通にUniRxを使っている範囲では(Distinct、DistinctUntilChanged、ObserveEveryValueChangedなど) IEqualityComparer の取得は UnityEqualityComparer.GetDefault[T] を通すようにしているため、極力ボックス化が発生しないようになっています。

ラムダ式と見えないnew

ボックス化、見えないGCゴミの話を書いたので、ついでにもう一つ見えないゴミを発生させるラムダ式について。ラムダ式は実際のところコンパイラ生成の塊みたいなもの、かつ、中身によってかなり生成物が変わってきます。ざっと6通りのパターンを用意してみました。

static int DoubleStatic(int x)
{
    return x * 2;
}
 
int DoubleInstance(int x)
{
    return x * 2;
}
 
void Run()
{
    var two = int.Parse("2");
 
    Enumerable.Range(1, 1).Select(DoubleStatic);           // 1
    Enumerable.Range(1, 2).Select(DoubleInstance);         // 2
    Enumerable.Range(1, 3).Select(x => x * 2);             // 3
    Enumerable.Range(1, 4).Select(x => x * two);           // 4
    Enumerable.Range(1, 5).Select(x => DoubleStatic(x));   // 5
    Enumerable.Range(1, 6).Select(x => DoubleInstance(x)); // 6
}

どんな感じになるか想像できました?では、答え合わせ。ちょっと簡略化しているので正確にはもう少しこんがらがった機会生成になっていますが、概ねこんな感じになってます。

static Func<int, int> cacheA;
static Func<int, int> cacheB;
 
internal static int LambdaA(int x)
{
	return x * 2;
}
 
class Closure
{
    internal int two;
 
    internal int LambdaB(int x)
    {
        return x * two;
    }
}
 
internal static int LambdaC(int x)
{
	return DoubleStatic(x);
}
 
internal static int LambdaD(int x)
{
	return DoubleInstance(x);
}
 
void Run()
{
    var two = int.Parse("2");
 
    // 1 - Select(DoubleStatic)
    Enumerable.Range(1, 1).Select(new Func<int, int>(DoubleStatic));
 
    // 2 - Select(DoubleInstance)
    Enumerable.Range(1, 2).Select(new Func<int, int>(DoubleInstance));
 
    // 3 - Select(x => x * 2)
    if(cacheA != null)
    {
        cacheA = new Func<int, int>(LambdaA);
    }
    Enumerable.Range(1, 3).Select(cacheA);
 
    // 4 - Select(x => x * two)
    var closure = new Closure();
    closure.two = two;
    Enumerable.Range(1, 4).Select(new Func<int, int>(closure.LambdaB));
 
    // 5 - Select(x => DoubleStatic(x))
    if(cacheB != null)
    {
        cacheB = new Func<int, int>(LambdaC);
    }
    Enumerable.Range(1, 5).Select(cacheB);
 
    // 6 - Select(x => DoubleInstance(x))
    Enumerable.Range(1, 6).Select(new Func<int, int>(LambdaD));
}

それぞれ似ているような違うような、ですよね?一つ一つ見ていきましょう。

パターン1、パターン2はメソッドを直接突っ込む場合。この場合、実際のところはデリゲートを生成して包んでます。そしてこのデリゲートはGCゴミになります。なります。全く見えないんですが地味にそうなってます。と、いうわけで、それを回避するには静的メソッドなら静的フィールドに静的コンストラクタででも事前に作ってキャッシュしておく、インスタンスメソッドの場合は、もし使うシーンがループの内側などの場合は外側で作っておくことで、生成は最小限に抑えられるでしょう。

パターン3は、恐らく最もよく使うラムダ式の形式で、使う値が全てラムダ式の中だけで完結している場合。この場合、自動的に静的にキャッシュを生成してそれを未来永劫使いまわしてくれるので、非常に効率的です。一番良く使う形式が効率的というのは嬉しい、遠慮無くどんどん使おう。

パターン4も、まぁよく使う形式、でしょう。ローカル変数をラムダ式内で使っている(キャプチャ)した場合。この場合、普通にクラスがnewされて、そこにラムダ式内部で使われる値を詰め込み、その自動生成のクラスのインスタンスメソッドを呼ぶ形に変換されます。というわけで、パターン4は見た目は人畜無害ですが、中身はそれなりのゴミ発生器です!いや、まぁたかがクラス一個。であり、されどクラス一個。画面上に大量に配置されるGameObjectのUpdateなどで無自覚に使っていたりすると危なっかしいので、それなりに気を留めておくと精神安定上良いでしょう。

パターン5、パターン6は内部でメソッドを使っている場合。ちなみにここではメソッドにしましたが、フィールドやプロパティでも同じ生成結果になります。抱え込む対象がstaticかinstanceかで変わってきて、staticの場合ならキャッシュされるので少しだけ有利です。

なお、この挙動は現時点でのVisual Studio 2015のC#コンパイラによって吐かれるコードであり(Unityの今のmonoもほぼ一緒、のはず、です、確か多分)、将来的にはそれぞれもう少し効率的になるかもしれません(メソッドを直接突っ込む場合のキャッシュとかは手を加える余地がある気がする)。とはいえ原理を考えたら、外部変数をキャプチャするラムダ式はどうやってもこうなるしかなさそうだったりなので、大筋で変わることはないと思います。

まとめ

正直なところ今回書いたのは細かい話です!別に気にしすぎてもしょうがないし、というかこんなの細部まで気にして避けながら書くのは不可能です。ギチギチに避けてラムダ式禁止だのLINQ禁止だの言い出すなら、早すぎる最適化の一種で、かなり愚かしい話です。が、ゲームの中にはひじょーにタイトな部分は存在するはずで、そこで無自覚に使ってしまうのも大きなダメージです。私だってタイトになることが想定されるUpdateループの中でLINQを貫くならやめろバカであり、普通にペタペタとforで書けとは思いますよ。

あんまりゼロイチで考えないで、柔軟に対処したいところですねえ。どこに使うべきで、使うべきでないか。まぁその見極めがむつかしいから全面禁止とかって話になるのは実際のところ非常によくわかる!のですが、それこそプロファイラで問題発見されてからでもいいじゃん、ぐらいの牧歌的な考えではいます。いやだって、そんなたかがLINQやラムダ式ぐらいであらゆるところがボトルネックになるわけないぢゃん?そんなのより大事なとこ沢山あるでしょう。それに比べたらLINQを普通に使えることのほうが、UniRxを普通に使えることのほうが100億倍素晴らしい。もちろん、地味な積み重ねでダメージが出てくるところであり、そして一個一個は地味だったりするから見つけづらくて辛いとかって話もありつつ。

そんなわけでUniRxは、かなり厳し目に考慮しながら作っているので、比較的概ね性能面でも安心して使えるはずです!まだもう少しやれることが残ってはいるんですが、ちょっと踏み込んで書いてみると謎のuNET weaver errorに見舞われて回避不能で死んでいるので、当面はこの辺が限界です(ほんとuNET絡みのエラーはなんとかして欲しい、理不尽極まりない)。とはいえ、何かネタがあれば継続してより良くしていきますので、よろしくおねがいします。

そういえば第一回Unityアセットコンテストでは、セミファイナリスト頂きました。ほぼほぼスクリプトのみの地味 of 地味なアセットであることを考えると全然上等で、嬉しい話です。

UniRx 5.0 - 完全書き直しによるパフォーマンス向上とヒューマンリーダブルなスタックトレース生成

UniRx(Reactive Extensions for Unity)のVer 5.0が昨日、AssetStoreにリリースされました。前回が4.8.2で6月なので、半年ぶりで、今回はメジャーアップデートとなります。現在の最新であるUnity 5.3(の新機能)に対応というのもあります、が、今回の目玉は書き直しです。半年間なにやっていたかというと、書き直そう!いよいよやっと重い腰を上げてスタックトレースに優しいコードにしよう!と思い立って始めてみたもののメンドウくささが極まって挫折して放置。してたんですが、先月ぐらいに、いい加減に手を付けたくて、ちょっとうちの会社の仕事時間を貰ってゴリゴリ進めてやっと終わりました。

とりあえず分かりやすい成果としては、スタックトレースです。

var rp = new ReactiveProperty<int>();
 
rp.Where(x => x % 2 == 0)
  .Select(x => x * x)
  .Take(10)
  .Subscribe(x => Debug.Log(x));
 
rp.Value = 100;

という人畜無害なコードがあるとして、以前のスタックトレースはこうです。

image

言ってることはわからんでもないコンパイラ生成の何かと、多量の中間物で埋まっていて、実に読み取りにくい。この程度のメソッドチェーンならまだマシで、もっと長大で、複雑なオペレータが絡んでる場合は困難極まってました。私も何度文句を言われて平謝りしたか分からないぐらいです。しかし、今回のバージョンからはこうです。

image

自動生成コードなし、中間物ナシ。圧倒的な読みやすさ!また、これはそのまま、書いたとおりに動いているということの証左でもあります。実行パイプラインの無駄がスタックトレースに出ているままに皆無になったので、パフォーマンスにも寄与しています(書き換えた今では、もはや前のが厚すぎた説はありますけれど、それはまぁ言わんといてください……)

実装はかなりメンドウで、ラムダ式を使うと問答無用でコンパイラ生成のクラスが吐かれてしまうので、ひたすら名前付きのクラスを作っていくお仕事をしました(一個のオペレーターにつき2~3のクラスを要求する、オーバーロードがあればその分だけ……)。また、Unityのコンソールの出力に合わせた細かい調整を施すことによって(+通常のスタックトレースへの吐かれ方に対しても調整して)作りました。すっかりスタックトレースのことを考えたプログラミングができる脳みそが出来上がったんですが、基本的に面倒くさ度100なので、ふつーのゲーム側のコードでは考えたくないしやりたくもないしやらなくていいと思ふ。

性能改善

じゃあ前のは遅かったのかよ、と言われると、うーん、そんなでもないですよ?、とは言いたいのですけれど、まぁカタログスペック的には実際3~10倍ぐらい速くなってます。これはねぇ、例えばMySQL 5.7が5.6の3倍速い!なるほど、じゃあ5.6はゲロ遅なのか?そうじゃあないっしょー、みたいな話なのですが、実際速くなったのは誰にとっても私にとっても嬉しい話です。

しかし、パフォーマンス低いとか気になるとか、漠然とした話で、何も言ってないに等しいんです。もちろん、3~10倍速くなったというのも何も言っちゃあいないです。プログラムの抱えている範囲に対して広すぎる、漠然としすぎていて何ら指標になっちゃいません。というのは気をつけてください。Rxのパフォーマンスを測るにあたって、フェーズ的に3つあって、

  • Observableを構築するフェーズ(さすがにこれはほとんど無視していい)
  • Subscribe = Observerを構築するフェーズ
  • OnNext

それぞれは独立して考える必要があります。また、ReactivePropertyはSubscribeと同時にOnNextも一回入るのでSubscribe + OnNextである、などなどがあるので、どこをどう測りたいかを明確にし、どう測るかを考えないとザルな結果になります。

基本的に、Rxのチェーンの寿命は長いのでOnNextの性能を最重要視して見るべきです。ここの区別は非常に大事です、長ければチェーン構築コストは相対的に無視できる範囲に収まるのでマイクロな結果で想像するのは違うってものです。が、初回に大量にSubscribeが発生するといった、ローディング的な意味合いでは、Subscribeのフェーズも鑑みる必要があります。

んで、これもザックリとしすぎでアレなんですが、OnNextは3~5倍ぐらい、Subscribeに関しては10~20倍速くなりました。OnNextは全体的なパイプラインの最適化のオペレーターの実装調整が効いてるんですが、Subscribeは抜本的に最適化/単純化したので、以前と全然違う結果になってます。これは、社内で大量のSubscribeがシーンロード初回に発生するという事案がありまして、Subscribeを改善しない限りロード長過ぎで終わぽ、だったのでなんとかしました、はい、すびばせん今まで手付かずで……(ちなみに本家Rx.NETとやり方変えてるので本家Rx.NETよりも速い)

あとのところはオペレーター次第です。WhereとかSelectとか、単純な奴は実装変わってないんで大差ないんですが、一部のメソッドの実装が素朴でしょっぱかったので、そういうのはきっちり直してるので以前のと全然性能変わってきてます。特にObserveOnが顕著かな。また、Observable.IntervalやTimerなどの一部の時間系メソッドも構造がガラッと変わってるので(MainThreadScheduler/ThreadPoolSchedulerが使われる場合には最適化パスを通るようにしてる)、かなり良好な結果が得られるのではないかと。

全体的にGCゴミも減ってます。まだもう少し減らせるポイントが残ってるので、次のマイナーアップデートではその辺の処理をする予定デス。

リリースノート

今回の。

破壊的変更:
iOS/AOTサポートは切りました。IL2CPPしかサポートしません。
Unit/Tuple/CancellationToken/TimeInterval/Timestampedをclassからstructに変えました。
MainThreadDispatcher.Postのメソッドシグネチャが変わり、T stateを要求します。
ObservableMonoBehaviour/TypedMonoBehaviourがObsoleteになりました。
AotSafe Extensions(WrapValueToClass)を消しました。
InputField.OnValueChangeAsObservableをOnValueChangedAsObservableにリネームしています(Unity 5.3の場合。Unity 5.3でInputField側で同様の変更が入っているため)
Subscribe in SubscribeでのException Durabilityを保証します。
 
追加メソッド/クラス:
Observable.ForEachAsync
Observable.Take(duration)
Observable.Aggregate
Observable.Zip(T3~T7)
Observable.CombineLatest(T3~T7)
Observable.Start(function, timeSpan)
Observable.ToYieldInstruction in Unity 5.3
Observable.DoOnError
Observable.DoOnCompleted
Observable.DoOnTerminate
Observable.DoOnSubscribe
Observable.DoOnCancel
Observable.CreateSafe
Progress
StableCompositeDisposable
MultilineReactivePropertyAttribute
 
その他色々修正:
色々色々(詳しくはGitHubのとこの正式なリリースノート見てくだしあ)

破壊的変更といっても、直撃することはないんじゃないかなあ、と思ってます。ただ社内ではUnit/Tupleのstructへの変更で引っかかったりはしました(想定外にもnullが代入されている場合があった!)。それは適切にdefault使うのと、Tupleに関してはTuple?にするなりする程度で対応はできます。struct化はAOTサポートを切ることで躊躇いなくできるようになって、ヨイことだなー、と。コードも全体的にAOTサポートのための余計なコードを順次切り落としています(パフォーマンスロスに繋がっていたので)。その辺はIL2CPPバンザイ、ですかねえ。

vs IL2CPP - Runtime UnitTest Runnner

IL2CPP万歳と言ったそばから言うのもアレですが、IL2CPP苦しい……。コンパイル死ぬほど遅いし、というのはおいておいても、まだ地雷は埋まっていて、たまに踏んで死ぬんですよね。その場合IL2CPPのバグなんで報告して直してもらうってことになるんですが、それはそれとして、なんで死ぬのかがAOTの場合は想像ついたし対処も比較的容易だったんですが、IL2CPPは踏むまで地雷かどうかを察知することが不能な上に、踏んだら踏んだで、何を踏んだからこうなったかがイマイチ分からなくて最小ケース作ってバグレポも辛いケースもちらほら。

とはいえ、それなりに安定してきてるのは確かだと思います。偉い。そこは賞賛されるべき。

のはいいんですが、実行するまで分からないじゃ(特にライブラリ側としては)困るので、iOS実機でユニットテストを動かしたいと思いました。Unity 5.3からEditor Test Runnerなども標準で入ってきましたが、端的に言えば、欲しいのはそれじゃない。実機で動かしたいの!エディターでの実行はどうでもいいの!

エディター上での実行も大事なんですが、元々UniRxは.NET用ライブラリとしても動くように設計されていて、ユニットテストも.NET用ライブラリとしてMSTestで書かれている(!)という特殊な環境なので、エディターでのテストサポートは完全に不要なのです。いや、だってVSのテストランナー使ったほうがやりやすいじゃん?

image

そうやってユニットテスト自体は書かれてるし、さすがに実機用に別のを書きなおすのは不可能なので、このユニットテストを実機で動かせるように持ってければそれでいいんだよねー。

ここで出てくるのがRoslyn。Roslynを使ってユニットテストプロジェクト内のユニットテストを、ソースコードのファイル単位ではなく、解析可能な構文木単位で取得し、T4 Text Templateで整形して吐き出せちゃえばいいんだ、という合わせ技で運搬することに成功しました。VS2015だから出来るハック、VS2015最高……。さすがにコード持ってくだけではMSTestの実体がなくて動かないんですが、そこは適当にモック(Shim)を用意して回避しました。

image

エクストリーム雑なUI。エラーが出た場合は赤くなってExceptionを表示します。これで、ちゃんとiOS/IL2CPPで全部パスしてるのを確認済みです。

ちなみにこのRoslyn + T4でコード生成するテクニック、今回のように別プロジェクトをターゲットにして運搬するというのもいいんですが、自プロジェクトを対象にすることもできます。T4で生成するためのコードのタネって、今まではT4側に書くしかなくて面倒だったんですが、もうその制限はありません。ありとあらゆるソースコードがコード生成のためのタネとして使えます。メタプログラミングの扉をまた一つ開いてしまった。

このテクニックは私の発明じゃなくてRoslynをT4テンプレート内で使う - ぷろじぇくと、みすじら。から拝借してますので、気になる人はそちらの記事をどうぞ。l

Unhandled Exception Durability

UniRx 5.0の変更のうち、ちょっとだけ重要なのがUnhandled Exception Durabilityというコンセプト。です。これは、Rxでイベントハンドリングするのはいいんだけどエラーでるとイベント購読が吹っ飛ぶの困るんだよねー、に対するUniRxからの回答ということで。内容ですが、Subscribe in Subscribe時の例外を外側に伝搬「しない」ことを保証しています(逆に言えば実は4.8では保証されてなくて解除されたりしてました。ちなみにRx.NETでも保証されてなくて解除されたりされなかったりします、ここはUniRx独自で挙動を明言する形に倒しています)。伝搬しない、というのは握りつぶすという意味ではなくて、ObservableのDispose処理を行わない、という意味です(例外自体はグローバルに飛ぶのでUnityのConsoleにExceptionが表示されるし、ログイベントでちゃんと捉えられます)

button.OnClickAsObservable().Subscribe(_ =>
{
    // もし内側でエラーが発生しても、外側のOnClickがデタッチされることはない
    ObservableWWW.Get("htttp://error/").Subscribe(x =>
    {
        Debug.Log(x);
    });
});

エラーハンドリングは難しい問題で、RxJavaのErrorHandlingの章を読んでも別にそんなワカラナイよね、とかって感じではある。UniRxでは Retry/OnErrorRetry でハンドルできなくはなく、まぁそれがスタンダードなRx WayではあるんですがRxJS の Operators (6) - Observable のエラーハンドリングのまとめコメント「これで本当にエラーハンドリングに十分なのか不安です。」とあるように、実に不安です。

で、入力用のハンドラーが吹っ飛ぶのは致命傷なので、どうしても救いたいその辺のとこに関してはSubscribe in Subscribeで処理するのがいいんじゃないかなー、というのを提唱します。入力イベントを合成したいって局面も多いと思うので、それはそれで合成してもらったうえで(そして、その合成パイプラインに関してはエラーが出ないよう厳重に作る!)、それを入力ストリームだと考えて、そこから先はSubscribe in Subscribe。あまり格好の良いものではないのも事実ですが、現実的っちゃあ現実的かなー、と。ちなみにこの挙動を保証するのはUniRxだけだと思うので他のRx系に持ってっても動きません(多分)

なお、Subscribe in Subscribeでの例外で解除されないのは最上流がHot Observableのものだけです。HotとColdに関してはRxのHotとColdについてなどを参照するといいと思いますが、とりあえず具体的にHotなのはUniRxデフォルトでは FromEvent/Subject/ReactiveProperty/ObservableTriggers/UnityUI.AsObservable です。ようはイベント的なやつです。Coldなのは Return/Interval/Timer/FromCoroutine などで、これらは例外で解除されます(そうじゃないとTimerとか無限に動き続けられても危なくて困るでしょ?FromCoroutineだって途中でエラーが出てる状態なのに回られても困るでしょ?)

CustomYieldInstuction

書き直しはいいんだけど、何か新機能ないと寂しいよなー、ということで、Unity 5.3用に一つ入れました。Unityブログでもカスタムコルーチンとして紹介されていますが、Unity 5.3からCustomYieldInstructionが搭載されました。というわけでUniRxもUnity 5.3以上ならToYieldInsturctionメソッドが使えるようになっています。

IEnumerator TestNewCustomYieldInstruction()
{
    // Rx Observableをyield returnで待ちます.
    yield return Observable.Timer(TimeSpan.FromSeconds(1)).ToYieldInstruction();
 
    // スケジューラを変える(Time.scaleを無視する)とかも当然可能
    yield return Observable.Timer(TimeSpan.FromSeconds(1), Scheduler.MainThreadIgnoreTimeScale).ToYieldInstruction();
 
    // 戻り値を得る場合はObservableYieldInstructionを変数に取れば、Result/Errorで受け取れます
    var o = ObservableWWW.Get("http://unity3d.com/").ToYieldInstruction(throwOnError: false);
    yield return o;
 
    if (o.HasError) { Debug.Log(o.Error.ToString()); }
    if (o.HasResult) { Debug.Log(o.Result); }
 
    // 当然こういう長めのものだって自由に書けます 
    yield return this.transform.ObserveEveryValueChanged(x => x.position)
        .FirstOrDefault(p => p.y >= 100)
        .ToYieldInstruction();
}

今までもToAwaitableEnumerator/StartAsCoroutineというメソッドで同様なことを出来るようにしていたのですが、ToYieldInsturctionのほうが効率的だし、使いやすいです。ToYieldInsturctionによるObservable->Coroutine変換のオーバーヘッドはないといっても過言ではない!Unity 5.3最高!

ちなみに、このToYieldInsturctionはCustomYieldInstructionクラスを実装してません。Unity 5.3のカスタムコルーチン対応というのは、yield returnでIEnumeratorを受け取ると毎フレームMoveNextを呼び出して待機する、というのが正しい話です。CustomYieldInstructionはあくまでIEnumerator実装のためのちょっとしたヘルパーなので、別にそれにこだわる必要はありません、ということで普通に独自の軽量なIEnumerator実装を刺しています。

ちなみに実行されるタイミングはCustomYieldInstructionの説明によると after MonoBehaviour.Update and before MonoBehaviour.LateUpdate だそうなので、実行タイミング調整のネタに使えるかもしれません。

まとめ

実際のトコver 2.0なんですが、諸事情で4始まりなのでver 5.0です!Unityのメジャーバージョンと偶然揃ったしいっか、という気がしますね!今回のコードはかなり自信あって、パフォーマンスがー、な局面であってもお薦めできます。どうせ、ライトウェイトを冠した超機能限定版の同じようなものを実装するなら、性能面であっても素直にUniRxを使ったほうがいいでしょう。と、言えます。言えます。

今月頭に書いたUnity 5.3のMulti Scene EditingをUniRxによるシーンナビゲーションで統合するなどのように、UniRxを前提に置くことで、やれることが大幅に広がります。根底から入れれば全体のプログラミングの世界観が(良くも悪くも)大きく変わります。が、まぁそれはエキセントリックすぎるということであれば、触りは単純なところからでも全然アリかな、とは。思います。特に非同期/マルチスレッド関連は、変なライブラリ入れるよりもずっと良いでしょう。

ところで半年前、今年6月に第一回UniRx勉強会を開催しましたが、第二回の需要ってありますか?もしありましたら、その前に発表者が必要!なので、是非話したい!人は、私のTwitterかメールかに連絡ください。開催するにも発表者いなければ開催もなにもないですからね……!

ついでにもはや触れちゃいけない扱いの気がしなくもないUnity アセットコンテストというのに応募していたのですが結果発表……。

Roslyn C# Scriptingによる実行できるコンフィグの手法と実活用例

Advent Calendar大遅刻組です。というわけでC# Advent Calendar 2015の10日目です!なんで遅刻したかというと、記事のネタのためのライブラリを作るのに思いの外時間がかかってしまったから…… コンセプトも固まってたしプロト実装も済んでたんですが、最終的な形に落としこむのが想定よりちょっと割と大変だった……。すびばせんすびばせん。

どうやらC# Advent Calendarは2011年から書いてるので5回目ですね、へぇー。過去を振り返るとModern C# Programming Style Guide、モダンつってもC# 4.0時代ですが、今ぱっと見直すと別にここで言ってることは今も変わらないですね、これに5.0, 6.0の話を足せばいいだけの話で。2012年はMemcachedTranscoder - C#のMemcached用シリアライザライブラリということで、このライブラリは別に私自身も使ってないので割とどうでもいー、んですが、まぁシリアライザにまつわる諸々についての知見が少しは入ってる模様。2013年の非同期時代のLINQはいい話だなー、これがC# 5.0のModern Styleの追記差分みたいなもので、実際、今現在においては超絶大事な部分。2014年はVS2015+RoslynによるCodeRefactoringProviderの作り方と活用法で、C# 6.0ではないですが、その世代ではAnalyzerは中心になってくるので、これがC# 6.0の差分といってもいいでしょう。多分きっと。

Roslyn C# Scripting

Roslyn、Compiler as a Serviceとか言ってましたが、やっぱスクリプティングが華形だと思うのです。が、しかし。が、しかし。今の今までRoslynに関する話題で、Scripting APIに関するお話はあまり上ってませんでした。理由は単純で、今の今まで未完成品だったから。先月末に出たVisual Studio 2015 Update 1でC# Interactiveが、そして同時にNuGetでもMicrosoft.CodeAnalysis.CSharp.Scriptingで、現在は1.1.1が配布されることにより(ところでこれのパッケージ名が中々定まらなくて実際これであってるのか不安だけどLast updatedが2015/12/3なのでこれでいいでしょう、まだDL数が405ですけど!)やっと全てのピースが揃った感じです。

Scriptingについてのドキュメントは、RoslynのWikiにある2つのページを見ておけば十分でしょう。Interactive-Windowには、csxの仕様っぽいもの、特殊なDirectiveの説明があります(#rとか#loadとか)。Scripting-API-Samplesにはプログラムから触った時のAPIとしてどんなものを持ってるか、どういう風に使えるかが書いてあります。かなりシンプルなので、そんな難しくなくすぐ使えます。

ちなみにC# Interactiveはまだまだ全然使えないって感じなので、期待するほどのものでもないですね。csxもエディタサポートが実質、シンタックスハイライトぐらいなので厳すぃ。黙ってLINQPad使いましょう、課金しましょう。

Roslyn時代のコンフィグ

最近というか数年前からずっと構造化ログにご執心で、EtwStream - ETW/EventSourceのRx化 + ビューアーとしてのLINQPad統合というのを作ってたんですが、今回はそれに、ファイル等への出力プラグイン(Sink)と外部サービス(EtwStream.Service)を作りました。アプリケーションから出力されるログは、ETWというWindows内部に流れてる高速なロギングストリーム機構を通して、別プロセスのEtwStream.Serviceで受け取ります。ログは特に最近ではファイル出力など比較的安定性が保証されているものだけでなく、ネットワークを通じて配信するケースも少なくありません。ログの扱いが別プロセスに別れることにより、アプリケーションに与える影響が少なくなるほか、アプリケーションの状態(アプリ自体の終了/デプロイでの入れ替わり等)に気を配る必要もなくなります。

というのが外部サービスであることの意義なのですが、問題はコンフィグです。コンフィグ。元々EtwStreamはObservableEventListenerという、IObservble<LogEvent>の形でログをストリームで受け取り、それをRxで自由にフィルタしたりグルーピングしたりマージしたりなんでも出来ますよね、という究極の自由度がウリでした。しかしコンフィグです、Rxのその柔軟性をコンフィグで実現するのは不可能です。物凄く機能を削った単純なSubscribeで我慢するか、あるいは超絶複雑なXMLでそれっぽいものを構築するか(log4netやNLogのXMLコンフィグが死ぬほど難解で複雑なのは、ログのルーティング自体が複雑で、それをコンフィグで表現することが困難だということなのです)になります。

せっかく、ログを現代的なReactive Extensionで表現することができたのに、外部サービスにした途端に破棄しなければならないのか。それでいいわけがなく、そこでC# Scriptingの出番になります。EtwStream.Serviceはコンフィグをconfiguration.csxとして、以下のように書きます。

// configuration.csx
 
// 5秒 or 1000件でバッファリング(ふつーのRxのBufferを利用)
// 出力フォーマットは普通にFunc<TraceEvent, string>で整形できる!
ObservableEventListener.FromTraceEvent("SampleEventSource")
    .Buffer(TimeSpan.FromSeconds(5), 1000, EtwStreamService.TerminateToken)
    .LogToFile("log.txt", x => $"[{DateTime.Now.ToString("yyyy/MM/dd hh:mm:ss")}][{x.Level}]{x.DumpPayload()}", Encoding.UTF8, autoFlush: false)
    .AddTo(EtwStreamService.Container);

基本的に完全にC#そのものなので、全てのRxのメソッドが使えて自由に合成・ルーティングが可能です。これはIn-Processで書いてる際(普通のロガーとしてC#コードで埋め込む場合)もOut-Of-Process Serviceでコンフィグとして書く場合も(ほぼ)同じコードで表現できるということです。もちろんC#で書けるということは、 System.Configuration.ConfigurationManager.AppSettings から設定を引っ張ってきたり、ネットワーク通信して何か引っ張ってきたりとかも自由自在やりたいほーだいです。

例えばこれをNLogで表現すると

<targets>
    <default-wrapper xsi:type="BufferingWrapper" bufferSize="1000" flushTimeout="5000" />
    <target name="file" xsi:type="File" fileName="log.txt" keepFileOpen="true"
		layout="[${date:format=yyyy/MM/dd hh\:mm\:ss}][${level}]${message}" />
</targets>
<rules>
    <logger name="*" minlevel="Debug" writeTo="file" />
</rules>

になります。NLogの独自フォーマットルールに従って書く必要があるし、メッセージ書式も独自テンプレートになります。とはいえ、これはまだ単体なので遥かにマシで、色々複合的なことをやろうとするとすぐに膨れ上がって意味不明なことになるのは、みんな経験のあることなのではないでしょうか?

バイナリがEtwStream/releases/EtwStream.Serviceに転がってるので、是非ちょっとだけ遊んでみてくださいな。

仕組み

csxをEvaluateしてるだけです。基本的にcsxの実行は即座に終わります、ObservableEventListenerをSubscribeしているだけですから。しかし、csxが終了してもSubscribeは生き続けています!(言われてみると当たり前のようで、最初はそうなの?と違和感はありました)。それにより、流れてくるログは(別スレッド上の)ObservableEventListenerを流れて、csx上のRxを通りcsx上でのSubscribeにより処理され続けます。というわけで、EtwStream.Serviceのcsxは、ただのXMLコンフィグがcsxに変わっただけ、ではなく、このcsxはコンフィグのようでコンフィグじゃなく、実行コードそのものなのです!

終了処理に関しては、ホスト側から渡しているTerminateTokenと、AddToを通してSubscriptionを登録していることにより制御されています。csxの評価としての実行が終わっていても、裏で生き続けている限り同じ参照を持っているので、ホスト側から干渉することが可能です。なので、ServiceのStopイベント時にはTerminateTokenにCancel命令をホストが出すことにより、Rxの残ってるBufferが送り出され、AddToで受け取っているSubscriptionを待つことにより、溜まったログの処理が完了するまで待機するといった、安全な終了処理を可能にしています。この辺はRxをフル活用してパイプライン組んだ成果ということで。

再びエディタとしてのLINQPad

さて、csxで書けるところのイイトコロはC#なのでコンパイルエラーも検出できるしシンタックスハイライトもあるし、などなど、なのですが、エディタサポートは……。Visual Studio 2015のUpdate 1によって確かにシンタックスハイライトはついた、が、それだけ……。IntelliSenseもDLL読み込ませたり色々しなければなので実質使えないみたいなもので厳しい……。

そこで出てくるのがLINQPad。

image

EtwStream.LINQPadには、EtwStream.Serviceのcsxで渡されてくるEtwStreamServiceクラスのShim成分が入っているので、csxと互換性があって、LINQPadで実際にコンパイルできる/動かして確認した結果をcsxに持っていくことが可能です。(というようなことが出来るようにAPIを調整したんです……)。C# Interactiveが使い物にならならいなら使い物になるまで我慢する、のではなくて、一時凌ぎでもなんでも、他の現実的な解法を探すのが正すぃ。クソだクソだと文句だけ言ってても何も動きませんしね。必要なのは今この場でどうするか、それだけ。

Topshelf

Windowsサービスの実装にはTopshelfというライブラリを用いています。これは、最高に良いです。もはやこれなしでWindowsサービスを実装するのは考えられません!Visual Studioのテンプレートからふつーにサービスを作ると、なんかゴチャゴチャしたのが吐かれてよくわからない上に実行も面倒だし(いちいちinstallしたくないでしょ?)デバッグも困難だし、実にヤバい最低な開発環境。Topshelfで作るとコンソールアプリケーションと同じ感覚で作れます。また、成果物のexeは、そのまんまふつーにコンソールアプリケーションとしても動くので、EtwStream.Serviceの場合、ビューアーとしてLINQPadを要求していましたが、EtwStream.Service.exeを実行すれば普通にビューアーになります(csxで書き出し先をConsole(LogToConsole)にすれば色分けもしてくれる)。サービスとしてのインストールは「install」をつけて実行するだけ。素晴らしい。

日本語ではWindowsサービスを楽に開発~TopShelf~TopShelf によるWindowsサービスの配置をDSCで自動化してみように説明ありますが、本当に簡単なので、サービスを作る機会がある人は是非使ってみてください。超お薦め。

ファイル出力時のロガーのパフォーマンス

今回ロガーを全部自作する都合上、さすがに単純なファイル書き出しと、ローテーションするファイル書き出しは用意しとかないとなぁ、ということで作ったんですが(FileSink, RollingFileSink)、作ってる上でなんとなく気づいたことなど。

そもそもファイルに吐くっていうこと自体がレガスィーで好きじゃないんですが、それはそれとしてもやはり重要なのは間違いありませんし、普通に使います。で、特にInProcessでのロギングの場合、これに気を配らないと普通にパフォーマンス上のボトルネックになってしまったりするわけですねー。さて、で、パフォーマンスは設定が全てです。とりあえず、バッファリングと非同期の二つのオプションを探しましょう。まず、ファイルに吐く場合のパフォーマンスはバッファするかしないかで全く変わるし、逆にバッファさえすればよほどタコな実装じゃない限りはそんな差はなくふつーに性能出ます(多分)。もう一点はasyncですね、これは別に大抵は非同期I/Oじゃなくて別スレッドで書くってだけのパターンなんですが、これが有効だとロガーの動作がアプリケーション自体に一切影響しなくなりますので。まぁバッファを有効にしてれば、例えば1000件に一回書く設定だったら1/1000回以外は書き込み処理に時間喰われることはなくなるので、ほぼ無視できてあってもなくても大差なくなるんですが、(起こるかもしれない)ちょっとしたスパイクは抑制できるかもしれません。また、あえてバッファはオフにしてasyncだけオン(+即時Flush)にすれば、ログが中々Flushされなくてリアルタイムで確認したいのにイライラ、というのがなくなって良いかもしれません。この辺は好みとか要件しだいで。

とりあえず言えるのはデフォの設定がどうなってるかはともかく、ノーバッファでノーエーシンクだと当然のように遅いです。更に設定によってはファイルストリームを都度閉じるか開きっぱがオプションになってるものもありますが、これは当然、開きっぱじゃないとゲロ遅いです。そういう項目がオプションにある場合は注意しましょう。デフォが都度閉じるだったりしてね……(NLogがそうです。NLogのデフォルトは安全寄りに倒し過ぎでパフォーマンスがヤヴァいことになってるので、NLog使う場合はそれなりに弄ったほうがいいでしょう。かといって他のロガーもそう変わりはなくて、大抵はそれなりに弄らないと遅いです)

かわりに、バッファや非同期ってのはログの消失の危険性があります。書いた瞬間には保存されてないってことですからね、アプリケーション終了への耐性が低くなります。気の利いたロガーは、可能な限り、終了を検知して(AppDomainが消える時のイベントとかをハンドリングして)、残ってるバッファを出力しに行ったり非同期の終了を待機しに行ってくれたりはしますが、パーフェクトではありません。例えばAppDomain.ProcessExitのタイムアウトは既定で2秒です。2秒以内にフラッシュが完了する保証はないわけで、そこで完了できなければログロストです。

それを避けるには、「パフォーマンス低下を承知してバッファや非同期オプションを使わない」というのも手ですが、EtwStreamは更に2つの選択肢を提供してます。一つは「Out-Of-Process Serviceでのログ収集」。ETWへのログ出力はほぼノーコストで即時に吐けるのでアプリケーションへの影響は一切無い上に、それを外部サービスで取り出せば、出力側の終了の影響を全く受けません。ただし当然、受け取る側の外部サービスが死んだらロストするという危険性はありますがね!そこに関しては知らんがなというかshoganaiというか精一杯堅牢性を高めますとしか言い様がないですにぇ。

もう一つは、プログラム的に終了が完全に待機できるSubscriptionシステム。もともとEtwStreamは設定をC#で、Rxで書く必要があるので、購読状態に関して100%コントロールできます。というわけでその辺に仕掛けを入れといて

static void Main()
{
    // ApplicationStartの部分でこの2つを用意する
    var cts = new CancellationTokenSource();
    var container = new SubscriptionContainer();
 
    // でログの設定する
    ObservableEventListener.FromTraceEvent("SampleEventSource")
        .Buffer(TimeSpan.FromSeconds(5), 1000, cts.Token)
        .LogToFile("log.txt", x => $"[{DateTime.Now.ToString("yyyy/MM/dd hh:mm:ss")}][{x.Level}]{x.DumpPayload()}", Encoding.UTF8, autoFlush: false)
        .AddTo(container);
 
    // --- 実際にアプリが動いてる部分 --- //
 
    // アプリが終了した時のイベントのところでハンドリングする(Form_ClosedでもApplication_Endでもなんでもいいですが)
    cts.Cancel(); // CancellationTokenのCancelによりBufferの残りが吐き出される
    container.Dispose(); // Subscriptionの完了を待機する
}

といった風にすれば、100%コントロールされて停止時のログ処理を完了させられます。csxでもEtwStreamService.TerminateTokenとか渡していたのと同じことをやればいいということで。

100%コントロールできる代わりに、逆にEtwStreamは自分でコントロールしないかぎりは、気の利いた終了の検知とか組み込んでないので、待たなければふつーにバッファは消えます。これに関しては、10年前はゴテゴテとブラックボックスの中で気の利いたことをしてくれるのが正義だったかもしれませんが、2015年の現代では仕組みはシンプルに、薄くしたうえで、自分でコントロールさせるのが正義だと思ってます。そういう流儀。どっちが正しいってこともないですが、まぁ、今風なんじゃないかな?

ついでに言えば、EtwStreamのFileSinkやRollingFileSinkのパフォーマンスはバッファしてる前提同士で比較しても、他のよりも高い性能を誇ります。理由は幾つかあって、そもそも性能を意識して書いてるから。というのと、.NET 4.6以外をサポートする気がないのでasync/awaitやTPL全開でコードを書いてるから。オプション自体も同期処理は一切なくて、書き出しは非同期I/Oのみに限定などの割り切り。そして、通常はログフォーマット整形などに独自テンプレート的なのを挟まなきゃいけないところを、csxのお陰でFuncで処理できるため、そもそもコードパスに一切のオーバーヘッドがない。C# Scriptingによるコンフィグはパフォーマンスにも寄与するわけです。

しかしまぁ、JavaではBlitz4jlog4j2のAsynchronous Loggerなどのスピード競争があるのに、.NETの牧歌的なこと、といった感じは否めませんねぇ。そんなだから私のとりあえずの雑実装でもfastestになってしまうわけで……。

出力先

EtwStreamが提唱するのは構造化ログ(Structured/Semantic Log)ですし、テキストログが終着点ではありません!テキストログは無視して、構造化されたペイロードを、そのままAzure EventHubsAmazon KinesisGoogle BigQueryのStreaming Insertに流して、ただたんに溜めるのではなくて、即座に分析可能な状態にするのが理想形です。特にお薦めなのは、というか弊社で使ってるのはGoogle BigQueryです。事例として株式会社グラニの Google Cloud Platform 導入事例: 「using CSharp;」という軸と BigQuery の活用で、先進性を求め続ける。を掲載してもらいました:)

今のとこEtwStream用のBigQuerySinkはないんですが(!)そのうち公開するかされるかするんじゃないでしょーか多分きっと。(本当はそれも作って持ってきたかったんですがもう完全に時間切れでして、すでに大遅刻だし……)。そういえばあとFromTraceEventでRegisteredTraceEventが取れるようになりました。これはつい数日前のTraceEventライブラリのアップデートでそうなったから、というだけなんですが、今まで取れてなかったんですよねー。これで大丈夫。というのと、SLABのOut-of-Process Serviceじゃダメな理由に.NET 4.6からEventSourceに追加されたself-describing events(超重要!)に対応してないとか色々あるんですが、そういった話はまたの機会にでも。

まとめ

ロガーの未来はこうあるべきだ、という構想自体は1年以上前からあったんですが、Roslyn C# Scripting APIが正式リリースされてやっと作れた!あと、基本的にはMicrosoft Patterns & PracticesのSemantic Logging Application Block(SLAB)の影響が濃くはあるんですが、更新されなすぎだし、v3はElastic Search + LogStash + Kibana on Azureとか言ってて、マジで終わってるなという感じでもはや見限るしかない……。P&Pは相変わらず本当にやっぱダメですねーという残念さ加減。なにがPatterns & Practicesだよっていう。

csxでコンフィグするライブラリとしてConfigRというのがあるんですが、XMLをcsxで置き換えるだけじゃ、あんま意味がないかな。必要なのは、コンフィグができることじゃなくて、それそのものが実行されて自走することだというのに気づいたので、使うことはなかったし、多分他のアプリケーションでもConfigRを使うことはないと思います。XMLはいうて設定ファイルとしては悪くないんですよねー、XSLTなんかもやり過ぎなければ良い機構ですし。逆にJSONを設定ファイルとして使うのは最低最悪なチョイス(なのでDNXへのやる気が0.1ミリも起きない)

何れにせよ、徐々にではあるでしょうが、csxの面白い活用例というのはどんどん出てくるのではないかと思います、と言いたいんですがfsxが別に対して面白い活用はされてないことを考えるとそんなに出てこないかもしれませんね、とも思いますが、いえいや面白い活用例はやっぱ出てくるかもしれません。batをcsxにしましたとかってだけだと別に面白くもないし意味もそんなにないですからねー、まぁあってもいいけど。もっと本質的に変わるような事例が増えてくれれば何よりです。

Unity 5.3のMulti Scene EditingをUniRxによるシーンナビゲーションで統合する

今回はUnity Advent Calendar 2015のための記事になります。昨日はtsubaki_t1さんによるUnity初心者を脱するためのデバッグ入門…的なやつでした。私はとりあえずVisual Studioでアタッチしてステップ実行、でしょうか……。最近はiOSのIL2CPPのスタックトレースが行番号出してくれなくて禿げそうというのが社内のホットトピックスらすぃ。

去年もUnity Advent Calendarには参加していて、その時はUnityのコルーチンの分解、或いはUniRxのMainThreadDispatcherについてという内容でした。今回も引き続き、私の作成しているUniRx - Reactive Extensions for Unityのお話ということでお願いします。とはいえ、中身的にはMulti Scene Editingや、シーン間での引数渡しをやるのにどうすればいいのか、みたいなところなので、Rxのメソッドは特に説明なくバンバン出てきますが、Rxワカラナイ人はそのへんは雰囲気で流し読みしてもらって、シーン遷移についてのお話を読み取ってもらえれば嬉しいですねん。

Multi Scene Editing

Multi Scene Editingは初出が2014/8/4のUnity Blogの記事でしょうか、1年経ってやっと正式リリース、までまもなく!ですね、5.3から搭載されることになりました。実際どういうことになるかというと、ヒエラルキーウィンドウがこんな感じに。

image

シーン加算で読み込んだシーンがヒエラルキー上でもきっちり分けられます。DontDestroyOnLoadがついたものは専用のところに隔離される。シーンを削除する場合も、そのまま指定してサクッと消したり、マージできたりと、随分とシーン管理がやりやすくなりました。Unity 5.3からはいよいよシーン加算で管理する時代が到来する!

コード的にはUnityEngine.SceneManagement.SceneManagerに全部のAPIがつまってます。基本的にはLoadScene/Asyncか、UnloadSceneぐらいで事足りるのではないでせうか。

// SceneA -> SceneBへボタン押したら加算
// 別にRx使う必要性はないけど無駄に使うエディション
button.OnClickAsObservable()
  .SelectMany(_ => SceneManager.LoadSceneAsync("SceneB", LoadSceneMode.Additive).AsObservable())
  .Subscribe(_ => { /* 完了時の処理何かあれば */ });

この程度だとRx使う必要性はゼロですが、一応、LoadSceneAsyncの戻り値であるAsyncOperationはAsObservableで直接サクッとRx的に変換可能です。

シーン間に引数を渡す

どういうこっちゃって話ですが、新しいシーンに遷移なり加算したいってことは、引数を渡したくて然りだと思うのです。そのシーンを表示する際の初期引数が。例えばアイテム一覧画面から、アイテムの詳細画面を出すなら、アイテムのIDを渡したいよね、とかね。別にAndroidやiOSアプリでも、ウェブのURLのクエリストリングなりなんなりでも、そんなのは普通によくある話です。さて、SceneManagerはその辺りのことは、別になにも面倒みてくれません。じゃあグローバル変数を経由してやりとりするのかというと果てしなくビミョウというかスパゲティ化まったなし。せっかく画面画面がシーンで独立しているなら、値の依存関係もシーン内に抑えてやりたい。

というわけで、遷移/加算時に引数を渡せるシーン遷移機構を作りましょう。

材料として使うのはUniRxのPresenterBaseです。これは何かというと、子要素の初期化の順序をコントロールするのと、値の受け渡しができる仕組みです。ご存知のとおりUnityのGameObjectの初期化順序は不定(Execution Orderでおおまかに指定できるけど、細かいコントロールのために使うものではない)ですが、PresenterBaseの管理下におくことで、Startフェーズにて決められた順序で起動するようにコード上で設定できます。

この性質は、シーンに引数が渡される、つまり全てのルートになるという条件にぴったりです!というわけで、引数を受け取るための基底クラス、SceneBaseをPresenterBaseを継承して作りましょう。

public abstract class SceneBase : PresenterBase
{
    // これがシーン遷移時にセットされる引数を表す
    public object Argument { get; set; }
 
    // 受け渡されたかどうかを管理するフラグ
    public bool IsLoaded { get; set; }
 
    protected override void OnAwake()
    {
        // 初期化が完了した際はロード済みと強制的にマークするおまじない
        this.InitializeAsObservable().Subscribe(_ => IsLoaded = true);
    }
}

こんなもので、割とあっさりめに。実際のシーンのクラスは

// このどうでもいいクラスを引数として渡していくということにする
public class Nanika
{
    public int HogeHoge { get; set; }
    public string Hugahuga { get; set; }
}
 
// 遷移元クラス、適当なボタン押したらSceneBに遷移する
public class SceneA : SceneBase
{
    public Button button;
 
    protected override IPresenter[] Children
    {
        get { return EmptyChildren; }
    }
 
    protected override void BeforeInitialize()
    {
    }
 
    protected override void Initialize()
    {
        button.OnClickAsObservable().Subscribe(_ =>
        {
            // 直接SceneManager.LoadSceneAsyncを呼ぶのではなく、
            // 独自に作成したNavigationService.NavigateAsync経由で引数を渡して遷移/加算する
            var arg = new Nanika { HogeHoge = 100, Hugahuga = "Tako" };
            NavigationService.NavigateAsync("SceneB", arg, LoadSceneMode.Additive).Subscribe();
        });
    }
}
 
// 遷移先クラス、Argumentに引数が渡されてきてる
public class SceneB : SceneBase
{
    protected override IPresenter[] Children
    {
        get { return EmptyChildren; }
    }
 
    protected override void BeforeInitialize()
    {
    }
 
    protected override void Initialize()
    {
        // 前のシーンから渡された引数が取れる
        var arg = Argument as Nanika;
        Debug.Log("HogeHoge:" + arg.HogeHoge + " HugaHuga:" + arg.Hugahuga);
    }
}

ちょっと長いですが、言いたいのは遷移元ではNavigationService.NavigateAsyncを使って引数を渡して遷移先を指定する。遷移先ではArgumentに渡されたものをキャストして取り出す。といった感じです。

作る上での制約としては、必ず各シーンに単一のSceneBaseがヒエラルキーの頂上にある必要があります。こんな感じに。

image

うーん、随分と大きな制約であり不格好ですね……、この手の制約は実際のトコ、ないほうが望ましいです。別に、この手のヘンテコな制約をつけるのがアーキテクチャ、ではないです。自由なほうがよほど良いのです。とはいえしかし、どうにもならなかったので、そこは受け入れるしかなかったということで。この辺が今のところの手札でできる精一杯の形かなぁ。

NavigationService

では、肝心要のNavigationServiceの実装を見ましょう!

public static class NavigationService
{
    public static IObservable<Unit> NavigateAsync(string sceneName, object argument, LoadSceneMode mode = LoadSceneMode.Single)
    {        
        return Observable.FromCoroutine<Unit>(observer => HyperOptimizedFastAsyncOperationLoad(SceneManager.LoadSceneAsync(sceneName, mode), observer))
            .Do(_ =>
            {
                // 型ベースでたぐり寄せる。Find系は避けたいとはいえ、シーン遷移時に一発だけなのでコスト的には許容できるでしょう。
                var scenes = GameObject.FindObjectsOfType<SceneBase>(); 
                var loadedScene = scenes.Single(x => !x.IsLoaded); // 一個だけになってるはず #雑
 
                loadedScene.IsLoaded = true;
                loadedScene.Argument = argument; // PresenterBase.BeforeInitializeが走る前にセットする
            });
    }
 
    static IEnumerator HyperOptimizedFastAsyncOperationLoad(AsyncOperation operation, IObserver<Unit> observer)
    {
        if (!operation.isDone) yield return operation;
 
        observer.OnNext(Unit.Default);
        observer.OnCompleted();
    }
}

なんてことはなく、LoadSceneAsyncが完了した時点でヒエラルキーに新しいシーンがぶちまけられているので、それのBeforeInitializeが走る前にArgumentにセットしておいてやる、というだけの割と単純なものです。ポイントは、BeforeInitializeの走るタイミングはStartということです。順序的に、LaodSceneAsyncが完了した時点で、新しいシーンのGameObjectのAwakeは走っています。なので、Awakeの前にArgumentを渡すのは何をどうやっても不可能です。しかし、Startの前に割り込むことは可能です。そこでルールとして遷移先のシーンでの初期化はStart以降に限定し(PresenterBaseがその辺を抽象化しているので実装者が意識する必要はない)、NavigateAsyncでは可能な限り最速のタイミングでArgumentをセットしにいきます。その秘訣がHyperOptimizedFastAsyncOperationLoadというフザケタ名前のコルーチンです。

yield return null vs yield return AsyncOperation

別にHyperOptimizedFastAsyncOperationLoadの中身は、見たまんまの超絶単純な yield return AsyncOperation です。そして、それこそが秘訣なのです。何を言ってるかというと……

IEnumerator WaitLoadAsyncA(AsyncOperation operation)
{
    while (!operation.isDone)
    {
        yield return null;
        Debug.Log(operation.progress); // 読み込み状態のプログレス通知
    }
}
 
IEnumerator WaitLoadAsyncB(AsyncOperation operation)
{
    yield return operation;
}

両者の違い、分かるでしょうか? WaitLoadAsyncA のほうはプログレスを受け取るためにyield return nullでisDoneを監視するスタイル。WaitLoadAsyncBは直接待つスタイル。結果的に、どちらも待つことができます。プログレス通知は大事なので、WaitLoadAsyncAのようなスタイルを多用するほうが多いのではないかなー、と思います。WWWとか。が、しかし、両者には非常に大きな違いがあります。それは、完了時のタイミング。

image

わざわざ無駄に画像を作ってまで声を大にして言いたいんですが、直接AsyncOperationをyieldすれば、AwakeとStartの間に割り込めます。yield return nullでは普通に1フレ後になるのでStartまで完了しちゃってます。これは超絶デカい違いです、この微妙なコントロールが死ぬほど大事です。きっと役に立ちます。どこかで。ちなみに一番最初に説明したAsyncOperation.AsObservableという神メソッドはyield return nullで待ってます。クソですね。カスですね。ゴミですね。すみません……(これは次のUniRxのリリースではプログレス通知を使わない場合は直接yieldするように変更します、それまでの間は手動コルーチン作成で対応してください)

もう一つ、コルーチンの駆動を各SceneのStartCoroutineで行うと、LoadSceneMode.Single(遷移)の場合、遷移元シーンが破壊された瞬間に紐付いてるコルーチンも強制的に止まる(そしてDestroyは遷移先シーンのAwakeの前)ため、Argumentを渡すという行為は不可能です。が、UniRxのFromCoroutineで駆動させると、中立であるMainThreadDispatcherによるコルーチン駆動となるため、元のシーンが壊れるとかそういうのとは無関係にコルーチンが動き続けるため、その手の制限と付き合わなくても済みます。この辺は実際UniRx強い。

シーン表示を遅らせる

実は、今のとこ別にRx使う必要性はあんまありません、なくても全然出来るレベルです(まぁコルーチンが破壊される件は回避しにくいですが)。それではあんまりなので、もう一歩次のレベルに行きましょう。例えばシーン遷移時に、引数を元にネットワークからデータを読み取って、その間はNow Loadingで待つ。ダウンロードが完了したら表示する。こうした、なんとなく良くありそうな気がする話を、NavigationServiceで対応させてみましょう。

animation

この、あんまり良くわからない例、SceneAボタンを押すとヒエラルキーにSceneBが表示されているけれど画面上には表示されていない、実際にはネットワークからデータをダウンロードしていて、それが完了したら、その結果と共にSceneBが表示される。というものです。なるほど……?

まず、SceneBaseにPrepareAsyncメソッドを追加します。

public abstract class SceneBase : PresenterBase
{
    public object Argument { get; set; }
    public bool IsLoaded { get; set; }
 
    // このPrepareAsyncメソッドを新設する
    public virtual IObservable<Unit> PrepareAsync()
    {
        return Observable.Return(Unit.Default);
    }
 
    protected override void OnAwake()
    {
        this.InitializeAsObservable().Subscribe(_ => IsLoaded = true);
    }
}

PrepareAsyncが完了するまで表示を待機する、といった感じで、それをIObservableによって表明しています。これで遷移先のSceneBクラスを書き換えると

public class SceneB : SceneBase
{
    public WwwStringPresenter display; // インスペクターから貼り付けてUnityEngineによるデシリアライズ時にセットされる(Awake前)
 
    string wwwString = null;
 
    protected override IPresenter[] Children
    {
        get { return new[] { display }; } // Sceneにぶら下がってる子をここで指定する(コードで!原始的!)
    }
 
    // 呼ばれる順番はPrepareAsync -> BeforeInitialize -> Initialize
 
    public override IObservable<Unit> PrepareAsync()
    {
        var url = Argument as string; // 前のシーンからURL、例えば http://unity3d.com/ が送られて来るとする
 
        // ネットワーク通信が完了するまでこのシーンの表示を待機できる
        // (もし自分で試して効果が分かりにくかったら Observable.Timer(TimeSpan.FromSeconds(5)) とかに差し替えてください、それで5秒後表示になります)
        return ObservableWWW.Get(url)
            .Select(x => // 本当はForEachAsyncを使いたいのですがまだ未リリース。
            {
                wwwString = x; // 副作用さいこー
                return Unit.Default;
            });
    }
 
    protected override void BeforeInitialize()
    {
        // この時点で通信が完了してるので、小階層に渡す。
        display.PropagateArgument(wwwString); // PresenterBase.PropagateArgumentで伝搬するルール
    }
 
    protected override void Initialize()
    {
    }
}

変えたところは、PrepareAsyncでWWW通信を挟んでいるところ。これが完了するまではシーン全体の表示が始まらない(BeforeInitializeが呼ばれない)です。表示に関しては、この程度の超絶単純な例では直接SceneBにTextをぶら下げたほうがいいんですが、無駄に複雑にするために、ではなくてPropagateArgumentの例として、もう一個、下にUI要素をぶら下げてます。それがWwwStringPresenterで、

public class WwwStringPresenter : PresenterBase<string>
{
    public Text displayView;
 
    protected override IPresenter[] Children
    {
        get { return EmptyChildren; }
    }
 
    protected override void BeforeInitialize(string argument)
    {
    }
 
    // 親からPropagteArugmentで渡されてくる
    protected override void Initialize(string argument)
    {
        displayView.text = argument;
    }
}

こんな感じに、親(この場合だとSceneB)から値が伝搬されます、適切な順序で(ふつーにやってるとGameObjectの生成順序は不定なので、値の伝搬というのは単純なようで深く、やりようが色々あるテーマだったり)。さて、一見複雑というか実際、色々ゴテゴテしてきてアレな気配を醸しだしてきましたが、実際どんな状態なのかというと、こんな感じ。

image

この分かったような分からないような図で言いたいことは、値の流れです。シーン間はNavigateAsyncによりArgumentが引き渡され、シーン内ではPresenterBaseによって構築されたチェーンがPropagateArgumentにより、ヒエラルキーの上流から下流へ流れていきます。これにより、グローバルでの変数保持が不要になり、値の影響範囲が局所化されます。スコープが狭いというのは基本的にいいことです、見通しの良さに繋がりますから。分かっちゃいても実現は中々むつかしい、に対する小道具を色々揃えておくと動きやすい。

NavigateAsync最終形

おお、そうだ、PrepareAsyncに対応したNavigateAsyncのコードを出し忘れている!こんな形になりました。

public static class NavigationService
{
    public static IObservable<Unit> NavigateAsync(string sceneName, object argument, LoadSceneMode mode = LoadSceneMode.Single)
    {
        return Observable.FromCoroutine<Unit>(observer => HyperOptimizedFastAsyncOperationLoad(SceneManager.LoadSceneAsync(sceneName, mode), observer))
            .SelectMany(_ =>
            {
                var scenes = GameObject.FindObjectsOfType<SceneBase>();
                var loadedScene = scenes.Single(x => !x.IsLoaded);
 
                loadedScene.IsLoaded = true;
                loadedScene.Argument = argument;
 
                loadedScene.gameObject.SetActive(false); // 一旦非Activeにして止める
 
                return loadedScene.PrepareAsync() // PrepareAsyncが完了するまで待つ
                    .Do(__ =>
                    {
                        loadedScene.gameObject.SetActive(true); // Activeにして動かしはぢめる
                    });
            });
    }
 
    static IEnumerator HyperOptimizedFastAsyncOperationLoad(AsyncOperation operation, IObserver<Unit> observer)
    {
        if (!operation.isDone) yield return operation;
 
        observer.OnNext(Unit.Default);
        observer.OnCompleted();
    }
}

足したコードは、Argumentをセットしたら即座にSetActive(false)ですね。これで画面に非表示になるのは勿論、Startも抑制されます。そうしてStartが止まっている間にPrepareAsyncを呼んでやって、終わったら再度 SetActive(true) にする、ことによりStartが発生しだして、PresenterBaseの初期化機構が自動で上流→下流への起動を開始します。

まとめ

実際にはPrepareAsyncだけでは足りなくて、シーンから出る時、シーンから戻ってきた時、機能としてシーンをキャッシュしてやろうとか、遷移でパラメータ渡ってくる前提だと開発時にパラメータが足りなくてダルいので任意で差し込めるようにする/開発用デフォルト用意するとか、色々やれることはあります、し、やったほうがいいでしょふ。それらも全てUniRx上で、IObservableになっていることにより、表現がある程度は容易になるのではないかと思います。非同期を表現する入れ物、が必要だというのは至極当然の答えになるのですけれど、そこにUniRxが一定の答え、定番を提供できているんじゃないかなー、と思いますね!些か長い記事となってしまいましたが、これに限らず応用例の発想に繋がってくれれば何よりです。

Advent Calendarの次は、@Miyatinさんです!

UniRx vNext

ところで実はいまものすごい勢いで作り変えています!性能もかなり上が(って)るんですが、割と分かりやすく大きいのは、スタックトレースが物凄く見やすくなります。意味不明度が極まった複雑なスタックトレースはRx名物でデバッガビリティが最低最悪だったのですが、相当まともになってます。例えば、以下の様なふつーのチェーンのDebug.Logで表示されるスタックトレースは

var rxProp = new ReactiveProperty<int>();
rxProp
    .Where(x => x % 2 == 0)
    .Select(x => x)
    .Take(50)
    .Subscribe(x => Debug.Log(x));
 
rxProp.Value = 100;

Before

image

After

image

劇的!Unityのスタックトレースの表示形式に100%フォーカスして、読みやすさ第一にハックしたので、圧倒的な読みやすさだと思います。スタックトレース芸極めた。普通にWhere.Select.Take.Subscribeがそのまま表示されてますからね。勿論、メソッドコール数が減っているのは単純に性能にも寄与しています。ここまでやれば文句もないでせう。

そんなvNextの完成時期ですが、今までやるやる詐欺すぎたのですが、そろそろ実際本当に出します。来週ぐらいには本当に出します。これは意地でも仕上げます(想像通りだけれど作業量は多いわコーナーケースの想定が複雑すぎて頭が爆発しそうになるしで辛い……)。というわけでもうちょっとだけ待っててください。

EtwStream - ETW/EventSourceのRx化 + ビューアーとしてのLINQPad統合

EtwStreamというのをリリースしました。ETW(Event Tracing for Windows) + EventSourceが.NETで構造化ログをやる際の決定版というか、ETWの最強度が高すぎてそれ以外考えられないレベルなんですが、しかし、がETWは最強な反面ビューアーがありませんでした。ETWというブラックホールにログを投げ込むのはいいんですが、それが自分自身ですら容易に見れないのは不便すぎる!PerfViewとか骨董品みたいなゴミUIを操ってなんとかして見るのは、無理ゲーなわけで、カジュアルにDumpしたいだけなんだよ!テキストのようなログビューアーが欲しいだけなんだよ!に対する答えです。いや、ほんと自分自身が死ぬほど欲しかったのが、これ。

etwstreamgif

インストールはLINQPadのNuGetで「EtwStream.LinqPad」。だけ。デフォルトにでも登録しとけばLINQPadを立ち上げるだけですぐにビューアーに!

EtwStreamが提供するのは、ETWをIObsevable[TraceEvent]に変換することです。Logs are streamsですから、そしてストリームといったらRxですから。あとは、LINQPadのDumpをそのまま流用して、色付けとか加えてあげただけです。フィルタリングしたい?グルーピングしたい?色々混ぜたい?そんなの全部Rxなんだから、ちょっとクエリ書けばいいだけなのです。最強の柔軟性がある。

Observable.Merge(
    ObservableEventListener.FromTraceEvent("LoggerEventSource"),
    ObservableEventListener.FromTraceEvent("MyCompanyEvent"),
    ObservableEventListener.FromTraceEvent("PhotonWire")
)
.DumpWithColor(withProviderName: true);

EventSourceの提供する構造化ログ(Structured Logging)に関してはC#における構造化ログの手法、そしてデータ可視化のためのDomoの薦めで書いたのでそっちを見てくださいな。そうしてEventSourceに移行した場合の最大の懸念であるビューアーがなさすぎ問題を、このEtwStreamが解決します。た。

ちなみについでにTailっぽくファイルもIObservable[string]に変換するObservableEventListener.FromFileTailもオマケとして入れといたので、そっちもそっちでログビューアー的に使うならきっとベンリ。

もしEventSourceを使ったロギングをやっていなくても、.NET標準組み込みの、例えばTplEventSourceあたりを眺めてみると、色々な挙動が見えて面白かったりします。あとFromClrTraceEventではGCやThraedPoolの挙動が見れたり、FromKernelTraceEventで普段絶対気にしないカーネルイベントが凄まじい勢いで流れて行ったりが簡単に観測できて、普通に勉強になります。オモチャとしてかなり良いと思いますねー。

最初のEventSource

EventSourceって何のことだかさっぱりわからん!という人におすすめなのがLogging What You Mean: Using the Semantic Logging Application BlockというMSDNに転がってるSLABのドキュメントです。これはさすがにひじょーによく書けてるしいいですね。あと、EtwStreamが提供してるのはObservableEventListenerだけで、ロガー的なファイル書き出しとかは一切ないので、そういうのやりたい人は普通にSLAB「も」使いましょう。という感じです。

さて、EventSourceですが、いきなり構造化ログってのもかなりダルいので、まずは非構造化ログをEventSourceで実現するところから初めてみましょう。いや実際それに、こういういのがちょっとあるとそれはそれでベンリでもありますし。

[EventSource(Name = "LoggerEventSource")]
public class LoggerEventSource : EventSource
{
    public static readonly LoggerEventSource Log = new LoggerEventSource();
 
    public class Keywords
    {
        public const EventKeywords Logging = (EventKeywords)1;
    }
 
    string FormatPath(string filePath)
    {
        if (filePath == null) return "";
 
        var xs = filePath.Split('\\');
        var len = xs.Length;
        if (len >= 3)
        {
            return xs[len - 3] + "/" + xs[len - 2] + "/" + xs[len - 1];
        }
        else if (len == 2)
        {
            return xs[len - 2] + "/" + xs[len - 1];
        }
        else if (len == 1)
        {
            return xs[len - 1];
        }
        else
        {
            return "";
        }
    }
 
    [Event(1, Level = EventLevel.LogAlways, Keywords = Keywords.Logging, Message = "[{2}:{3}][{1}]{0}")]
    public void LogAlways(string message, [CallerMemberName] string memberName = "", [CallerFilePath] string filePath = "", [CallerLineNumber] int line = 0)
    {
        WriteEvent(1, message ?? "", memberName ?? "", FormatPath(filePath) ?? "", line);
    }
 
    [Event(2, Level = EventLevel.Critical, Keywords = Keywords.Logging, Message = "[{2}:{3}][{1}]{0}")]
    public void Critical(string message, [CallerMemberName] string memberName = "", [CallerFilePath] string filePath = "", [CallerLineNumber] int line = 0)
    {
        WriteEvent(2, message ?? "", memberName ?? "", FormatPath(filePath) ?? "", line);
    }
 
    [Event(3, Level = EventLevel.Error, Keywords = Keywords.Logging, Message = "[{2}:{3}][{1}]{0}")]
    public void Error(string message, [CallerMemberName] string memberName = "", [CallerFilePath] string filePath = "", [CallerLineNumber] int line = 0)
    {
        WriteEvent(3, message ?? "", memberName ?? "", FormatPath(filePath) ?? "", line);
    }
 
    [Event(4, Level = EventLevel.Warning, Keywords = Keywords.Logging, Message = "[{2}:{3}][{1}]{0}")]
    public void Warning(string message, [CallerMemberName] string memberName = "", [CallerFilePath] string filePath = "", [CallerLineNumber] int line = 0)
    {
        WriteEvent(4, message ?? "", memberName ?? "", FormatPath(filePath) ?? "", line);
    }
 
    [Event(5, Level = EventLevel.Informational, Keywords = Keywords.Logging, Message = "[{2}:{3}][{1}]{0}")]
    public void Informational(string message, [CallerMemberName] string memberName = "", [CallerFilePath] string filePath = "", [CallerLineNumber] int line = 0)
    {
        WriteEvent(5, message ?? "", memberName ?? "", FormatPath(filePath) ?? "", line);
    }
 
    [Event(6, Level = EventLevel.Verbose, Keywords = Keywords.Logging, Message = "[{2}:{3}][{1}]{0}")]
    public void Verbose(string message, [CallerMemberName] string memberName = "", [CallerFilePath] string filePath = "", [CallerLineNumber] int line = 0)
    {
        WriteEvent(6, message ?? "", memberName ?? "", FormatPath(filePath) ?? "", line);
    }
 
    [Event(7, Level = EventLevel.Error, Keywords = Keywords.Logging, Version = 1)]
    public void Exception(string type, string stackTrace, string message)
    {
        WriteEvent(7, type ?? "", stackTrace ?? "", message ?? "");
    }
 
    [Conditional("DEBUG")]
    [Event(8, Level = EventLevel.Verbose, Keywords = Keywords.Logging, Message = "[{2}:{3}][{1}]{0}")]
    public void Debug(string message, [CallerMemberName] string memberName = "", [CallerFilePath] string filePath = "", [CallerLineNumber] int line = 0)
    {
        WriteEvent(8, message ?? "", memberName ?? "", FormatPath(filePath) ?? "", line);
    }
 
    [NonEvent]
    public IDisposable MeasureExecution(string label, [CallerMemberName] string memberName = "", [CallerFilePath] string filePath = "", [CallerLineNumber] int line = 0)
    {
        return new StopwatchMonitor(this, label ?? "", memberName ?? "", FormatPath(filePath) ?? "", line);
    }
 
    [Event(9, Level = EventLevel.Informational, Keywords = Keywords.Logging, Message = "[{0}][{2}:{3}][{1}]{4}ms")]
    void MeasureExecution(string label, string memberName, string filePath, int line, double duration)
    {
        WriteEvent(9, label ?? "", memberName ?? "", FormatPath(filePath) ?? "", line, duration);
    }
 
    class StopwatchMonitor : IDisposable
    {
        readonly LoggerEventSource logger;
        readonly string label;
        readonly string memberName;
        readonly string filePath;
        readonly int line;
        Stopwatch stopwatch;
 
        public StopwatchMonitor(LoggerEventSource logger, string label, string memberName, string filePath, int line)
        {
            this.logger = logger;
            this.label = label;
            this.memberName = memberName;
            this.filePath = filePath;
            this.line = line;
            stopwatch = Stopwatch.StartNew();
        }
 
        public void Dispose()
        {
            if (stopwatch != null)
            {
                stopwatch.Stop();
                logger.MeasureExecution(label, memberName, filePath, line, stopwatch.Elapsed.TotalMilliseconds);
                stopwatch = null;
            }
        }
    }
}

ちょっと長いですが、これで

LoggerEventSource.Log.Debug("ほげほげ!");

とか書いていくだけです。それを書いたアプリを、LINQPadでは

ObservableEventListener.FromTraceEvent("LoggerEventSource").DumpWithColor();

で、ファイルとかを通さずそのままストリームで外から観測できます。

Logs are event streams

脱ファイル。ちなみにETWで流したのは最終的にBigQueryに流すのが超おすすめですね!そしてLINQ to BigQuery + LINQPadで解析する。完璧!これがC#の次世代ログのあるべき姿だ!と、オモイマス。というか逆にもう以前には戻れないかなあ、やっぱり世代が一つ変わった感あります、便利度が全然違うので。

LINQPad Driver + LINQ to BigQueryによるBigQueryデスクトップGUIクライアント

Happy signed!何かというと、長らく署名の付いていなかったGoogle APIの.NET SDKに署名が付いたのです!署名が付くと何ができるかというと、LINQPadのDriver(プラグイン)が作れます。LINQPadのDriverは署名なしだと起動できないので……。正直、私ももはや署名とか全然重視してないし100億年前の化石概念の負の異物だろ、ぐらいに思ってなくもないのですが、さすがに、LINQPad Driverを作れない、という事態には随分と嘆いたものでした。が、やっと作ることが出来て感無量。そして、実際動かしてみると相当便利ですね。これがやりたかったんですよ、これがー。

LINQ to BigQueryのLINQPad Driverが可能にする範囲は、

  • サイドバーでのスキーマのツリー表示
  • thisを読み込んでいるConnectionで認証済みのBigQueryContextに変更
  • 関連するアセンブリと名前空間を自動で読み込み
  • スキーマに対応するクラスを動的に生成/読み込み
  • ちょっとしたユーティリティDumpの追加(DumpRun/DumpRunToArray/DumpChart/DumpGroupChart)
  • もちろんクエリのローカルでの保存/読み込みが可能

です。元々のLINQ to BigQueryが提供している機能としては

  • TableDateRangeに対するサポート
  • DateTimeの自動変換(一部のBigQueryの機能はUnix Timestampで書く必要があり、実質手で書くのは不可能なものもありましたが、自動変換により救われる)
  • 結果セットをローカル時間に自動変換(基本的にUTCで帰ってくるので、ローカル時間で考える際に+9時間しなきゃいけなかったりしますが、C#側でデシリアライズする際にローカルタイムに自動変換する)
  • 全てが型付きで入力補完が全面的に効く
  • 全てのBigQuery関数の入力補完にドキュメント付き

があって(この辺の詳しい話は以前に書いたLINQ to BigQuery - C#による型付きDSLとLINQPadによるDumpと可視化を見てください)、相乗効果でかなり強まったのではないでしょうか。

公式ウェブコンソールで叩くのとどっちがいいかといったら、まぁ私自身も結構、ウェブから叩くのは多かったりしますので、どっちでもいいといえばいいんですが、それもプラグインを作る前は……かしら。今後は私自身もLINQPad利用が増えるかなー。明らかにウェブから叩くのじゃ提供できない機能というか、素のBigQuery SQLじゃ中々できない機能を多く提供しているわけで、LINQPad + LINQ to BigQUeryにはかなりのアドバンテージがあります。

Excel統合

問答無用に愚直なExcel統合があります。

legendary_dump_to_excel

そう、DumpToExcel()で実行すると結果セットがダイレクトにExcelで開く……。しかし実際こういうのでいいんだよこういうので感あります。Excelでクエリ書く系の統合は面倒くさい(実際アレはダルいのでない)。いちいちCSVに落として開くのは面倒くさすぎる。LINQPadでクエリ書く、結果がExcelで見れる。あとはピボットテーブルなりで好きに分析できる。そう、そういうことなんですよ、これなんですよ #とは

入れ方

ExplorerのAdd Connection→View More Drivers からLINQ to BigQueryを探して、clickでインストールできます。簡単。

image

かなり上の方のいい位置に入れてもらいました!

using static

BigQueryの関数はLINQ to BigQueryではBqFunc以下に押し込める形をとっていますが、C# 6.0から(Javaのように)静的メソッドのインポートが可能になりました。また、LINQPad 5でもスクリプトのバックエンドがRoslynになり、C# 6.0にフル対応しています。LINQ to BigQueryのDriverでは、LINQPad 5以上に読み込ませた場合のみ、using static BigQuery.Linq.BqFunc が自動インポートされます。

これにより、クエリを書いた際の見た目がより自然に、というかウザったいBqFuncが完全に消え去りました!関数名を覚えていない、ウロ覚えの時はBqFunc.を押して探せるし

image

慣れきった関数なら、直接書くことができる。完璧。

How to make LINQPad Driver

難しいようで難しくないようで難しいです。しっかりしたドキュメントとサンプルが付属しているので、スタートはそれなりにスムーズに行けるかと思います。一つ、大事なのはプラグイン開発だからってデバッグ環境に妥協しないでください。ふつーの開発と同じように、F5でVisual Studioが立ち上がってすぐにブレークポイント貼ってステップ実行できる環境を築きましょう。細かいハマりどころが多いので、それ出来ないと挫けます。逆に出来てれば、あとは気合、かな……?細かいやり方はここに書くには余白が(以下略

変わったハマりどころとしては、例えば別々に呼ばれるメソッド間で変数渡したいなー、と思ってprivate fieldに置くと、そもそも都度頻繁にコンストラクタが呼ばれて生成されなおすので、共有できない。なるほど、じゃあせめてstatic変数だったらどうだろうか?というと、LINQPadの内部の実行環境の都合上、AppDomainがガンガン切られて飛んで来るので、static fieldすら消える!マジか!なるほどねー厳しいねー、などなど。

ちなみに動的なアセンブリ生成ではCodeDomのCSharpCodeProviderを利用しています。つい先月、Metaprogramming Universe in C# - 実例に見るILからRoslynまでの活用例でCodeDomはオワコン、使わないとか言ってたくせに!舌の根も乾かぬうちに自分で使うことになるとは思わなかった!

まとめ

社内でのBigQuery活用法として、定形クエリのダッシュボードはDomoにより可視化、アドホックなクエリはLINQPad + LINQ to BigQueryによりクエリを色々書いたり、そのままExcelに送り込んで(LINQPadはデスクトップアプリなので、DumpToExcel()メソッドとかを作ることによりシームレスに結果セットをExcelに投げ込んだりできるのも強い)PowerPivotでこねくり回したり、などをしてます。とはいえ、今までは事前にスキーマに対応するクラスを生成して保存しておかなければならないという面倒くささがあったので、イマイチ活用しきれてなかったのも事実。実際、私自身ですらBigQueryの公式ウェブコンソールでクエリ叩いたりが多かったですし。それが、今回のLINQPad Driverにより圧倒的に利便性が上がった(というか前のがもはや原始時代に見える)ので、使える度合いが桁違いに上がったんじゃないかなー、と思います。

デスクトップGUIクライアントの便利さは、例えばMySQLだったらウェブでphpMyAdminよりもHeidiSQLやMySQL Workbenchのほうが100億倍便利なわけでして、良いところ沢山あるんですよね。BigQuery関連だとCloud DataLabなんかもちょうど出ましたが、ウェブとデスクトップ、それぞれ良さがあるので、ここはうまく使い分けていきたいところです。

最近のBigQueryのアップデートへの追随だと、新メソッドは全部実装が完了してます。また、GroupByへのRollupなど文法の追加もOK。ただ、大きな目玉であるUDF(User Defined Function)への対応がまだです。別にそんな難しくもないんですが、APIの馴染ませ方どうしようかな、とか思ってる間にLINQPad Driverの作成に時間喰われたので、対応入れるのは近いうちの次回ということで。

同期(風)コードと対比させたUnity+UniRxで非同期を扱う場合のパターン集

UniRxのGitHubのStar数が500行きました!

image

今のところGitHub上でのUnity + C#でスター順の検索だと、世界5位です。おおー。更に上を狙いたいところですね。最近はちょっと更新が滞っていますが、ネタはあるのでより完成度を高めたい。(滞った理由は、PhotonWireとか色々他のところに手を出していたため……)

さて、本題。イベント結合に使う際はあてはまりませんが、Rx(UniRx)を非同期(長さ1のIOservableシーケンス)として扱う場合、それなりに癖があります。とはいえ、基本的には同期(或いはyield return)で書いていた際と、1:1で対比できるパターン化した形で概ね対応できるので、そのためのチートシートを考えてみました。コード例はC# 5.0のasync/awaitで出しますが、同期コード or IEnumeratorと同じように思ってもらえればいいです。例えば

public void Sync()
{
    /* before action */
    Method();
    /* after action */
}
 
public IEnumerator IEnumerator()
{
    /* before action */
    yield return StartCoroutine(Method());
    /* after action */
}
 
public async Task Task()
{
    /* before action */
    await MethodAsync();
    /* after action */
}

みたいな感じです、awaitに馴染みのない人も、なんとなくイメージしながら眺めてみてもらえると嬉しいです。

非同期汚染

コード例の前に非同期汚染、或いは非同期の伝搬について。まぁ、あんまし汚染という言い方は好きじゃないのですが、基本的に非同期、つまりTaskでもFutureでもPromiseでもIObservableでも、は、下層から上層まで伝搬していきます。メソッドが非同期であるなら戻り値はIObservableであり、そのIObservableを呼ぶメソッドもまた自然と非同期でなければならないので、IObservableになる、と。何故非同期の連鎖でなければならないのか。消費(Subscribe)してしまうと、その瞬間Fire and Forgetになってしまい、戻りを待ったりキャンセルしたりなどの別の操作が行えなくなってしまうからです。別にFire and Forgetしたければ、呼び元がそれを選択(Subscribeして放置)すればいいわけで、呼ばれる側が決定することではない。

もちろん、最終的にはどこかの層で消費(Subscribe)しなければならないので、そこで伝搬は止まるのですけれど、それは、基本的には上層であればあるほどよいということですね。どこが上層やねんって話はあるかもしれませんが、ユーザーインタラクションに近かったり、MonoBehaviourのイベント層に近かったり、あたりがそうですかねー。あとは、ごく一部でしか使わないんだ!という確固たる思いがあれば、早い段階でSubscribeして伝搬を止めるのも策ではあります、その辺はケースバイケースで。

非同期の伝搬に都合の良いメソッドが現状のUniRxには足りてません。実は!というわけで、次期バージョンではForEachAsyncというものを足したいのですが、それまでは以下のものをコピペって代用してください。挙動的にはシーケンスを消費して長さ1のIObservable[Unit]を返すもので、元シーケンスが非同期(長さ1)ならDoやSelectと、概ね一緒です。

// 次期バージョンに入るので、それまでの代用ということで。
// 元シーケンスが非同期なら .Select(x => { /* action(); */ return Unit.Default; }) とほぼ同様
namespace UniRx
{
    public static class UniRxExtensions
    {
        public static IObservable<Unit> ForEachAsync<T>(this IObservable<T> source, Action<T> onNext)
        {
            return Observable.Create<Unit>(observer =>
            {
                return source.Subscribe(x =>
                {
                    try
                    {
                        onNext(x);
                    }
                    catch (Exception ex)
                    {
                        observer.OnError(ex);
                        return;
                    }
                }, observer.OnError, () =>
                {
                    observer.OnNext(Unit.Default);
                    observer.OnCompleted();
                });
            });
        }
    }
}

また、副作用(外の変数への代入など)に関しては、あまり気にしないほうが吉です。いや、Rxのパイプラインに押し込めたほうが美しくはあるんですが、それがオブジェクトであるなら、副作用かけてフィールド変数を変えたり、ReactivePropertyに結果を伝えたりとかは、あって然りかな、と。考える際には「もしこれが同期コードだったらどうなのか」を意識したほうがいいかもしれません、同期コードで自然なら、別にRxでそれを行っても、構わないのです。とはいえ、以下に紹介するコードは全部、副作用大前提みたいな説明なので、それはそれで若干の狂気でもありますが、その辺は慣れてきてからでよいかと。

戻り値のない場合

public async Task Demo1_TaskAsync()
{
    /* before action */
    var x = await Task.Factory.StartNew(() => 100);
    /* after action */
}
 
public IObservable<Unit> Demo1_IOAsync()
{
    /* before action */
    return Observable.Start(() => 100)
        .ForEachAsync(_ =>
        {
            /* after action */
        });
}

メソッドに戻り値がない場合は、awaitの位置にForEachAsyncで、その中にactionを書く形になります。RxにおいてはIObservable[Unit]を戻り値のないことの表明として使います。

内部に複数の非同期がある場合

public async Task Demo2_TaskAsync()
{
    /* before action */
    var x = await Task.Factory.StartNew(() => 100);
    /* after action 1 */
    var y = await Task.Factory.StartNew(() => 200);
    /* after action 2 */
}
 
public IObservable<Unit> Demo2_IO_1Async()
{
    /* before action */
    return Observable.Start(() => 100)
        .SelectMany(x =>
        {
            /* after action 1 */
 
            return Observable.Start(() => 200);
        })
        .ForEachAsync(y =>
        {
            /* after action 2 */
        });
}

awaitの位置にSelectManyを置くことで繋げることができます。最後の消費だけForEachAsyncで。

パイプライン中に複数の値を伝搬したい場合

public IObservable<Unit> Demo2_IO_2Async()
{
    /* before action */
    return Observable.Start(() => 100)
        .SelectMany(x =>
        {
            /* after action 1 */
            return Observable.Start(() => 200);
        }, (x, y) => new { x, y }) // transport argument to next chain
        .ForEachAsync(o =>
        {
            /* after action 2 */
            // { o.x, o,y } 
        });
}
 
public IObservable<Unit> Demo2_IO_2_2Async()
{
    /* before action */
    return Observable.Start(() => 100)
        .SelectMany(x =>
        {
            /* after action 1 */
            var z = SyncMethod();
            return Observable.Start(() => 200).Select(y => new { x, y, z });
        })
        .ForEachAsync(o =>
        {
            /* after action 2 */
            // { o.x, o,y, o.z } 
        });
}

同期コードでは、そのスコープ中の全ての値が使えるわけですが、Rxのメソッドチェーンでは次のパイプラインに送り込める値は一つしかありません。というわけで、匿名型(もしくはUniRx.Tuple)を使って、次のパイプラインへは値をまとめて上げる必要があります。SelectManyには第二引数があり、それにより前の値と次の値をまとめることができます。また、SelectMany内部で作った値を送り込みたい場合は、戻り値のところでSelectを使ってスコープ内でキャプチャして返してあげればいいでしょう。(匿名型、Tupleともにclassなので、気になる場合はstructの入れ物を用意してもいいかもしれない、何か箱を作って運搬しなきゃいけないのは残念ながら仕様です)

非同期が連鎖する場合

public IObservable<Unit> Demo2_IO_2_MoreChainAsync()
{
    /* before action */
    return Observable.Start(() => 100)
        .SelectMany(x =>
        {
            /* after action 1 */
            return Observable.Start(() => 200);
        }, (x, y) => new { x, y })
        .SelectMany(o =>
        {
            /* after action 2 */
            return Observable.Start(() => 300);
        }, (o, z) => new { o.x, o.y, z }) // re-construct self
        .ForEachAsync(o =>
        {
            /* after action 3 */
            // { o.x, o,y, o.z } 
        });
}

SelectManyの連打になります。また、伝搬する値は自分で分解して付け直してあげる必要があります、これは面倒くさいですね!この辺はクエリ構文を使った場合、Transparent Identifierという仕組みで自動的にコンパイラが行うのですが(An Internal of LINQ to Objectsの35P、Rxでクエリ構文は結構頻繁にクエリ構文の範疇を逸脱するのと、副作用をパイプライン途中に書けないためあまり使い勝手は良くないので、面倒くさいながら手作業再構築を薦めます。

戻り値を返す場合

public async Task<int> Demo3_TaskAsync()
{
    /* before action */
    var x = await Task.Factory.StartNew(() => 100);
    /* after action */
    return x; // return value
}
 
public IObservable<int> Demo3_IOAsync()
{
    /* before action */
    return Observable.Start(() => 100)
        .Select(x =>
        {
            /* after action */
            return x; // return value
        });
}

ForEachAsyncではなく、Selectを使っていきましょう。戻り値の型が同一で副作用だけ起こしたいならDoでも構わないのですが、まぁどっちでもいいです。また、awaitが複数になる場合は、SelectManyになります。そのうえでSelectManyのままreturnするか、最後に再びSelect(もしくはDo)を使うかどうかは、状況次第、かな。

例外をキャッチ

public async Task Demo4_TaskAsync()
{
    /* before action */
    try
    {
        var x = await Task.Factory.StartNew(() => 100);
    }
    catch (Exception ex)
    {
        /* onerror action */
        throw;
    }
 
    /* after action */
}
 
public IObservable<Unit> Demo4_IOAsync()
{
    /* before action */
    return Observable.Start(() => 100)
        .Catch((Exception ex) =>
        {
            /* onerror action */
            return Observable.Throw<int>(ex);
        })
        .ForEachAsync(x =>
        {
            /* after action */
        });
}

これはCatchで賄えます。なお、Catchメソッドを使う際は、Catch<T>で例外の型を指定するよりも、ラムダ式の引数側で例外の型を書いたほうが書きやすいです(そうしたほうが型推論の関係上、ソースシーケンスの型を書かなくて済むため)。Catchの戻り値では再スローをObservable.Throw、握りつぶしをObservable.Return/Emptyで表現可能です。

Finally

public async Task Demo5_TaskAsync()
{
    /* before action(1) */
    try
    {
        var x = await Task.Factory.StartNew(() => 100);
    }
    finally
    {
        /* finally action(2) */
    }
 
    /* after action(3) */
}
 
// not equivant try-finally
public IObservable<Unit> Demo5_IO_PseudoAsync()
{
    /* before action(1) */
    return Observable.Start(() => 100)
        .Finally(() =>
        {
            /* finally action(3) */
        })
        .ForEachAsync(x =>
        {
            /* after action(2) */
        });
}
 
public IObservable<Unit> Demo5_IO_CorrectLightweightButIsNotDryAsync()
{
    /* before action(1) */
    return Observable.Start(() => 100)
        .Do(_ => { /* finally action(2) */}, _ => {/* same finally action(2) */})
        .ForEachAsync(x =>
        {
            /* after action(3) */
        });
}

Finallyに関しては、実は同じに扱える表現がありません!RxのFinallyはパイプラインの終了時の実行なので、実行順序がベタtry-finallyで書いた時と異なるんですよねえ。いちおう、DoでOnNextとOnErrorのところに同じコードを書くことでそれっぽい表現は可能ではありますが……。

並列処理

public async Task ParallelAsync()
{
    var a = Task.Factory.StartNew(() => 100);
    var b = Task.Factory.StartNew(() => 200);
    var c = Task.Factory.StartNew(() => 300);
 
    var xs = await Task.WhenAll(a, b, c);
    /* after action */
}
 
 
public IObservable<Unit> ParallelIO()
{
    var a = Observable.Start(() => 100);
    var b = Observable.Start(() => 200);
    var c = Observable.Start(() => 300);
 
    return Observable.WhenAll(a, b, c)
        .ForEachAsync(xs =>
        {
            /* after action */
        });
}

並列処理は非同期固有の実行ですが、WhenAllでドバッとまとめるというのが基本方針。

タイムアウト

public async Task TimeoutAsync(TimeSpan timeout)
{
    var task = Task.Factory.StartNew(() => 100);    
    var delay = Task.Delay(timeout);
    if (await Task.WhenAny(task, delay) == delay)
    {
        /* timeout action */
        throw new TimeoutException();
    }
    /* after action */
}
 
 
public IObservable<Unit> TimeoutIO(TimeSpan timeout)
{
    return Observable.Start(() => 100)
        .Timeout(timeout)
        .Catch((TimeoutException ex) =>
        {
            /* timeout action */
            return Observable.Throw<int>(ex);
        })
        .ForEachAsync(x =>
        {
            /* after action */
        });
}

タイマウトも非同期固有の処理。async/awaitの場合、特有のイディオムがあります。UniRxの場合はTimeoutだけでOK。特に例外時に処理するものもないなら、Catchは不要です。

IEnumeratorに戻す

public IObservable<Unit> Demo6_IE()
{
    /* before action(1) */
    return Observable.FromCoroutine(() => Demo6_IECore());
}
 
IEnumerator Demo6_IECore()
{
    // 戻り値の不要な場合
    yield return Observable.Start(() => 100).StartAsCoroutine();
 
    int ret;
    yield return Observable.Start(() => 100).StartAsCoroutine(x => ret = x);
}

SelectManyの連打が辛い場合、ふつーのコルーチンに戻して、更にIObservableでラップするという手段も取れます。まあ、この辺は複雑さ度合いで自由に!

だったらもはや最初から全部コルーチンでええやん!Rxでメソッドチェーン複雑だし見た目だけならコルーチン最強にスッキリじゃん!というのは正しい。正しいんですが、例外処理・戻り値・合成可能性・並列処理・マルチスレッド、などといった要素が欠落してるので、コルーチンはコルーチンで苦しいところが多いというか実際のところシンプルなケース以外では相当苦しいので、基本的にはRxのほうが有利です。

async/awaitは必要?

みたとーり、必要です。どう考えても。さすがにSelectManyの連打を同期コードほどスッキリと言い張るのは無理があるでしょう。とはいえまぁ、書いて書けないこともないので、今あるツールの中でベストを尽くすのまた良きかな、とは思いますねー。というわけで良き非同期生活を!UniRxでイベントを扱う際のパターン集は、またそのうちにでも!

実例からみるC#でのメタプログラミング用法集

Metaprogramming Universe in C# - 実例に見るILからRoslynまでの活用、という題でMetro.cs #1にて話してきました。

Metaprogramming Universe in C# - 実例に見るILからRoslynまでの活用例 from Yoshifumi Kawai

現在、PhotonWireというフレームワークを作っているのですが、それで使ったメタプロ技法を紹介しました。ExpressionTree, T4, ILGenerator, Roslyn(Analyzer), Mono.Cecilとそれなりに満遍なく使っているので、それらをどーいう時に使えばいいのかというヒントになれば幸いです。まとめに書きましたが、手法自体は少なくするに越したこたぁないです、メタプロってついやりすぎちゃう傾向にあるんで、目的Firstを忘れないようにしないと本末転倒になりがちです。あと、それぞれは別にそんなに難しくない、というか難しくやらないようにするのが良いですね、そもそも長い式木とか長いILとか書きたくないですし……。

Proxyのvirtual強制は制約強くてゲロなので喪われた技法って感じですが、Roslyn Analyzerでコンパイラエラーに制限できることによって復活したかもしれない気がするかもしれない!あと、Taskは大事ですね、非同期のシグネチャとしてTaskで表明できるようになったのはひじょーーーーーーに大きな事です。これは実際めちゃくちゃ大きなことなのに過小評価してたり勘違いしてたりすると、いくない。もちろん、async/awaitで手軽にハンドリングできるようになったことも大事。RPC Revisitedですよ。そんなわけでごった煮しつつも、私的な独断と偏見に基づくバランス感覚で取捨してます。この辺の感覚はかなり大事だと思うんだな。

なお、書籍ではメタプログラミング.NETが良書なのでオススメです。

PhotonWire

題材のPhotonWireは、グラニで現在開発中のリアルタイムネットワーク通信用フレームワークです(ところでUnity、特にUniRxをゴリゴリ活用した先端的(エキセントリックともいう)なスクリプティング環境や、クライアントからサーバーまで全てC#で統一したあいそもーふぃっくな開発に興味のある人はいつでもウェルカムで採用募集中です)。といってもレイヤー的には比較的高レベルで、下回りではPhoton Serverというミドルウェアを採用していて、その上のRPCフレームワークを提供という感じです。キャッチコピーは「Typed Asynchronous RPC Layer for Photon Server + Unity」ということで、特にUnityとの繋ぎ込みを重視していて、クライアント-サーバー、サーバー-クライアント、サーバー-サーバーの方向のRPCを提供します。クライアント-クライアントは非サポート(あれは百害あって一利なし)。

クライアント-サーバーはご存知SignalR、サーバー-サーバーはOrleansという分散アクターフレームワークのAPIを参考にしています、が、サーバーの分散に関しては、別に全然賢くないです。というか機能全く無いです。もともとのPhotonがそこに対するサポートがゼロで、PhotonWireでもたいしたサポートを入れてません。私的にはこの素朴な割り切りは結構好きですね。変に透過的に見せるよりも、それぞれのサーバー/それぞれのレイヤーを独立して、ある程度プリミティブな操作を可能にしたほうがはまりどころも少ないし。別に賢くはないんだけど、手堅い。ゲームという用途で考えると、あまりカシコイものよりも、愚直なシステムのほうがマッチしそうな感触があります。必要になったら、まぁ適当に考える。

Photon(+Unity)にはもともとPhoton Unity Network(PUN)という高レベルなクライアントが用意されているのですが、正直あんまり良いものでもない(特にPhoton Serverで自前ロジックを入れてくような場合は)ので、無視です、無視。で、PUNを通さない低レベルのSDKもあって、こちらは相当低レベルで本当に接続とデータ転送しか提供していないので(ただし低レベルSDKとしてはこのぐらいのほうが好ましい、へたに変なのがゴチャゴチャついてるよりも)、サーバーSDK(こちらもかなり低レベル)ともども統一した形で、ちょっと高レベルなもの、ぐらいの位置づけで作り上げてみました。

クラスとメソッドに属性でIDつけさせて、それで振り分けしているのでJSON-RPC的なメソッド名なども送っちゃうのでサイズが大きくなる、ということはなく、通常の転送に較べてもオーバーヘッドは2byteです。別に全然ない。ユーザー定義の型を送る場合(通常のPhotonはこれをサポートしてない)はMsgPack-CLIでシリアライズ/デシリアライズするため、その際の容量増大も極小です。また、シリアライザ/デシリアライザはその型に合致したものを事前生成するため、Unityにおいても高速に動作させられます、といったシステムも含まれています。

デバッグ用の専用クライアント(WPF製)なども込み込みで(これのデザイン面の話はMaterial Design In XAML Toolkitでお手軽にWPFアプリを美しくに書いてます)、痒い所に手が届きつつも、機能自体は小さく「型付きの非同期RPCの提供」から逸脱しない程度におさめているので、まーまー使いやすいんじゃないかなー、と思いますね。もちろん、クライアント側はUniRx前提です。

UniRx同様、GitHub/AssetStoreでの公開予定はあるというか、早く公開したいんですが、Photonの次バージョンのベータ版を使って開発してるので、そっちが正式リリースされないと公開できないので早く出ないかなぁ(チラッ とオモッテマス。

Material Design In XAML Toolkitでお手軽にWPFアプリを美しく

なんとブログ書くのは3ヶ月ぶり近い!えー、うーん、そんな経っちゃってるのか、こりゃいかん。と、いうわけかでWPFアプリを入り用で作ったんですが、見た目がショボくてゲッソリしてました。WPFでアプリ書いても別に綺麗な見た目にならんのですよね、むしろショボいというか。自分でデザイン作りこんだりなんて出来ないし、でもWPFのテーマ集なんかを適用してもクソダサいテーマしかなかったりして一層ダサくなるだけで全く意味ないとかそんなこんなんで、まぁ割とげっそりだったのですが、Material Design In XAML Toolkitは相当良い!良かった、のでちょうど手元に作り中のWPFアプリがあって適用してみたんで紹介してきます。

最終的に↑のような感じになりました。サクサクッとテーマ適用してくだけでこの程度に整えられるならば、上等すぎるかな、と。私的にはマテリアルデザイン、相当気に入りました。WindowsのModern UI風のフラットテーマは普通に適用しただけだと超絶ダサくなるという、センスが要求されすぎてキツかったんですが、マテリアルデザインはそれなりに質感が乗っかってるのでまぁまぁ見れる感じになる。また、画像からは分かりませんが結構細かくアニメーションが設定されていて感触が良い(マテリアルデザインの重要な要素だそうで)のも嬉しい。

Before

Beforeはこんな感じです。

TextBoxとボタンの羅列、実にギョーミーな雰囲気。機能的には私の要件はこれで満たしてるんですが(ちなみにコレが何かは後日紹介するしGitHubで公開もするつもりですが今は本題ではないのでスルーします)、いかんせん見た目が悲しいかな、と。そこで現れたMaterial Design In XAML Toolkit!NuGetからのインストールとコピペ一発で素敵な見た目に……。 なるほど世の中はさすがに甘くなかったですね:)

適用は簡単で、NuGetからMaterialDesignThemesをダウンロード、そしてApp.xaml.csにこのApp.xamlのApplication.Resourcesをコピペ。そしてMainWindowに以下の4項目を貼っつけてあげればできあがり。

<MainWindow
    xmlns:wpf="clr-namespace:MaterialDesignThemes.Wpf;assembly=MaterialDesignThemes.Wpf"
    TextElement.Foreground="{DynamicResource MaterialDesignBody}"
    Background="{DynamicResource MaterialDesignPaper}"
    FontFamily="pack://application:,,,/MaterialDesignThemes.Wpf;component/Resources/Roboto/#Roboto">

簡単簡単。これで美しくなるなら素晴らしいですね?そしてその結果がこれ。

うん、ダメ、理想とは程遠いダサさに溢れてます。Bootstrapを適用しただけじゃ普通にダサいままってのと同じ。ボーダーが吹っ飛んだので境目がわからず使いにくくなったし、やっぱダサ……、なんか一部のボタンは文字埋まっちゃってるし。で、引き返そうと思ったんですが、なんとなく良さそうな気配は感じたのでもう少し粘って作業することにしました。

デモアプリを見ながら細工

まずMaterialDesignInXamlToolkitのプロジェクトを落としましょう。CloneしてもいいしDownload Zipでもいいので。で、MainDemo.Wpfをビルドして実行しましょう、特に躓くことなくビルドできるはずですので。このデモアプリが非常によく出来ていて、出来ること全ての解説になってますし、当然それをやりたければそのxamlを開いてコピペすればなんとかなります!

というわけでデモアプリを眺めつつ自分のクソダサアプリのどこから手を入れようか。まず画面の構成要素のうち、上の部分のテキストボックスとボタンが並んでるところはコンフィグに近いので色分けしようかな、と。ヘッダ部分の色分け例はマテリアルデザインでよく見るパターンですしね。よく見るパターンということは、専用のパーツがしっかり用意されています。ColorZoneで囲むことで色がガラッと変わります。

<wpf:ColorZone Mode="Inverted" Padding="0">
    ...
</wpf:ColorZone>

ModeのInvertedは逆転した色、というわけで、これだけでまぁまぁ引き締まった雰囲気が出てきました、これはやって正解。また、ボタンの文字が埋まっているのはMargin入れて小さくしてたせいだったので、Heightを設定する形で小さくすることにしました。この状態でちょっとだけ問題があって、コンボボックスの選択時のフォントが通常カラーのままなので色が薄く見えなくなってしまうことに……。

これはテーマから外れたItemContainerStyleを設定して回避。

<ComboBox ItemsSource="{Binding UseConnectionType}">
    <ComboBox.ItemContainerStyle>
        <Style TargetType="ComboBoxItem">
            <Setter Property="Foreground" Value="Gray" />
        </Style>
    </ComboBox.ItemContainerStyle>
</ComboBox>

よくわからんけどこんなんでいいでしょふ、よくわからんけど。真面目にXAML書くの5年ぶりぐらいなんで正直もう全然覚えてないんですよね。

そういえば、オマケコントロール(?)としてTextBoxにウォーターマークがつけれるのが入ってます。使い方はwpf:TextFieldAssist.Hintを入れるだけ。

<TextBox wpf:TextFieldAssist.Hint="うぉーたーまーく" />

かなり綺麗に出て素敵なので最高だと思いました、まる。

MahAppsの導入

タイトルウィンドウが乖離しててダサいというか気になってきた。ので、ここを手軽に改変できるMahAppsを入れましょう。MahAppsだけだと、Metro風ということでこれ単体では別に素敵な見た目に出来ないんですが(ほんとメトロ風はムズカスぃ!)、Material Design In XAML Toolkitと合わせるとお互いの領域をカバーできる。ちゃんとMaterial Design In XAML Toolkit側で統合のための設定が用意されているので組み合わせるのは簡単です。MahAppsの基本的な導入はQuick Startに従う通り、まずWindowをMetroWindowに差し替えて

// Xaml 
<Controls:MetroWindow
    xmlns:Controls="clr-namespace:MahApps.Metro.Controls;assembly=MahApps.Metro">
 
// CodeBehind
public partial class MainWindow : MahApps.Metro.Controls.MetroWindow

App.xamlにリソースを投下、なのですが、MaterialDesignInXamlToolkitと統合するためのサンプルがMaterialDesignInXamlToolkit側に用意されているので、リソースはMahMaterialDragablzMashUp/App.xamlからコピペってきましょう。DragablzというChromeみたいなドラッグアンドドロップで切り離せるタブのためのライブラリを使わない場合(今回は使いませんでした)は、Dragablzに対する行は削除しておk(というか削除しないと動きません)。これで

となりました。うーん、よくなってきた!タイトルバーのところにテキストでいい感じなレイアウトで手軽にコマンドを突っ込めるのも嬉しかった。というわけでBeforeではステータスバーのところにやけくそにダサい感じで置いてたDuplicate Windowボタン(ウィンドウを複製する)をタイトルバーに移動。ついでにAlign Window(複数ウィンドウを整列させる)コマンドも追加。ちなみにこのアプリは複数ウィンドウを並べて使うのが前提なので、並べた時に重なって鬱陶しいためウィンドウ枠を光らせるのはあえて切ってるんですが、単体アプリなら光らせたほうが見栄え良いかもですね。入れるの自体は簡単で

<!-- 光らせるところ、GlowBrushを削れば光らない -->
<Controls:MetroWindow
    GlowBrush="{DynamicResource AccentColorBrush}">    
 
    <!-- コマンド入れるところ -->
    <Controls:MetroWindow.RightWindowCommands>
        <Controls:WindowCommands>
            <Button Content="Align Window" Click="AlignWindow_Click" />
            <Button Content="Duplicate Window" Click="DuplicateWindow_Click" />
        </Controls:WindowCommands>
    </Controls:MetroWindow.RightWindowCommands>

をMainWindows.xamlに突っ込むだけです。お手軽素敵。

最終調整

Purpleじゃない色調にしたかったのでテーマをデモアプリのパレットから眺めてBlueGrayに決定。テーマはApp.xamlを弄ればヨイデス。MaterialDesignColor.xxx.xamlの部分ですね、他の色とかはデモアプリのPaletteで確認できます。その他Light/Darkの切り替えやSecondaryColourの設定なんかも、xxx.xamlのそれっぽい部分をなんとなく書き換えれば書き換わります。

<!-- include your primary palette -->
<ResourceDictionary.MergedDictionaries>
    <ResourceDictionary Source="pack://application:,,,/MaterialDesignColors;component/Themes/MaterialDesignColor.BlueGrey.xaml" />
</ResourceDictionary.MergedDictionaries>

これで全体の色が変わったので、最後に、中央部分がMarginが消えてて区切りめがわからず使いづらいのは変わらずだったので、ここは枠をいれて明確な分離を。最初はボーダー入れて調整とかしてみたんですがイマイチしっくりこなかったんで、まぁ枠かな、と。マテリアルデザイン風のシャドウのある枠はヘッダーで色分けした時と同じく ColorZone で囲むだけです、ModeはStandardを選択。モードがどんなのがあるかもデモアプリを見れば一発で分かります。

<wpf:ColorZone Mode="Standard" Padding="5" CornerRadius="3" Effect="{DynamicResource MaterialDesignShadowDepth1}" Margin="2">
    <local:OperationItem />
</wpf:ColorZone>

影の出方はMaterialDesignShadowDepthの1~5で調整可能で、今回は1にしてます。その他の調整として、ログを表示しているテキストボックスのボーダーを上にも出すようにしたり、中身によって拡縮するようになっちゃたのでVerticalContentAlignmentを設定したりとちょっとした調整を少し入れて、最初に出した画像のものになりました。もっかい同じのを載せますけれど。

アプリの見た目が良くなるってのは純粋にテンション上がるんでいいものですねぇ、機能的には何も変わっちゃいないですが、気分は随分と良いです。まぁギョームコウリツとは関係ないとこなんであんまり手を入れまくってもアレですが、ちょっとテーマ適用して調整するだけで必要最低限整ってくれるのは実に良いです。

+アイコン

あとパラメータのコピペが欲しくなりました、複数ウィンドウ間で貼って回ったりするので。というわけでボタンにアイコンを用意したくて、それもマテリアルデザインなら簡単!

<Button Background="{StaticResource PrimaryHueLightBrush}"
        HorizontalAlignment="Left"
        Width="24" Height="24" Padding="0" Margin="5"
        Command="{Binding PasteCommand}"
        ToolTip="Paste">
    <Viewbox Width="16" Height="16">
        <Canvas Width="24" Height="24">
            <Path Data="M19,20H5V4H7V7H17V4H19M12,2A1,1 0 0,1 13,3A1,1 0 0,1 12,4A1,1 0 0,1 11,3A1,1 0 0,1 12,2M19,2H14.82C14.4,0.84 13.3,0 12,0C10.7,0 9.6,0.84 9.18,2H5A2,2 0 0,0 3,4V20A2,2 0 0,0 5,22H19A2,2 0 0,0 21,20V4A2,2 0 0,0 19,2Z"
                     Fill="{DynamicResource MaterialDesignBody}" />
        </Canvas>
    </Viewbox>
</Button>

これはMaterial Design Iconsにあるアイコンから取ってきてます。そこにはXAMLのPath Dataも載ってるので、タグをそのまま貼り付けるだけでアイコンとして使えます。これは楽ちんでめっちゃ良い!アイコンは揃えるのどうしても面倒ですからねー、このお手軽さは嬉しすぎます。色とかを用意されてるMaterialDesignのスタイルを入れ込んでやればそれだけで中々見栄えのするアイコンの出来上がり。

ReactiveCommand

えむぶいぶいえむ的なのはReactivePropertyで実装してます。で、ReactivePropertyもいーんですが、私的には昔から結構ReactiveCommand押しなんですよ、ReactiveCommandいいんだけどなー。例えば実際こんなコードになってます。

// peer = ReactiveProperty<Connection>
// ObserveStatusChangedで状態の変化の監視 + コネクションは切り替わることがあるので前のを破棄するSwitch
// Disconnectが押せるのはStatusがConnectの時だけ
Disconnect = peer.Select(x => x.ObserveStatusChanged())
    .Switch()
    .Select(x => x == StatusCode.Connect)
    .ToReactiveCommand();
 
// Disconnectの逆、だけどConnectが押せるのはそれに加えて接続先アドレス入力欄が空でない場合
Connect = peer.Select(x => x.ObserveStatusChanged())
    .Switch()
    .CombineLatest(Address, (x, y) => x != StatusCode.Connect && !string.IsNullOrEmpty(y))
    .ToReactiveCommand();

とか。若干込み入って面倒くさいのがスッキリ + ボタンのCanExecuteとぴったり来る。あとはプロセスを監視してて、存在してれば止めるボタンが押せるというのは、一秒毎のチェックにしていて、Observable.Intervalで繋ぎあわせてます。

// PhotonSocketServerが存在すれば押せるコマンド、1秒毎のポーリングで監視
KillPhotonProcess = Observable.Interval(TimeSpan.FromSeconds(1))
    .Select(x => Process.GetProcessesByName("PhotonSocketServer").Any()); 
    .ToReactiveCommand();

こういうの悩まずサクサク書けるのは幸せ度高い。

で、これ何なの?

なんなんでしょーねぇ。ということの一端はMetro.cs #1という勉強会で「IL から Roslyn まで - Metaprogramming Universe in C#」というタイトルでお話しますよ!2015-09-16(水)19:30 - 22:00に渋谷でやりますので、気になる人は是非是非参加くだしあ。内容はRoslyn 20%, C#全般 60%, WPF 10%, Unity 10%ぐらいなイメージですかしらん。このWPFのどこにメタプログラミング要素があるかというと、中身はMono.Cecil使ってアセンブリ解析してるからです。へー。とかそういうことを話します。

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

と、いうわけかでUniRx勉強会を開催しました。当日の模様はtogetterまとめで。登録が150人ほど、生憎の雨天でしたが130人以上来てくださってめっちゃ嬉しかったですね。慣れないというかはぢめての主催+司会でその辺アレだったのですが、会場をお貸し下さったgloopsさんの手厚い協力のお陰で、なんとか成立させることができ、ほんとうに感謝です。

私の発表資料は「History & Practices for UniRx UniRxの歴史、或いは開発(中)タイトルの用例と落とし穴の回避法」になります。

History & Practices for UniRx UniRxの歴史、或いは開発(中)タイトルの用例と落とし穴の回避法 from Yoshifumi Kawai

あまりUniRx固有、という感じでもなく、また凄い話、でもなんでもない地味な内容なのですけれど、ちょっとはまると嫌だなー、けどはまりがちなポイントを説明してみた、といった感。地味すぎてトリとしてはなんともいえない感じでしたね、うむむむ。ちなみにReal World UniRxというのは、Real World Haskell―実戦で学ぶ関数型言語プログラミングという本が名前的には元ネタです。Real World、現実世界で使われるUniRx。というわけで要約すれば事例求む、みたいな。

はじめてのUniRx

toRisouPさんの発表です。

はじめてのUniRx from torisoup

資料的価値が非常に高く、わかりやすい。めっちゃ読みこむと良いと思います、スゴクイイ!Cold/Hotとか大事なんですがむつかしいところですしねー。

若輩エンジニアから見たUniRxを利用したゲーム開発

gloopsの森永さんの発表です。

若輩エンジニアから見たUniRxを利用したゲーム開発 from Hirohito Morinaga

toRisouPさんのが中級者向けでしたので、こちらが初心者向けでしたね。UniRxがどういう風に自分の中で浸透というか理解が進んでいくか、というのがstep by stepで紹介されていて、伝わりやすいと思います。あと、全然紹介していなかったObservableTriggerまわりの応用が完璧に書かれていてすばら。

Interactive UI with UniRx

トライフォートの岩下さんのセッションです。

Interactive UI with UniRx from Yuto Iwashita

UniRxって基本的にスクリプティング領域の技術なので、とにかく地味!なのですが、このセッションは地味どころかDemo含め、めっちゃ伝わるし美しさ、手触りが伝わって凄かった。実際本日一番の感動でしたにゃ。

「ずいぶんとダサいライティングを使っているのね」〜UniRxを用いた物理ベースライティング制御〜

ユニティ・テクノロジーズ・ジャパンの名雪さんのLTです。

「ずいぶんとダサいライティングを使っているのね」〜UniRxを用いた物理ベースライティング制御〜 from Toru Nayuki

色々なものへのReactiveな入力/出力ができるんじゃもん!と言ってはいるし興味はかなりあるのだけれど、自分でやったことが全くない領域で、それが実際になされてる様を目にするとオオーッってなりました。

その他

そういえば、ブログに書いてなかったんですがちょっと前に「Observable Everywhere - Rxの原則とUniRxにみるデータソースの見つけ方」という発表をしていました。

Observable Everywhere - Rxの原則とUniRxにみるデータソースの見つけ方 from Yoshifumi Kawai

これ、自分的には結構良い内容だなー、と思っているので見たことないかたは是非目を通してもらえると。

まとめ

第一回、というわけなんですがかなり密度濃い内容になったのでは!?懇親会でも、自分の思っていたよりもずっと遥かに使い出している、注目している、という声をいただき嬉しかったですねー。もっとドンドン良くしていかなければ、と気が引き締まります。次回がいつになるかは完全不明(というか当分後かな?)ですが、やっていきたいなー、と思いましたです。

NotifyPropertyChangedGenerator - RoslynによるVS2015時代の変更通知プロパティの書き方

半月前にIntroduction to NotifyPropertyChangedGeneratorというタイトルでセッションしてきました。

Introduction to NotifyPropertyChangedGenerator from Yoshifumi Kawai

コードはGitHubで公開しているのと、NuGetでインストールもできます。

なにかというとVS2015のRoslynでのAnalyzerです。AnalyzerというとStyle Copに毛の生えたようなもの、をイメージしてしまうかもなのですが、全くそれだけじゃなく、真価はコードジェネレーターのほうにあると思っています。コンパイラでのエラーや警告も出せて、自然にVSやプロジェクトと統合されることから、Compiler Extension + Code Generatorとして私は捉えています。その例としてのINotifyPropertyChangedの生成となります。

POMO

Plain Old MVVM Object(笑)を定着させたいという意図は特にないのですが、割と語感が気に入ったので使ってみまふ。まぁとはいえ、やっぱ変更通知プロパティ程度で基底クラスを継承させるのは、そんなによろしいことではない、という認識はあるかなぁ、と。そのためのアプローチとして、こういったものが現実解にはなってくると思います、VS2015時代では。

さて、ちょうどufcppさんが【Roslynメタプログラミング】ValueChangedGaneratorを公開されました。アプローチが異なるわけですが、結構好みも出てくるかな、と思います。特に違いはpartialで外部ファイルに隔離 or 同一ファイル内で成形、は根本的に違うかもです。私はあまりpartialって好きではなくて、というのも結構迷子になるんですよね。いや、partial自体は素晴らしい機構でT4生成の時などに捗るんですが、このINotifyPropertyChanged程度のものでファイル分離されると、ファイル数が膨大になって、ちょっと……。また、プロパティのようなコードで触るものが外のファイルにあるのも、綺麗にはなるものの見通しは低下してしまうのではないかなあ、と。まぁ、この辺は良し悪しというかは好みかなー、といった感ですね。

色々なアプローチが考えられると思うので、色々試してみるのが良いと思います、Analyzer、可能性あって面白いです。ぜひ触ってみてくださいな。

UniRxでの空呼び出し検出、或いはRoslynによるCode Aware Libraries時代の到来について

UniRx - Reactive Extensions for Unity用に、メソッド呼んだだけで何も処理してないIObservable<T>があったらWarningを出すAnalyzerを作ってみました。

AnalyzerはVisual Studio 2015からの機能です。というわけでVisual Studio 2015 RCが必要です。あとは、NuGetからAnalyzerが入れられるようになっているので

でOK。Unityのプロジェクトであっても問題なく使えます(ただしVSTUのcsproj自動生成でAnalyzerタグは吹っ飛ぶので、生成をフックして復元する必要はあります、フック方法の詳細はみんな大好き Boo.Lang を SATSUGAI する方法を参照のこと)。もし、他にこういうAnalyzerがあったら便利なのになー、とかってアイディアあったら気楽に言ってください!作りますので!

現在のうちの会社(グラニ)のプロジェクトはRxが土台から、ありとあらゆる全てで使われているので、ちょっとした呼び出しのつもりでやってたら何もおこらなくて(Susbcribe漏れ)クソが!となるシチュエーションが少なくなかったので、こういうAnalyzerが必需品だったのでした。

ようするにC# 5.0のTaskでawaitしてないと警告が出るのと同じ話なのですが、そういうのが言語組み込みキーワードでなくても自由に、(VS2015で動かせるなら)簡単にプロジェクト単位で追加出来る、というのがミソです。こういったライブラリとアナライザーの組み合わせは、Code Aware Librariesという言葉でまとめられます。.NET Compiler Platform (”Roslyn”): Analyzers and the Rise of Code-Aware Libraries。従来はライブラリのみの提供でしたが、そこにAnalyzerも組み合わせて、Best Practiceを一体化して伝えていくような世界観が広がっています。

例えば、私はLightNodeというWebAPIフレームワークを作っていますが、これは引数の型に幾つかの制約があります。また、メソッドのオーバーロードを許していなかったりします。それらは実行時のウォームアップのタイミングでフェイルファストとして気づかせるようにしていますが、それよりも前のタイミング、コードを書いている最中にリアルタイムで警告できれば、より良いでしょう。なので、Analyzerを同梱すれば、より良い形、より良いライブラリの有り様になります。

DiagnosticAnalyzerの作り方 Part2

以前にVS2015のRoslynでCode Analyzerを自作する(ついでにUnityコードも解析する)VS2015+RoslynによるCodeRefactoringProviderの作り方と活用法という記事を書きましたが、基本的にはそれらと同じです、アタリマエですが。↑の記事はCTPの頃のもので、若干インターフェイスが変わっちゃっていますが、少し修正するだけでほぼほぼ同じかな。

今回作ったのはCode Analyzerで、Fixは含めていないのでcsファイル一個だけで済んでいます。

[DiagnosticAnalyzer(LanguageNames.CSharp)]
public class HandleObservableAnalyzer : DiagnosticAnalyzer
{
    public const string DiagnosticId = "HandleObservable";
 
    internal const string Title = "IObservable<T> does not handled.";
    internal const string MessageFormat = "This call does not handle IObservable<T>.";
    internal const string Description = "IObservable<T> should be handled(assign, subscribe, chain operator).";
    internal const string Category = "Usage";
 
    internal static DiagnosticDescriptor Rule = new DiagnosticDescriptor(DiagnosticId, Title, MessageFormat, Category, DiagnosticSeverity.Warning, isEnabledByDefault: true, description: Description);
 
    public override ImmutableArray<DiagnosticDescriptor> SupportedDiagnostics { get { return ImmutableArray.Create(Rule); } }
 
    public override void Initialize(AnalysisContext context)
    {
        context.RegisterSyntaxNodeAction(AnalyzeMethodDeclaration, SyntaxKind.MethodDeclaration);
    }
 
    private static void AnalyzeMethodDeclaration(SyntaxNodeAnalysisContext context)
    {
        var invocationExpressions = context.Node
            .DescendantNodes(descendIntoChildren: x => !(x is InvocationExpressionSyntax))
            .OfType<InvocationExpressionSyntax>();
 
        foreach (var expr in invocationExpressions)
        {
            var type = context.SemanticModel.GetTypeInfo(expr).Type;
            // UniRx.IObservable? System.IObservable?
            if (new[] { type }.Concat(type.AllInterfaces).Any(x => x.Name == "IObservable"))
            {
                // Okay => x = M(), var x = M(), return M(), from x in M()
                if (expr.Parent.IsKind(SyntaxKind.SimpleAssignmentExpression)) continue;
                if (expr.Parent.IsKind(SyntaxKind.EqualsValueClause) && expr.Parent.Parent.IsKind(SyntaxKind.VariableDeclarator)) continue;
                if (expr.Parent.IsKind(SyntaxKind.ReturnStatement)) continue;
                if (expr.Parent.IsKind(SyntaxKind.FromClause)) continue;
 
                // Okay => M().M()
                if (expr.DescendantNodes().OfType<InvocationExpressionSyntax>().Any()) continue;
 
                // Report Warning
                var diagnostic = Diagnostic.Create(Rule, expr.GetLocation());
                context.ReportDiagnostic(diagnostic);
            }
        }
    }
}

戦略的には、メソッド呼び出し、つまりInvocationExpressionを拾いだして、そこからローカル変数代入/フィールド代入/return/LINQクエリ構文/メソッド呼び出しで使われていなければダメ扱いにする、という流れ。コード自体は行数も少なくて難しくはないのですけれど、戦略を決定するまでは割と悩みました。SyntaxTree自体も大量のメソッドがあり、SemanticModelも絡めると、色々な手段が取れそうでいて取れなさそうで、相当悩ましい。最終的にはかなり単純な手法に落ち着きましたが、直線距離で到達できるようになるまでには、かなり慣れが必要そうです。あと、最初作った時はクエリ構文のチェックを見落としてたりとか(さすがにこれを最初から気づくのは無理)、必要なケースを全て洗い出すのはそこそこ大変かな、といった感はあります。

DescendantNodesのdescendIntoChildrenという引数が中々面白くて、これは子孫ノードの探索を打ち切る条件を指定できます。これの何がいいって、例えばメソッド Observable.Range().Where().Select() があった場合、最上位のInvocationExpressionはObservable.Range().Where().Select()なのですが、その子孫に Observable.Range().Where() や Observable.Range() がいます。ふつーのDescendantNodesだとそれら全部を列挙してしまうんですが、今回は欲しいのは最上位だけなので、descendIntoChildrenで条件フィルタを足しています。

以前には紹介していない、ユニットテストのやり方も紹介しましょう。といっても、テンプレートに最初からTestプロジェクトと、便利クラス群が同梱されています。Analyzerだけの場合は基底クラスをDiagnosticVerifierに変えて……

namespace UniRxAnalyzer.Test
{
    [TestClass]
    public class HandleObservableAnalyzerTest : DiagnosticVerifier
    {
        protected override DiagnosticAnalyzer GetCSharpDiagnosticAnalyzer()
        {
            return new UniRxAnalyzer.HandleObservableAnalyzer();
        }
 
        [TestMethod]
        public void UnHandle()
        {
            var source = @"
using System;
 
class Test
{
    IObservable<int> GetObservable() => null;
 
    void Hoge()
    {
        GetObservable();
    }
}";
            var expected = new DiagnosticResult
            {
                Id = UniRxAnalyzer.HandleObservableAnalyzer.DiagnosticId,
                Message = "This call does not handle IObservable<T>.",
                Severity = DiagnosticSeverity.Warning,
                Locations = new[]
                {
                    new DiagnosticResultLocation("Test0.cs", 10, 9)
                }
            };
 
            this.VerifyCSharpDiagnostic(source, expected);
        }
    }
}

ようするにVerifyCSharpDiagnosticにテスト用のC#コードと、期待するDiagnosticResultを渡すだけです、実に簡単。もしエラーじゃなくOKの場合はsourceだけをVerifyCSharpDiagnosticに渡せば、そういうことになります。

まとめ

Analyzer、かなりイイです。実際。とにかくとりあえず触ってみませう。現状リファレンスとかは特にないですが、まぁLINQ to XML辺りがわかっていればSyntaxVisualizerとIntelliSenseを頼りになんとか作り上げられるでしょう!メソッド名を見ながらカンを働かせましょう。大丈夫大丈夫。また、GitHubには既にお手本となるAnalyzerが出回っているので、それを参照にすればかなりいけます。代表的なところではNR6Pack, StyleCopAnalyzers, Code Crackerなどがあります。

では、よきRoslynライフを!

LightNode 1.2.0 - Swagger統合によるAPIのデバッグ実行

グラニのC#フレームワークの過去と未来、現代的なASP.NETライブラリの選び方という記事で、スライドと補足は先に上げましたが、以前に弊社で行った勉強会「Build Insider MEETUP with Grani 第1回」のレポートがBuild Insiderに上がっています。「using CSharp;」な企業を支える技術方針とベスト.NETライブラリ記事によるレポートは、さくさく読めるし、スライドで欠けていた部分も補完できていーんじゃないでしょーか。まる。

さて、で、基本的にAPIサーバーはOWINで行くんですが、API開発はとにかくふつーのウェブよりも開発がしにくい!の欠点を、前回LightNode 1.0、或いはWeb APIでのGlimpseの使い方はGlimpseフル統合で補おうとしました。それはそれでいいんですが、もう一つ足りない。それはともかく根本的にそもそも実行しづらい。さすがにモバイルのエミュレーターなりUnityのEditorなりから毎度実行は効率悪すぎてありえないし、POSTMANFiddlerで叩くのも面倒くせえ。

そこでSwagger。とはなにか、というのは見てもらえれば。

実行機付きのAPIのヘルプ・ドキュメントです。ヘルプ/ドキュメントはどうでもいいんですが、この実行機がかなり使いやすくいい!パラメータ入力してTry it out!でOK。認証が必要な場合も、右上にapi_keyというのが見えてますが、ちょっとindex.htmlを書き換えてこのapi_keyの部分を好きに都合のいい形態に変えてしまえば、機能します。実に便利。

SwaggerはAzureのAPI Appsでも利用されるようになったので、今後.NETでも目にする機会はちょっとずつ増えていくのではないでしょうか?ASP.NET Web APIで利用する方法はみそせんせーのSwagger を使った ASP.NET Web API のドキュメント生成を参照すれば良いでしょふ。

さて、LightNode(ってここではじめて解説しますが、私の作っているOwin上で動くMicro REST/RPCフレームワークです)では、Swagger統合はMiddlewareとして実装してあります。

ルートをapiと別系統にswaggerとして切ってもらって、

// 今のところPOSTしかサポートしてないのでPostを有効にしてね
app.Map("/api", builder =>
{
    builder.UseLightNode(new LightNodeOptions(AcceptVerbs.Get | AcceptVerbs.Post, new JilContentFormatter(), new GZipJilContentFormatter())
    {
        ParameterEnumAllowsFieldNameParse = true, // Enumを文字列で並べたいならこれをONにして
        // 下2つはSwagger前提で使うならエラー表示的に便利
        ErrorHandlingPolicy = ErrorHandlingPolicy.ReturnInternalServerErrorIncludeErrorDetails,
        OperationMissingHandlingPolicy = OperationMissingHandlingPolicy.ReturnErrorStatusCodeIncludeErrorDetails
    });
});
 
// こっちでSwaggerを有効にする
app.Map("/swagger", builder =>
{
    // XMLコメントから引っ張ってくるばあい(オプション)はパスを指定してください
    // メソッドに付与されているsummary, remarks, paramを情報として使います     
    var xmlName = "LightNode.Sample.GlimpseUse.xml";
    var xmlPath = System.AppDomain.CurrentDomain.BaseDirectory + "\\bin\\" + xmlName; // もしくは HttpContext.Current.Server.MapPath("~/bin/" + xmlName);
 
    // LightNode側のAPIのbasePathを指定
    builder.UseLightNodeSwagger(new Swagger.SwaggerOptions("LightNodeSample", "/api")
    {
        XmlDocumentPath = xmlPath,
        IsEmitEnumAsString = true // Enumを文字列で並べたいならtrueに
    });
});

といった感じです。ちょっとややこしーですが、基本的にはUseLightNodeSwaggerだけでOK、ということで。これで、例えば http://localhost:41932/Swagger/ にアクセスすればSwaggerの画面が出てきます。Swagger-UI自体はdllに埋め込まれています。また、定義ファイル(JSON)はapi-default.jsonにアクセスすることで、直接取得できます。

もしOwinをIISでホストしている場合、IISのStaticFileハンドラーが邪魔してうまくホストできない場合があります。その場合、StaticFileハンドラーを殺してください。Owinでやる場合は、とにかくOwinに寄せたほうがいいですね(StaticFile系はMicrosoft.Owin.StaticFiles使いましょう)

<system.webServer>
    <handlers>
        <remove name="StaticFile" />
        <!-- もしGlimpseもホストする場合はGlimpseのを先に書いといて -->
        <add name="Glimpse" path="glimpse.axd" verb="GET" type="Glimpse.AspNet.HttpHandler, Glimpse.AspNet" preCondition="integratedMode" />
        <add name="OWIN" path="*" verb="*" type="Microsoft.Owin.Host.SystemWeb.OwinHttpHandler" />
    </handlers>
</system.webServer>

もし、例えば最初に例に出しましたが、認証情報を付与するとかでindex.htmlを埋め込みのではなくカスタムのを使いたい場合、OptionのResolveCustomResourceをハンドリングすればできます。例えばこんな感じに、別の埋め込みリソースから取り出したものに差し替えたり。

app.Map("/swagger", builder =>
{
    builder.UseLightNodeSwagger(new LightNode.Swagger.SwaggerOptions("MySample", "/api")
    {
        ResolveCustomResource = (filePath, loadedEmbeddedBytes) =>
        {
            if (filePath == "index.html")
            {
                using (var resourceStream = typeof(Startup).Assembly.GetManifestResourceStream("MySample.Swagger.index.html"))
                using (var ms = new MemoryStream())
                {
                    resourceStream.CopyTo(ms);
                    return ms.ToArray();
                }
            }
            return loadedEmbeddedBytes;
        }
    });
});

当然、index以外でもハンドリングできます。

まとめ

GlimpseとSwaggerがあわさって最強に見える!あとはもともとあるクライアント自動生成もあるので、三種の神器コンプリート。実際、これでAPI開発の苦痛に思えるところがかなり取り除かれたのではないかなー、って思ってます。これを全部自分で用意するのはそれはそれは大変なので、Owinで良かったし、組み合わせに関しても、.NETでOSSを使うってこういうことですよね?という例になればよいかな。

UniRx 4.8 - 軽量イベントフックとuGUI連携によるデータバインディング

UniRx(Reactive Extensions for Unity)のVer 4.8が昨日、AssetStoreにリリースされました。UniRxとはなにか、というと、巷で流行りのReactive Programming、の.NET実装のReactive Extensions、のUnity実装で、私が去年ぐらいからチマチマと作っています。実際のところ細かいリリースは何度も行っているんで(差分はGitHubのReleasesに書いてあります)、開発/アップデートはかなりアクティブな状態でした。その間に、Google PlayやiOSのAppStoreでもチラホラと使用しているタイトルがあったりと(ありがとうございます!!!)、案外存外しっかりRealWorldしていました。GitHubのStarも順調に伸びていて、まぁまぁメジャーになってきた気はします。

その間に、いくつか素晴らしいプレゼン資料も作っていただきました!@torisoupさんの未来のプログラミング技術をUnityで -UniRx-は、分かりやすく魅力を感じさせてくれる内容になっていて、とても素晴らしいです。読むべし読むべし。toRisouPさんはQiitaでも多くの記事を書いてくださっていて、(私がgdgd書くよりも)はるかに分かりやすくていいですね!

また、@Grabacr07さんのUniRx とか ReactiveProperty とかは、今回紹介するuGUI連携についての話が、分かりやすく綺麗に紹介されているので、こちらも必読です。必読。

UniRxの最初の発表は2014/04/19のUniRx - Reactive Extensions for Unityというところで、発端は非同期処理の解消、という一面からスタートしていたのですが、すぐにUnityの発する色々なイベント処理をRxで行おうという、本来の、でありつつも応用的なところが盛んに試されるようになったのは素晴らしいことだなぁ、と思っています。これはゲームプログラミングの持つ複雑さが、Reactive Programmingの使い道を無数に産むという、相性の良さがあるのかしらん。非同期だけじゃない、データバインドだけじゃないRealなReactive Programmingがここにあり、プログラミングを、C#の可能性を、パラダイムシフトを大いに楽しめる環境です。是非楽しんでください。もちろん、実用性もありますしね!

当然(?)フリーです。

ObservableTriggers

UniRx 4.8から、MonoBehaviourのイベントハンドリング手法をObservableTriggersという概念に全面移行しました。どういうことかというと、まず、ObservableMonoBehaviourは廃止です:) Obsoleteはつけていないし、動作はしますが、非推奨になりました。その代わりとなるのがObservableTriggersです。まず利用例を。

using UniRx;
using UniRx.Triggers; // この名前空間以下にTriggerは入ってるのでusingしときましょう
 
public class MyComponent : MonoBehaviour
{
    void Start()
    {
        // AddComponentでTriggerを付与する
        var trigger = this.gameObject.AddComponent<ObservableUpdateTrigger>();
 
        // すると*Event*AsObservableが使えるようになる
        trigger.UpdateAsObservable()
            .SampleFrame(30)
            .Subscribe(x => Debug.Log(x), () => Debug.Log("destroy"));
 
        // 3秒後に自殺:)
        GameObject.Destroy(this, 3f);
    }
}

Triggerは対象GameObjectがDestroyされると、OnCompletedを流してイベント発火を終了します。

ObservableMonoBehaviourを継承するのではなく、AddComponentで必要なイベントのためのTriggerを与えてください。そうすれば、そのイベントがRxで取り扱えるようになります。標準では ObservableAnimatorTrigger, ObservableCollision2DTrigger, ObservableCollisionTrigger, ObservableDestroyTrigger, ObservableEnableTrigger, ObservableFixedUpdateTrigger, ObservableUpdateTrigger, ObservableLastUpdateTrigger, ObservableMouseTrigger, ObservableTrigger2DTrigger, ObservableTriggerTrigger, ObservableVisibleTrigger, ObservableTransformChangedTrigger, ObservableRectTransformTrigger, ObservableCanvasGroupChangedTrigger, ObservableStateMachineTrigger, ObservableEventTrigger を用意してあります。「ほぼ」全部です。4.6から追加された新しいイベント(OnTransformChildrenChangedとか)も網羅しています。(とはいえ全部ではないので、足りなくて必要なものがあったら自分で追加するか、私にリクエストください、単純にあまり需要なさそうだと勝手に判断したものはオミットしちゃっているので……)

また、AddComponentが面倒くさい!ので、GameObject/Componentに対して、UniRx.Triggersをusingしている場合は、XxxAsObservableメソッドを直接拡張メソッドから呼べて、するとTriggerが自動付与されるようになっています。

using UniRx;
using UniRx.Triggers; // 必ずこのusingが必要です
 
public class DragAndDropOnce : MonoBehaviour
{
    void Start()
    {
        // OnMouseDownAsObservableが生えてる
        this.OnMouseDownAsObservable()
            .SelectMany(_ => this.UpdateAsObservable()) // UpdateAsObservableが生えてる
            .TakeUntil(this.OnMouseUpAsObservable()) // OnMouseUpAsObservableが生えてる
            .Select(_ => Input.mousePosition)
            .Subscribe(x => Debug.Log(x));
    }
}

なので、通常使う場合は、Triggerに関しては意識する必要はありません。(ObservableEventTrigger(uGUI用)とObservableStateMachineTrigger(Animation用)だけは自動付与がないので、これらの場合だけ自分で意識的に付与する必要があります)

ObservableMonoBehaviourは継承が必要だったり(基底クラスが強制される!)、baseメソッドの呼び出しが必須だったり、空イベントの呼び出しが必ず含まれるパフォーマンス低下などなど、決して使い勝手の良いものではありませんでした。というか使い勝手は最悪でした。なんで当初からObservableTriggerのようなやり方じゃなかったか、というと……、まぁ、単純に私のUnityへの理解不足です、すびばせん。ObservableTriggerは、Unityのコンポーネント指向を活かしつつ、Rxによってイベントを自然に外側で取り出せるようになっているので、圧倒的に便利な形になったのではないかなと思います。

uGUI

uGUIのイベントがUniRxでパーフェクトにハンドリングできます!この辺の話は前述のスライドUniRx とか ReactiveProperty とかに綺麗にまとまっているのですが、例えばボタンとかが

// インスペクタから貼っつけるとか
public Button MyButton;
 
// こんな感じで取る(onClick.AsObservable もしくは OnClickAsObservable)
MyButton.onClick.AsObservable().Subscribe(_ => Debug.Log("clicked"));

ほぅ……。普通だ。どうでも良さそうだ。と、いう具合にuGUIのEvent + AsObservableでイベントハンドリングができるようになっています。もう少し例を出すと

// ビューからのコントロールはインスペクタでペタペタ貼り付ける
public Toggle MyToggle;
public InputField MyInput;
public Text MyText;
public Slider MySlider;
 
// Startとかで宣言的にUIを記述していきましょう
void Start()
{
    // チェックボックスのオン/オフでボタンの有効/非有効が切り替わるようにします
    // OnValueChangedAsObservableは.onValueChanged.AsObservableのヘルパーで、単純に省略が楽という他に、
    // 初期値(最初のisOnの値)がSubscribe時に流れていきます
    // また、SubscribeToInteractableはUniRxのヘルパーで、 x => .interactable = x を省略できます
    MyToggle.OnValueChangedAsObservable().SubscribeToInteractable(MyButton);
 
    // 入力文字は1秒後にテキストラベルに反映されます
    MyInput.OnValueChangeAsObservable()
        .Where(x => x != null)
        .Delay(TimeSpan.FromSeconds(1))
        .SubscribeToText(MyText); // SubscribeToTextを使うと簡単に紐付けできます
 
    // SubscribeToTextの人間の読める形に変換したい場合用ヘルパ
    MySlider.OnValueChangedAsObservable()
        .SubscribeToText(MyText, x => Math.Round(x, 2).ToString());
}

こんな風になります。uGUIの標準コントロールに関しては直接EventAsObservableできるように拡張されてます。ともあれ、uGUIのイベントハンドリングはスクリプトで行いましょう。uGUI標準のAddHandlerなどはやりづらいですが、UniRxはそれを簡単に行える仕組みが用意してあります。uGUIのチュートリアルや解説本では、インスペクタのイベントの部分をクリックしてメソッドと紐付けてー、などとやるかもしれませんが、あのやり方は最低最悪なので忘れましょう。スクリプトレスでイベント設定できるとか幻想なんで、少なくともRxを使おうとしているようなプログラマなら、一切見なかったことにしましょう。100億パーセントどうでもいい次元の話なので無視しておきましょう。やりづらいだけです。

unityEvent.AsObservableのかわりに、全てのUnityコントロールにはUnityEventAsObservableが定義されています。ButtonのonClickの場合は違いはないのですが、一部の値が流れるものに関しては違いがあって、コントロールに直接生えているものは初期値が流れるようになっています。この初期値が流れる、という性質は非常に重要です。と、いうのも、今回のようにUIを宣言的に記述した場合、初期値が流れないと、初期値を設定して回らなければならなくて全体の構築が狂ってしまうからです。と、いうわけで、基本的にはコントロールに生えているAsObservableを使いましょう。

ReactiveProperty

UniRx 4.8からReactivePropertyという特別な型が用意されています(あとReactiveCollectionとReactiveDictionary)。これは何かというと、通知可能なプロパティ。なんのこっちゃ。うーん、イベントと値がセットになった型。うーん、なんのこっちゃ……。

// 変更通知付きなモデル
public class Enemy
{
    // HPは変更あったら通知して他のところでなんか変化を起こすよね?
    public ReactiveProperty<long> CurrentHp { get; private set; }
 
    // 死んだら通知起こすよね?
    public ReadOnlyReactiveProperty<bool> IsDead { get; private set; }
 
    public Enemy(int initialHp)
    {
        // 宣言的に記述していく。
        // ReactivePropertyはそれ自体がIObservable<T>なので、Rxでチェーン可能で、更にそれをReactivePropertyに変換も可能
        // 死んだかどうかというのはHPが0以下になったら、で表現できる         
        CurrentHp = new ReactiveProperty<long>(initialHp);
        IsDead = CurrentHp.Select(x => x <= 0).ToReadOnlyReactiveProperty();
    }
}
 
// こんなふうにして使う
// ボタンクリックしたらHPが99減ってくとする(実際はなんかCollision受けたら減るとか色々)
// ReactivePropertyの値は.Valueで取り出せる)
MyButton.OnClickAsObservable().Subscribe(_ => enemy.CurrentHp.Value -= 99);
 
// その変更を受けてUIに変更を戻す
enemy.CurrentHp.SubscribeToText(MyText); // とりあえず現在HPをTextに表示
 
// もし死んだらボタンクリックできないようにする
enemy.IsDead.Select(isDead => !isDead).SubscribeToInteractable(MyButton);

今まではイベント+普通の値で表現していたものが、プロパティ一個で表現できるようになります。また、イベント自体の取り扱いもRxなので合成可能になっていて、取り回しが向上します。というわけで、めちゃくちゃ便利。実際便利。通知が必要な値は片っ端からReactivePropertyにしましょう、それで幸せになれます!

更にReactivePropertyはInspectorで利便性が向上しています。

IntRxPropのところ、インスペクタに値を表示しているのですが、これの値をインスペクタで変更すると、紐付けていたイベント(.Subscribeしているもの)への通知も飛んでいきます。地味に捗る神機能。注意点としては、ジェネリックの型はインスペクタに表示できないという制限を引き継いでいるので、インスペクタに表示したいReactiveProeprtyは、専用のReactivePropertyを使いましょう。例えばIntReactivePropertyやBoolReactiveProperty、Vector2ReactivePropertyなどが標準では用意されています。EnumをReactiveProeprtyとして表示したい、というシチュエーションも多いと思います。その場合はSpecializedなReactivePropertyを定義していきましょう。例えば

// こんなEnumがあるとして
public enum Fruit
{
    Apple, Grape
}
 
// こういう特化したReactiveProeprtyを作ればOK
[Serializable]
public class FruitReactiveProperty : ReactiveProperty<Fruit>
{
    public FruitReactiveProperty()
    {
    }
 
    public FruitReactiveProperty(Fruit initialValue)
        :base(initialValue)
    {
    }
}
 
// また、InspectorDisplayDrawerにたいしてCustomPropertyDrawerを指定するとインスペクタでの表示が向上/イベント通知が可能になるので
// 特化ReactiveProeprtyの作成とワンセットで行いましょう
// ExtendInspectorDisplayDrawer自体は一個あればそれで大丈夫です
[UnityEditor.CustomPropertyDrawer(typeof(FruitReactiveProperty))]
[UnityEditor.CustomPropertyDrawer(typeof(YourSpecializedReactiveProperty2))] // 他、沢山ここにtypeofを追加していく
public class ExtendInspectorDisplayDrawer : InspectorDisplayDrawer
{
}

といった感じに拡張することで、より便利になっていきます。

MV(R)P

これらのUIの作り方を指して、Model-View-(Reactive)Presenterパターンというものを提唱します。

なぜMVPか、なんでMVVMではないか。まず、Unityはバインディングエンジンを持っていません。一般的にMVVMはViewとViewModelの間をバインディングエンジンが受け持ちます。なので、素の状態ではそもそもMVVMはできません。じゃあバインディングエンジンを作るか、となると、そんなレイヤーを挟むのは複雑になるしパフォーマンスも低下するし、デメリットがメリットを上回るバインディングエンジンを作るのは難しい。バインディングは誰かが動的レイヤーを引き受けなければならなくて(例えばName直書きなINotifyPropertyChangedであったり)、ピュアC#の世界とは相性が悪い。それをWPFではXAMLに押し付けているが、動的コード生成高速化の手段が取れないUnityでは、無理して実現する価値はない。

そんなわけで、MVVMはやらない。やらないとなると、バインディング機構が存在しない都合上、どこかで、だれかが、Vを知る必要がある(じゃなきゃViewのUpdateがかけれない)。というわけでVMは存在できず、Presenterを立てる。Model自体はPresenterにも依存しないし、Viewは知らない。ただしViewまで伝搬するため通知は可能でなければならない。それらをRxが繋ぎます。従来のMVPはステートの複雑化や伝搬に困難があったが、Observableはバインディングのようにシンプルに通知を行うことができるし、Viewへの適用もバインディングであるかのように綺麗に見せることができる。Rxを介すことによって、アプリケーションを作る上での問題が解消する。しかもレイヤー的にはないに等しく薄いので、一切のデメリットはない。

再度、コードと当てはめてみましょう。

// Presenter(Canvasのルートだったり、Prefabやパーツ分割単位のルート)
public class ReactivePresenter : MonoBehaviour
{
    // PresenterはViewのコンポーネントを知っている(さわれる)
    public Button MyButton;
    public Toggle MyToggle;
 
    // ModelからのState-Change-EventsはReactivePropertyによって伝搬される
    // Modelの変更は基本的に自身が上層に通知可能であり、それはReactiveProeprtyで表現される
    Enemy enemy = new Enemy(1000);
 
    void Start()
    {
        // Viewからのuser eventsはRxによって伝搬され、Modelにまでリアクティブに浸透していく
        MyButton.OnClickAsObservable().Subscribe(_ => enemy.CurrentHp.Value -= 99);
        MyToggle.OnValueChangedAsObservable().SubscribeToInteractable(MyButton);
 
        // Modelからの伝搬もまた、Presenterを介してRxによってViewのUpdateをかける
        enemy.CurrentHp.SubscribeToText(MyText);
        enemy.IsDead.Where(isDead => isDead == true)
            .Subscribe(_ =>
            {
                MyToggle.interactable = MyButton.interactable = false;
            });
    }
}

この場合、ViewとPresenterの紐付けはUnityのインスペクタでやります、ぴっ、ぴっ、ぴっってドラッグアンドドロップですねん。ふつーの(?)MVPだと、このViewをIViewとしてモックと差し替え可能にしたりもしたりしなかったりですが、そこまでやってもメリットゼロなんでそんなことはやらないでダイレクトにViewの実体とひもづける形でOK。

それぞれの伝搬ポイントにUniRxのメソッドやクラスが用意されているので、全てをシームレスに、Reactiveにつなぎ合わせることが可能です。UniRxならね。これの何が嬉しいかというと、見通しが良く、コード量が減ります。それがもう単純に嬉しい。また、イベントの関連付けはスクリプト側に寄っているので、インスペクタがカオティックにならずに済みます。かなりUnity(+Rx)の現実に沿った作り方なのではないかなー、と思うのですがどうでしょう?この辺は意見大募集中といったところです。

カスタムトリガーを作ろう

そんな風にアプリケーションを作っていくと、イベントはRx的に発動させるのが都合が良い、ということがわかってきます。実際そう。で、SubjectやReactivePropertyなどを駆使することによりModelをRx的に作っていくのは可能なのですが、ViewからのイベントをRx的に流すためにはどうすればいいのか。標準ではTriggerが用意されてますが、それだけじゃ足りない、例えばロングタップ作りたいとかジェスチャー作りたいとか……。という場合はTriggerを自作します。作り方は、ObservableTriggerBaseを継承して……

public class ObservableLongPointerDownTrigger : ObservableTriggerBase, IPointerDownHandler, IPointerUpHandler
{
    public float IntervalSecond = 1f;
 
    Subject<Unit> onLongPointerDown;
 
    float? raiseTime;
 
    void Update()
    {
        if (raiseTime != null && raiseTime <= Time.realtimeSinceStartup)
        {
            if (onLongPointerDown != null) onLongPointerDown.OnNext(Unit.Default);
            raiseTime = null;
        }
    }
 
    void IPointerDownHandler.OnPointerDown(PointerEventData eventData)
    {
        raiseTime = Time.realtimeSinceStartup + IntervalSecond;
    }
 
    void IPointerUpHandler.OnPointerUp(PointerEventData eventData)
    {
        raiseTime = null;
    }
 
    public IObservable<Unit> OnLongPointerDownAsObservable()
    {
        return onLongPointerDown ?? (onLongPointerDown = new Subject<Unit>());
    }
 
    protected override void RaiseOnCompletedOnDestroy()
    {
        if (onLongPointerDown != null)
        {
            onLongPointerDown.OnCompleted();
        }
    }
}

こんな感じ、これで他のTriggerと同じノリ、OnPointerDownAsObservableでタップを拾えるように、OnLongPointerDownAsObservableでロングタップを拾えるようになります。Subjectでイベント通知することと、RaiseOnCompletedOnDestroyのところでOnCompletedを発行するのが原則です。こういう形でイベントを拡張すると、よりスムーズにRxで全てが繋がっていきます!

ライフサイクル管理

で、全部がRxになると、イベントをSubscribeしたのをどこで解除すればいーんですかー、って話になってきたりこなかったりする。基本的にTrigger系は自身が死んだ時に終了するからいいんですが、それ意外のもの、例えばObservable.TimerやObservable.EveryUpdateは自動的に止まらないので、自分で登録解除する必要があります。そのためのヘルパーとして、IDisposable.AddToがUniRxには用意されています。また、CompositeDisposableがSubscriptionの管理に使えます。

// CompositeDisposableはList<IDisposable>のようなもので、複数のIDisposableが管理できます
CompositeDisposable disposables = new CompositeDisposable(); // これをfieldにおいておいて
 
void Start()
{
    Observable.EveryUpdate().Subscribe(x => Debug.Log(x)).AddTo(disposables); // AddToで詰める
}
 
void OnTriggerEnter(Collider other)
{
    // .Clear() => 中の全てのdisposableのDisposeが呼ばれて、Listが空になります
    // .Dispose() => 中の全てのdisposableのDisposeが呼ばれて、以降はAddされたら即対象をDisposeするようになります
    disposables.Clear();
}

よくあるシチュエーションとして、Destroyした瞬間に解除したい、というのがあると思います。その場合AddTo(gameObject/component)が使えます。

void Start()
{
    // 自分が消滅したらDispose
    Observable.IntervalFrame(30).Subscribe(x => Debug.Log(x)).AddTo(this);
}

DisposeじゃなくてOnCompletedを出して欲しい、という場合にはTakeWhile, TakeUntil, TakeUntilDestroy, TakeUntilDisable辺りが使えます。

Observable.IntervalFrame(30).TakeUntilDisable(this)
    .Subscribe(x => Debug.Log(x), () => Debug.Log("completed!"));

イベントを「繰り返す」場合に、Repeatが通常使われますが、実は危険です。源流がOnCompletedを発行すると無限ループ化するからです。ObservableTriggersが終了するとOnCompletedを発行するため、安易なRepeatの使用は無限ループ行きとなります。それを避けるには、RepeatUntilDestroy(gameObject/component), RepeatUntilDisable(gameObject/component), RepeatSafeが使えます。RepeatUntilDestroyとかは文字通りなんですが、RepeatSafeは連続してOnCompltedが発行された場合はRepeatを取りやめるという、無限ループ禁止機構のついたRepeatです。ベンリ。

最後に、ObserveEveryValueChangedを紹介します。これは、ラムダ式で指定した値を変更のあった時にだけ通知するという、つまり変更通知のない値を変更通知付きに変換するという魔法のような(実際ベンリ!)機能です(実際は毎フレーム監視してるんで、ポーリングによる擬似的なPull→Push変換)

// watch position change
this.transform.ObserveEveryValueChanged(x => x.position).Subscribe(x => Debug.Log(x));

これは監視対象がGameObjectの場合はDestroy時にOnCompletedを発行して監視を止めます。通常のC#クラス(POCO)の場合は、GCされた時に、同様にOnCompletedを発行して監視を止めるようになっています(内部的にはWeakReferenceを用いて実装されています)。ただのポーリングなので多用すぎるとアレですが、お手軽でベンリには違いないので適宜どうぞ。

パフォーマンス

パフォーマンスの話は一口で言うには結構難しいところです。まずいうと、RxとLINQを関連付けてLINQだからパフォーマンスがー、というのは微妙にあてはまりません。RxはPush型、最初のタイミングでパイプラインを構築し、それを(大抵の場合)かなり長い期間(最長でObjectが消滅するまで)購読する形になります。つまり、ライフサイクルが非常に長い。だから、パイプライン構築のためのオブジェクト(のGC)のコストというのは、そんなでもないと思ってもらっていいでしょふ。Updateの度に頻繁に数千構築/解体を繰り返すようなものではない、ということですねん。パイプラインに流れる値に関しては、その頻度と書きよう次第ですけれど。また、Rxのメソッドも軽いメソッドと重いメソッドがあるので、それ次第という面もあります。とはいえそこまで気にするほどではないかなー、と。

全体的にRxを適用すると、アプリケーションはPushベースで構築されることになるので、頻繁な問い合わせ処理(Pullベース)が消え、つまり更新駆動の最小限の差分処理だけが走るので、逆にパフォーマンスは上がる、という見方もできなくもないですが、まぁさすがにそれは都合の良すぎる捉え方でしょう:) ともあれ、そういったアプリケーション構築手法の変革もあるので、そこのところも含めて評価しなければなりません。

単純なコルーチンの代替、非同期通信処理の代替レベルでなら、実質ない、と言っても過言ではないところなので、それぐらいならばもうまるっきり気にせず、ですね。また、今まではObservableMonoBehaviourが不要な場合にも空イベントを回していて、それが若干の消費があったのですが、今回からは軽量なTriggerベースで必要なものにしかイベントを付与しないスタイルになったので、全体的にはかなり取り回しよくなってきたんじゃないかなー、と思います。

まだまだ全然パフォーマンスチューニングできる領域は沢山あるので、都度行っていくつもりです(分かりやすく効果の出るところでいえばWhere.Where.Whereチェーンは1個のWhereにできたり、かなり多用されるWhere.Selectチェーンも1個のWhereSelectチェーンにまとめあげられたり、などなど)

LINQ to GameObject

あと、これはUniRxとは関係ないのですがLINQ to GameObjectもアップデートしてます。

メインはLINQ風メソッドでtransformを自在に辿れるってところで、それはLINQ to GameObjectによるUnityでのLINQの活用を読んでいたたきたいのですが、もう一つの機能に、階層上の任意の位置にGameObjectをAddしたりMoveしたりするメソッドもあります。今回のアップデートで、これがuGUIのRectTransformに対応しました!uGUIはヒエラルキーの位置を表示情報としてかなり大事に扱うため、それのコントロールが容易になるLINQ to GameObjectは役立つはずです。

まとめ

今回のObservableTrigger、uGUI連携、そしてLifetime管理といった機能によって、より様々なところに導入しやすくなった、より使いやすくなったのではないでしょうか!

直近では4/16 18:30~の歌舞伎座.tech#7「Reactive Extensions」で「Observable Everywhere - UniRxによるUnityでのReactive Programming」と題して発表を行います。こちらはニコ生での放送もあるようなので、見るといいんじゃないかなー、ということで!

グラニのC#フレームワークの過去と未来、現代的なASP.NETライブラリの選び方

Build Insider MEETUP with Graniというイベントで、グラニのC#フレームワーク(というほどのものはない!)の今までとこれからってのを話しました。

A framework for light up applications of grani from Yoshifumi Kawai

そのうちBuild Insiderで文字起こしとか公開されると思います。

2015年の今、どういうライブラリを選んだか、とかNLog大脱却、とかって話が見どころですかね。うちの考えるモダンなやり方、みたいな感じです。

実際、EventSourceSemantic Logging Application Blockは良いと思いますので、触ってみるといいですね。少なくとも、イマドキにハイパーヒューマンリーダブル非構造化テキストログはないかなぁ、といったところです。

スライドにしたら判別不能になったOWINのStartup部分も置いておきます、参考までに。

// 開発環境用Startup(本番では使わないミドルウェア/設定込み)
public class Startup
{
    public void Configuration(IAppBuilder app)
    {
        app = new ProfilingAppBuilder(app); // 内製Glimpse表示用AppBuilderラッパー(Middlewareトラッカー)
        app.EnableGlimpse(); // Glimpse.LightNdoe同梱ユーティリティ
        app.Use<GlobalLoggingMiddleware>(); // 内製ロギングミドルウェア
        app.Use<ShowErrorMiddleware>(); // 内製例外時表示ミドルウェア
 
        app.Map("/api", builder =>
        {
            var option = new LightNodeOptions(AcceptVerbs.Get | AcceptVerbs.Post,
                new LightNode.Formatter.Jil.JilContentFormatter(),
                new LightNode.Formatter.Jil.GZipJilContentFormatter())
            {
                OperationCoordinatorFactory = new GlimpseProfilingOperationCoordinatorFactory(),
                ErrorHandlingPolicy = ErrorHandlingPolicy.ThrowException,
                OperationMissingHandlingPolicy = OperationMissingHandlingPolicy.ThrowException,
            };
 
            builder.UseLightNode(option);
        });
        // Indexはデバッグ画面に回す
        app.MapWhen(x => x.Request.Path.Value == "/" || x.Request.Path.Value.StartsWith("/DebugMenu"), builder =>
        {
            builder.UseFileServer(new FileServerOptions()
            {
                EnableDefaultFiles = true,
                EnableDirectoryBrowsing = false,
                FileSystem = new PhysicalFileSystem(@".\DebugMenu"),
            });
        });
        // それ以外は全部404
        app.MapWhen(x => !x.Request.Path.Value.StartsWith("/Glimpse.axd", StringComparison.InvariantCultureIgnoreCase), builder =>
        {
            builder.Run(ctx =>
            {
                ctx.Response.StatusCode = 404;
                return Grani.Threading.TaskEx.Empty;
            });
        });
    }
}

インデックスでアクセスすると表示するページはGlimpse.axdと、シングル全画面ページで表示できるローンチ部分へのリンクを貼っつけてあります。

<!DOCTYPE html>
<html xmlns="http://www.w3.org/1999/xhtml">
<head>
    <title>Debug Index</title>
</head>
<body>
    APIのデバッグ<br />
    <p>
        <a href="../../Glimpse.axd?n=glimpse_redirect_popup">Glimpse Launch</a>
    </p>
    <p>
        <a href="../../glimpse.axd">Glimpse Config</a>
    </p>
</body>
</html>

まぁ、こういうのあると、Glimpseへのアクセスが近くで非常に便利です。

あと最後に、OWINでやるならこーいうのどうでしょう、というWeb.config。Owin Middlewareと機能重複して鬱陶しいからHttpModule丸ごと消そうぜ、という過激派な案ですにゃ。

<?xml version="1.0" encoding="utf-8"?>
 
<!-- OWIN向けウェブコン -->
<!-- Glimpse系のはリリース時にはxsltでまるっと消す -->
<configuration>
    <configSections>
        <section name="glimpse" type="Glimpse.Core.Configuration.Section, Glimpse.Core" />
    </configSections>
    <connectionStrings configSource="<!-- 接続文字列は外部に回す(DebugとReleaseでxsltで変換して別参照見るように) -->" />
    <appSettings>
        <!-- なんかここに書いたり外部ファイルとmergeしたり:) -->
    </appSettings>
    <system.web>
        <!-- system.web配下のは片っ端から消してしまう -->
        <httpModules>
            <clear />
            <add name="Glimpse" type="Glimpse.AspNet.HttpModule, Glimpse.AspNet" />
        </httpModules>
        <httpHandlers>
            <clear />
            <add path="glimpse.axd" verb="GET" type="Glimpse.AspNet.HttpHandler, Glimpse.AspNet" />
        </httpHandlers>
        <roleManager>
            <providers>
                <clear />
            </providers>
        </roleManager>
        <customErrors mode="Off" />
        <trace enabled="false" />
        <sessionState mode="Off" />
        <httpRuntime targetFramework="4.5" requestPathInvalidCharacters="" />
        <globalization culture="ja-jp" uiCulture="ja-jp" />
        <!-- リリース時にxsltでfalseにする -->
        <compilation debug="true" />
    </system.web>
    <system.webServer>
        <validation validateIntegratedModeConfiguration="false" />
        <globalModules>
            <clear />
        </globalModules>
        <modules>
            <!-- モジュールも全消し -->
            <remove name="OutputCache" />
            <remove name="Session" />
            <remove name="UrlRoutingModule-4.0" />
            <!-- 以下デフォで読まれるモジュール名が延々と続く(system.webServer下は一括clearが使えなくて辛い)... -->
            <add name="Glimpse" type="Glimpse.AspNet.HttpModule, Glimpse.AspNet" preCondition="integratedMode" />
        </modules>
        <handlers>
            <add name="Glimpse" path="glimpse.axd" verb="GET" type="Glimpse.AspNet.HttpHandler, Glimpse.AspNet" preCondition="integratedMode" />
        </handlers>
    </system.webServer>
    <!-- おまじない(笑)セクション -->
    <system.net>
        <connectionManagement>
            <add address="*" maxconnection="1024" />
        </connectionManagement>
        <settings>
            <servicePointManager expect100Continue="false" useNagleAlgorithm="false" />
        </settings>
    </system.net>
    <!-- WebServiceでやるならPersistResultsで(当然このセクションもリリースでは消す) -->
    <glimpse defaultRuntimePolicy="PersistResults" endpointBaseUri="~/Glimpse.axd">
        <tabs>
            <ignoredTypes>
                <add type="Glimpse.AspNet.Tab.Cache, Glimpse.AspNet" />
                <add type="Glimpse.AspNet.Tab.Routes, Glimpse.AspNet" />
                <add type="Glimpse.AspNet.Tab.Session, Glimpse.AspNet" />
                <add type="Glimpse.Core.Tab.Trace, Glimpse.Core" />
            </ignoredTypes>
        </tabs>
        <runtimePolicies>
            <ignoredTypes>
                <add type="Glimpse.Core.Policy.ControlCookiePolicy, Glimpse.Core" />
                <add type="Glimpse.Core.Policy.StatusCodePolicy, Glimpse.Core" />
                <add type="Glimpse.Core.Policy.AjaxPolicy, Glimpse.Core" />
                <add type="Glimpse.AspNet.Policy.LocalPolicy, Glimpse.AspNet" />
                <add type="Glimpse.Core.Tab.Trace, Glimpse.Core" />
            </ignoredTypes>
        </runtimePolicies>
    </glimpse>
</configuration>

Web API的なサービスでもGlimpse使えるよ!ってのはもっと知ってほしいかしらん。その辺はLightNode 1.0、或いはWeb APIでのGlimpseの使い方で詳しく解説しています。

Prev |

Search/Archive

Category

Profile


Yoshifumi Kawai
Microsoft MVP for .NET(C#)

April 2011
|
March 2016

Twitter:@neuecc