2015年を振り返る
- 2015-12-31
振り返るシリーズ第四弾。去年の目標は
テーマは「クライアントサイドとサーバーサイドをC#で統一することのメリットの実証」「さらにリアルタイムネットワークもC#で統一」「のためのヒットアプリケーションの創出」です。指向はあんま変わってないんですが、より具体的に。来年は動く年かな、といったところ
ようするに会社(グラニ/CTO)でゲーム出して実証する。であり、結果としては……。はい。なので、本当に来年こそはね。というところです。
C#
今年はVS2015のリリースということもあり、かなりRoslynに傾倒しました。その集大成としてのまとめは実例からみるC#でのメタプログラミング用法集にスライドで出してますが、その後にもAnalyzerだけではなく、Roslyn C# Scriptingによる実行できるコンフィグの手法と実活用例やWorkspace APIを使ったプロジェクトコードからのT4生成での応用例といった形で、ただたんに触ってみた、ではなくて、実用的にどう使えるのか、の応用例をきっちり掲示できたんじゃないかと思います。これらの話は実際に使っているもので、便利、かつ世界が広がったのは確かなので、Roslynはこれからどんどん応用的に使われるといいかな。
また、いつになくライブラリ書いてました。会社で必要だから作ってったという面も大きいんですけれど、こうして並べると、実際結構やりましたね、えらいえらい。
まずAPIサーバーとしてLightNodeという自家製フレームワークを採用しているので、Glimpse対応とSwagger対応を入れてます。この対応は大正解で、もはやGlimpseとSwaggerなしでAPI開発していくのは無理ゲーとすら思える。超絶便利。Glimpseは主要開発者がMicrosoft入りしてより専任して開発することになったのと、Swaggerはもはやウェッブ標準といってもいい立ち位置を確立したということで、どちらもメジャーなテクノロジとなったことも含めて、技術選定にも成功したと言えるでせう。ASP.NET 5がまだまだ時間かかるので様子見したのも含めて、現状での最適解ではあるんじゃないかな、と。
RedisライブラリのCloudStructuresもGlimpse対応とStackExchange.Redis対応によって、相当リッチなものになりました。特にGlimpseのは相当気合入れたので充実してて実用度超絶高い。とはいえ本体部分は原則的には薄いラッパーなので、粛々とやってきます、というところですね。
LINQ to BigQueryはLINQPad Driverを作りました!これで実用度が飛躍的に上がりました。世間的にもデータ分析はBigQueryで決まりだよね!という流れが出来上がった年なわけですが、ここも乗り遅れずというか、むしろ引っ張る側に回れたのではないかと自負するところです。あとLINQPadにも相当詳しくなった。
今年最後の新顔はEtwStreamで、↑でLINQPadに詳しくなった結果、応用例が頭に浮かんで一気に実用に載せられました。ETW(Event Tracing for Windows)のビューアーとしてまともに使える世界唯一の解といっても過言ではない(むしろ今までのフベンサがヤバすぎた……)。年末にはRoslyn C# Scriptingによる実行できるコンフィグというコンセプトを打ち出したOut-of-Process Serviceも追加していて、来年はこれの稼働実績を作っていきたいところです。
変わり種だけど誰にとっても実用度100億なのはOpen on GitHubというVS拡張で、もはやこれなしでぎっはぶでコミュニケーションを取るのは無理なのでは疑惑もあるほどに神拡張。地味に12/29にVer 1.4.0のアップデート出してます。
そしてUniRx。今年はお陰様で大躍進の年で、GitHubスター順でも、GitHubで公開されてるUnity用ライブラリでは世界4位と、中々に中々の感じで、いやほんと良かった。
年の始めではReacitvePropertyやPresenterBaseの導入によるModel-View-(Reactive)Presenterというコンセプトの確立が地味に大きめ(ちなみに私は設計とかってこのレベルの話こそ最も大事だと思ってて、コードを小奇麗にするようなレベルの話は設計、ですかねえ?とは思ってる)。真ん中でObservableTriggers、そして年末駆け込みのUniRx 5.0で完全書き直しによるパフォーマンスとデバッガビリティの向上。もはやUniRxなしで書けと言われると困りすぎるぐらいに必要不可欠な存在となれました。
UniRxが成功したポイントは、Reactive Extensions自体が素晴らしいコンセプトで実績もある、というのも勿論そうなんですが、UniRx自体の、Unityへフィットさせるための繋ぎ方の工夫の面も大きいとは自負してます。あまり頭でっかちにならずに柔軟に作り込めたのが良かった。ReacitvePropertyなんかも、実装はシンプルですが、(.NET版を作った時の)コンセプト立証と実装には普通に時間かけてたし、そもそもコロンブスの卵的なところもありましたし、更にそのUnity化でも、削ぎ落とし方には相当神経使ってます(ゴテゴテ足すだけが設計ではない)。
また、お陰様でUnityのスクリプティングと、そしてRxには詳しすぎるほどに詳しくなれました、というかRxに関しては実際隅から隅まで理解した……。
現在未公開だけど予定があるものとしてはPhotonWireというUnity/Photon Server用の非同期RPCフレームワークがあります。これは来年の初頭に公開できればな、ですかねー。
お仕事
書いたライブラリは基本的に会社でフルに使ってるものなので、成果っちゃあ成果です。が、集中力散漫な年でした。成果として些かライブラリ過多になったのは、それが時間の捻出が比較的しやすい(土日とかにちょっと徹夜して気合いれればグッと形にできたりするんで)というのが大きいですね。細切れな時間で、いまいちうまくプロダクトそのものにコミットできなかったのは減点度大きめで、非常に良くない。ライブラリ側の成果と相殺してプラマイゼロと言いたくはあるんですが、私的な理想像とは離れたところにあるので、まぁ、60点ぐらい……(ちょっと甘め)
いろいろと白髪が増える、ハゲる。といったぐらいな感じです、うみぅ。来年は100点目指します。
ゲームとか音楽とか
今年はライブにまぁまぁ行ったんですが、年末のKing Crimsonの来日講演が震えすぎたのでそれが全て。生きててよかったというか生きてるうちに生で聴けることがあるなんて……!内容もばっちしで、いやはやホンモノは永遠にホンモノであり続ける、というかホンモノであり続けようとする姿勢に感服するばかりです。この編成での本気のライブ版が早く出てほすぃ。
面白かったといえばSquarepusherの来日講演も見に行ったのですが、実に面白かった。サイトでは360度動画が公開されてますが、そうした全方位動画やプロジェクションマッピング、VRの未来すら感じられて、思い出すと行けて本当に良かったなぁ。
ゲームはDownwellがオモシロイッス。
漫画は、iPhone 6s Plusを買ってから、スマフォで本や漫画が十分に読めるようになってしまって革命的に体験が変わった。いやあ、大画面スマフォ(ファブレット)は良いですね!一度体験すると、もう小さいのには戻れない。というわけで基本Kindleで買ってiPhoneで読むというインスタントな生活になりました。今年読んだ中だとヴァーチャル・レッドが良かったかなぁ、終始、陰鬱な空気が流れていて息苦しいんだけどどこか心地良くもある。気に入りすぎてKindleで読んだ後に実書籍のほうも買ったんですが装丁がよく出来てて、そちらも満足度高し。
来年
シンプルに、全力で仕事してゲーム出す。ですね。それ以外なし。もちろん、それはC#を全方面で活かした実証結果として成り得るものです。絶対。これは絶対。
私個人としてはヌルヌルとC#でのスクリプティングにだけ篭もりすぎた感あるので、UniRxも一段落したことだし(ちなみに、もう少し性能向上のためのアップデートが控えていてというか現在審査中なので年明け早々にそれはリリースされるでしょう)、シェーダーをそれなりにすらすら書ける程度にはグラフィック処理もできるようにってのは年頭の宿題にしておきます。
UniRx 5.0 - 完全書き直しによるパフォーマンス向上とヒューマンリーダブルなスタックトレース生成
- 2015-12-21
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;
という人畜無害なコードがあるとして、以前のスタックトレースはこうです。
言ってることはわからんでもないコンパイラ生成の何かと、多量の中間物で埋まっていて、実に読み取りにくい。この程度のメソッドチェーンならまだマシで、もっと長大で、複雑なオペレータが絡んでる場合は困難極まってました。私も何度文句を言われて平謝りしたか分からないぐらいです。しかし、今回のバージョンからはこうです。
自動生成コードなし、中間物ナシ。圧倒的な読みやすさ!また、これはそのまま、書いたとおりに動いているということの証左でもあります。実行パイプラインの無駄がスタックトレースに出ているままに皆無になったので、パフォーマンスにも寄与しています(書き換えた今では、もはや前のが厚すぎた説はありますけれど、それはまぁ言わんといてください……)
実装はかなりメンドウで、ラムダ式を使うと問答無用でコンパイラ生成のクラスが吐かれてしまうので、ひたすら名前付きのクラスを作っていくお仕事をしました(一個のオペレーターにつき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のテストランナー使ったほうがやりやすいじゃん?
そうやってユニットテスト自体は書かれてるし、さすがに実機用に別のを書きなおすのは不可能なので、このユニットテストを実機で動かせるように持ってければそれでいいんだよねー。
ここで出てくるのがRoslyn。Roslynを使ってユニットテストプロジェクト内のユニットテストを、ソースコードのファイル単位ではなく、解析可能な構文木単位で取得し、T4 Text Templateで整形して吐き出せちゃえばいいんだ、という合わせ技で運搬することに成功しました。VS2015だから出来るハック、VS2015最高……。さすがにコード持ってくだけではMSTestの実体がなくて動かないんですが、そこは適当にモック(Shim)を用意して回避しました。
エクストリーム雑な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による実行できるコンフィグの手法と実活用例
- 2015-12-13
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。
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ではBlitz4jやlog4j2のAsynchronous Loggerなどのスピード競争があるのに、.NETの牧歌的なこと、といった感じは否めませんねぇ。そんなだから私のとりあえずの雑実装でもfastestになってしまうわけで……。
出力先
EtwStreamが提唱するのは構造化ログ(Structured/Semantic Log)ですし、テキストログが終着点ではありません!テキストログは無視して、構造化されたペイロードを、そのままAzure EventHubsやAmazon Kinesis、Google 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によるシーンナビゲーションで統合する
- 2015-12-03
今回は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から搭載されることになりました。実際どういうことになるかというと、ヒエラルキーウィンドウがこんな感じに。
シーン加算で読み込んだシーンがヒエラルキー上でもきっちり分けられます。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がヒエラルキーの頂上にある必要があります。こんな感じに。
うーん、随分と大きな制約であり不格好ですね……、この手の制約は実際のトコ、ないほうが望ましいです。別に、この手のヘンテコな制約をつけるのがアーキテクチャ、ではないです。自由なほうがよほど良いのです。とはいえしかし、どうにもならなかったので、そこは受け入れるしかなかったということで。この辺が今のところの手札でできる精一杯の形かなぁ。
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とか。が、しかし、両者には非常に大きな違いがあります。それは、完了時のタイミング。
わざわざ無駄に画像を作ってまで声を大にして言いたいんですが、直接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で対応させてみましょう。
この、あんまり良くわからない例、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の生成順序は不定なので、値の伝搬というのは単純なようで深く、やりようが色々あるテーマだったり)。さて、一見複雑というか実際、色々ゴテゴテしてきてアレな気配を醸しだしてきましたが、実際どんな状態なのかというと、こんな感じ。
この分かったような分からないような図で言いたいことは、値の流れです。シーン間は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
After
劇的!Unityのスタックトレースの表示形式に100%フォーカスして、読みやすさ第一にハックしたので、圧倒的な読みやすさだと思います。スタックトレース芸極めた。普通にWhere.Select.Take.Subscribeがそのまま表示されてますからね。勿論、メソッドコール数が減っているのは単純に性能にも寄与しています。ここまでやれば文句もないでせう。
そんなvNextの完成時期ですが、今までやるやる詐欺すぎたのですが、そろそろ実際本当に出します。来週ぐらいには本当に出します。これは意地でも仕上げます(想像通りだけれど作業量は多いわコーナーケースの想定が複雑すぎて頭が爆発しそうになるしで辛い……)。というわけでもうちょっとだけ待っててください。
EtwStream - ETW/EventSourceのRx化 + ビューアーとしてのLINQPad統合
- 2015-11-03
EtwStreamというのをリリースしました。ETW(Event Tracing for Windows) + EventSourceが.NETで構造化ログをやる際の決定版というか、ETWの最強度が高すぎてそれ以外考えられないレベルなんですが、しかし、がETWは最強な反面ビューアーがありませんでした。ETWというブラックホールにログを投げ込むのはいいんですが、それが自分自身ですら容易に見れないのは不便すぎる!PerfViewとか骨董品みたいなゴミUIを操ってなんとかして見るのは、無理ゲーなわけで、カジュアルにDumpしたいだけなんだよ!テキストのようなログビューアーが欲しいだけなんだよ!に対する答えです。いや、ほんと自分自身が死ぬほど欲しかったのが、これ。
インストールは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クライアント
- 2015-10-25
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統合があります。
そう、DumpToExcel()で実行すると結果セットがダイレクトにExcelで開く……。しかし実際こういうのでいいんだよこういうので感あります。Excelでクエリ書く系の統合は面倒くさい(実際アレはダルいのでない)。いちいちCSVに落として開くのは面倒くさすぎる。LINQPadでクエリ書く、結果がExcelで見れる。あとはピボットテーブルなりで好きに分析できる。そう、そういうことなんですよ、これなんですよ #とは
入れ方
ExplorerのAdd Connection→View More Drivers からLINQ to BigQueryを探して、clickでインストールできます。簡単。
かなり上の方のいい位置に入れてもらいました!
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.を押して探せるし
慣れきった関数なら、直接書くことができる。完璧。
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で非同期を扱う場合のパターン集
- 2015-10-23
UniRxのGitHubのStar数が500行きました!
今のところ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#でのメタプログラミング用法集
- 2015-09-29
Metaprogramming Universe in C# - 実例に見るILからRoslynまでの活用、という題でMetro.cs #1にて話してきました。
現在、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アプリを美しく
- 2015-09-10
なんとブログ書くのは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勉強会を開催しました+スライドまとめ
- 2015-06-20
と、いうわけかでUniRx勉強会を開催しました。当日の模様はtogetterまとめで。登録が150人ほど、生憎の雨天でしたが130人以上来てくださってめっちゃ嬉しかったですね。慣れないというかはぢめての主催+司会でその辺アレだったのですが、会場をお貸し下さったgloopsさんの手厚い協力のお陰で、なんとか成立させることができ、ほんとうに感謝です。
私の発表資料は「History & Practices for UniRx UniRxの歴史、或いは開発(中)タイトルの用例と落とし穴の回避法」になります。
あまりUniRx固有、という感じでもなく、また凄い話、でもなんでもない地味な内容なのですけれど、ちょっとはまると嫌だなー、けどはまりがちなポイントを説明してみた、といった感。地味すぎてトリとしてはなんともいえない感じでしたね、うむむむ。ちなみにReal World UniRxというのは、Real World Haskell―実戦で学ぶ関数型言語プログラミングという本が名前的には元ネタです。Real World、現実世界で使われるUniRx。というわけで要約すれば事例求む、みたいな。
はじめてのUniRx
toRisouPさんの発表です。
資料的価値が非常に高く、わかりやすい。めっちゃ読みこむと良いと思います、スゴクイイ!Cold/Hotとか大事なんですがむつかしいところですしねー。
若輩エンジニアから見たUniRxを利用したゲーム開発
gloopsの森永さんの発表です。
toRisouPさんのが中級者向けでしたので、こちらが初心者向けでしたね。UniRxがどういう風に自分の中で浸透というか理解が進んでいくか、というのがstep by stepで紹介されていて、伝わりやすいと思います。あと、全然紹介していなかったObservableTriggerまわりの応用が完璧に書かれていてすばら。
Interactive UI with UniRx
トライフォートの岩下さんのセッションです。
UniRxって基本的にスクリプティング領域の技術なので、とにかく地味!なのですが、このセッションは地味どころかDemo含め、めっちゃ伝わるし美しさ、手触りが伝わって凄かった。実際本日一番の感動でしたにゃ。
「ずいぶんとダサいライティングを使っているのね」〜UniRxを用いた物理ベースライティング制御〜
ユニティ・テクノロジーズ・ジャパンの名雪さんのLTです。
色々なものへのReactiveな入力/出力ができるんじゃもん!と言ってはいるし興味はかなりあるのだけれど、自分でやったことが全くない領域で、それが実際になされてる様を目にするとオオーッってなりました。
その他
そういえば、ブログに書いてなかったんですがちょっと前に「Observable Everywhere - Rxの原則とUniRxにみるデータソースの見つけ方」という発表をしていました。
これ、自分的には結構良い内容だなー、と思っているので見たことないかたは是非目を通してもらえると。
まとめ
第一回、というわけなんですがかなり密度濃い内容になったのでは!?懇親会でも、自分の思っていたよりもずっと遥かに使い出している、注目している、という声をいただき嬉しかったですねー。もっとドンドン良くしていかなければ、と気が引き締まります。次回がいつになるかは完全不明(というか当分後かな?)ですが、やっていきたいなー、と思いましたです。
NotifyPropertyChangedGenerator - RoslynによるVS2015時代の変更通知プロパティの書き方
- 2015-06-13
半月前にIntroduction to NotifyPropertyChangedGeneratorというタイトルでセッションしてきました。
コードは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時代の到来について
- 2015-05-11
UniRx - Reactive Extensions for Unity用に、メソッド呼んだだけで何も処理してないIObservable<T>があったらWarningを出すAnalyzerを作ってみました。
AnalyzerはVisual Studio 2015からの機能です。というわけでVisual Studio 2015 RCが必要です。あとは、NuGetからAnalyzerが入れられるようになっているので
- Install-Package UniRxAnalyzer
で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のデバッグ実行
- 2015-04-19
グラニの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なりから毎度実行は効率悪すぎてありえないし、POSTMANやFiddlerで叩くのも面倒くせえ。
そこで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として実装してあります。
- PM> Install-Package LightNode.Swagger
ルートを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連携によるデータバインディング
- 2015-04-13
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 ?? ( 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」と題して発表を行います。こちらはニコ生での放送もあるようなので、見るといいんじゃないかなー、ということで!
Microsoft MVP for .NET(C#)を再々々々受賞しました
- 2015-04-02
今年も受賞で、5年目です。実は今年から受賞分野がC#が.NETに統合されたので、エキスパタイズとしてはfor .NETになります。
会社は第一段階が終わり、といった感じで、それに付随する活動内容としても総まとめみたいなものが多かったかな、といったところでしょうか。今年はまた次の段階の始まりということで、より新しい勝負が必要になってきています。今、私が主に力を入れているのはUnityと、そのReactive Extensions実装のUniRxで、特にUniRxはかなりヒットさせられたとは思います。が、まだまだ兆しといったところなので、確固たるものにしなければならない。また、それを基盤にして、C#の強さというのを、ただの今までの.NETコミュニティにだけに留まらず、幅広い世界に届ける、伝えていきたいし、幸いにして私はそれが出来る立場にいると思っています。
より力強く、Real World C#というのを示し続けてきます。そんなわけで引き続き、今年もよろしくお願いします。