Archive - Unity

Unityにおけるコルーチンの省メモリと高速化について、或いはUniRx 5.3.0でのその反映

UniRx 5.3.0をリリースしました!今回のアップデートは、内部的な最適化、です。最適化は、もうそろそろあんまやるところ残ってないよね、なんて思ってたんですが、じっくり考えるとそんなことなく割とあったので埋めました。それが表題のコルーチンの省メモリと高速化です。使い方次第ではありますが、場合によっては今回のアップデートでものすごく恩恵に授かる人もいればそこそこの人もいるかもです。ともあれ基本的に内部的に変更してるだけなので、入れるだけでそれなりに高速化したりする可能性がそれなりにあります。

前回が2月だったので3ヶ月ぶりですね。あまりオペレータ追加がないので、次はオペレータ追加に集中したい気もする。なんか優先的に欲しいのあればリクエストもどうぞ(Observable.Windowとかいい加減そろそろ入れろよって話なんですが)

MicroCoroutine

今回の大きい変化はMicroCoroutine(と、自称してる)の導入です。特に大量にEveryUpdateやEveryValueChangedを呼んでるシチュエーションにおいて10倍、というのは場合によりで大雑把なのですが、相当速くなります。

void Start()
{
    // Start 10000 Coroutines
    for (int i = 0; i < 10000; i++)
    {
        // Standard Unity Coroutine
        // StartCoroutine(Counter());
 
        // Use UniRx 5.3 - MicroCoroutine
        MainThreadDispatcher
          .StartUpdateMicroCoroutine(Counter());
    }
}
 
IEnumerator Counter()
{
    while (true)
    {
        count++;
        yield return null;
    }
}

こんな10000個、単純なコルーチンを起動するコードがあったとして

image

大きく違いがでます。ちょっと恣意的すぎではあるんですが、UniRxはコルーチンを簡単にかけるが故に、これに近いシチュエーションってのが意図せず起こりがちではありました。また、Resources.LoadAsyncなど非同期系APIからの取得に関しても、一時的に多くのコルーチンを起動するシチュエーションはあり得るのではないでしょうか。

性能改善した理由は、基本的にはUnityの公式ブログUPDATE()を10000回呼ぶで紹介されていることの話で、10000個のUpdateは遅くて、配列に詰めて直接ループで呼ぼうぜ、と。どうせUpdate内のメソッドは呼ばれてC#の領域で実行されるんだから、マネージド(C#)-アンマネージド(C++)の繋ぎのレイヤーは純粋にオーバーヘッドになってくるよ、と。なるほどそうだねそりゃそうだねぇ。それはStartCoroutineにも言えて、というかコルーチンのほうがもっと性能劣化度が大きいんですよね。

この記事は非常に素晴らしくて、大量にモノ出して速度遅くなってるのがスクリプト起因なら、マネージャー立ててまとめて、あとUpdateに限らずマネージド-アンマネージドの繋ぎをやってる部分が遅いだろうからそこを適切に取り除ける限り除けば、全然まだまだそれなりに捌ける余裕は残ってるぜ。ということで、むしろ希望に満ちていていい感じです。実際、ハイパフォーマンスを謳うDOTweeenとかのライブラリもそんな感じですね、動かすものそれぞれにUpdateするコンポーネントを挿したりはしない、中央管理で動かすのだ、と。

さて、UniRxでは幾つかのメソッドはコルーチン依存でループを回しています。Observable.EveryUpdateとかEveryValueChangedとか。少しに使う分にはいいんですが、気楽に使えるが故に、大量に使うと、10000個とまではいかなくてもやっぱり、それぞれがコルーチンを起動することによるマネージド-アンマネージドオーバーヘッドがそのまま乗っかってきてしまいます。というわけで、やはりコルーチン自前管理の道を進むしかない……。幸い、自前管理で問題になる機能面での低下に関しては、UniRx自体がコルーチンを凌ぐだけの機能を提供しているので、気にしないでよし。というわけで純粋にいかにコルーチン(IEnumerator)を高速に回転させ、高速にメンテナンスするかにだけ集中すればよし。

回転させるのはforループ回すだけの話なんですが、マネージャー作ろうぜ、となった時に、Listに詰めるのはいいんですが、面倒くさいのは削除。削除は要注意で、単純にListのRemoveやって済ませたりするのは結構アレです(Removeは相当高コストな操作です)。かといってDictionaryやSet、LinkedListでやるなんていうのは論外で(列挙の性能が死ぬので本末転倒)、基本的に配列で頑張るべきなんですが、さてはて。結局、その辺のめんどーを見るのがめんどーだからUpdateやStartCoroutineでぶん回すのだ。割と本気で。

ではどうしたか、というと、UniRxのMicroCoroutineのアプローチはRemoveしない。です。しない。空いた部分はnullで埋めて純粋にスキップするだけにする。多少の空きなら、いちいち削るよりもスキップさせたほうが速い。しかし、それだけだとブヨブヨと膨らみ続けてしまうので、xフレーム毎に空きスペースに詰めなおして小さくします。縮める際も前の方に整列させるんじゃなくて、空きスペースに対して後ろから埋めるようにするので、順番はグチャグチャになります。その代わり余計な配列へのセットが発生しないので速い。そして膨らんだ配列は放置して膨らんだままにします、終端のインデックスだけ記録して管理するのみ(ところでアセットストアにアップデート申請出してから気づいたのですが、この配列の使い方なら定期的なお掃除じゃなくて、動かしながら埋めるようなコードにするのも可能っぽい感、なので次回アップデートでそうします)

というわけで、UniRxのMicroCoroutineは中央集権的なので多少膨らむことが許される(でしょう!)ことを利用して、とにかく高速にコルーチンを捌く、ということだけに集中してます。ので速い。下手に自前管理するよりも速いかもしれませんし、Updateで監視するよりもObserveEveryValueChangedのほうがむしろ速い、Rxで書いたほうが速い、みたいな逆転現象も全然発生しうるような話になります。

ObserveEveryValueChanged
EveryUpdate 
EveryFixedUpdate
EveryEndOfFrame
NextFrame
TimerFrame 
IntervalFrame
DelayFrame 
SampleFrame
ThrottleFrame
ThrottleFirstFrame
TimeoutFrame

この辺りのメソッドを使った場合、内部の実装がMicroCoroutineに差し替わったので自動的に恩恵に預かれます。コルーチン -> Observable変換に関しては FromMicroCoroutine が追加されました。基本的にはFromCoroutineと一緒なのですが、MicroCoroutineではyield returnするのはnullだけにしてください、それ以外には対応してません(UnityEditor上ではWarning出して警告します)。MicroCoroutineの制約はそれなんですが、なんだかんだで、8割ぐらいはyield return nullだけで成立するんちゃうんちゃうん、みたいな。賢くやろうとすればもう少しは出来なくもないんですが、シンプルで高速なコルーチンの回転を損ねちゃうのでナシ。IEnuemrator.Currentの呼び出しや、その型チェックすら省きたい。残り2割ぐらいなら普通にStartCoroutineすればいいじゃん、ということで。実際、UniRxの↑のメソッドはそれでかなり置き換えることが出来る、ということを発見できたので、全面的に導入する気になったのです。

また、最悪待ちたい場合は、isDoneのループを回すようにToYieldInstruction経由でIObservableを待てるので、大抵のことはなんでもできます。

IEnumerator MicroCoroutineWithToYieldInstruction()
{
    var www = ObservableWWW.Get("http://aaa").ToYieldInstruction();
    while (!(www.HasResult || www.IsCanceled || www.HasError)) // 3つもプロパティ並べるのダルいので次回アップデートでIsDoneを追加します予定
    {
        yield return null;
    }
 
    if (www.HasResult)
    {
        UnityEngine.Debug.Log(www.Result);
    }
}

もっとプリミティブに直接利用したい場合は、StartCoroutineの代わりにMainThreadDispatcherに3つ生やしてあります。

MainThreadDispatcher.StartUpdateMicroCoroutine
MainThreadDispatcher.StartFixedUpdateMicroCoroutine
MainThreadDispatcher.StartEndOfFrameMicroCoroutine

それぞれがコルーチンを消費するタイミングで、まぁ普通はStartUpdateMicroCoroutineを使えばよいでしょふ。もし大量のStartCoroutineがプログラム中にあるのなら、これに差し替えるだけで本当にすっごく速くなるでしょう。ほんと。

SubscribeWithState

ここから先はUniRxのアップデートの話だけ。そして本当にMicro Micro Microな最適化であんま意味はないんですが、まず、SubcribeWithStateを追加しました。これによって何が変わるか、というと、例えば……

// Before
public static IDisposable SubscribeToText(this IObservable<string> source, Text text)
{
    return source.Subscribe(x => text.text = x);
}
 
// After
public static IDisposable SubscribeToText(this IObservable<string> source, Text text)
{
    return source.SubscribeWithState(text, (x, t) => t.text = x);
}

という感じの使い方ができます。どういう違いが出るのかというと、以前にUnityでのボクシングの殺し方、或いはラムダ式における見えないnewの見極め方という記事の中で説明したのですが、ラムダ式はその中身によってコンパイル時に生成されるコードがかなり変わってきます。で、最速なのはそのメソッド内だけで完結していて外部の変数等には一切触っていない状態。onNextはActionなので、副作用かける際にどうしても外部変数をキャプチャしてしまうことが多いんですよね。そこでSubscribeWithStateを使うと、必要な変数を閉じ込めることができるので最速ゴミなしの形で記述できます。

ただまぁ、これやると、じゃあSelectやWhereなんかもState取れたほうがいいんですか?(理屈上はそうです)、とか、ああクロージャ殺さなきゃ死ね死ね死ね、とか思ったりしそうなのですけれど、Subscribeの回数ってパイプライン内の実行頻度に比べれば圧倒的に少なくなるはずなんですよね。だから全体のバランスで見たら無視できるといっても過言ではないはず、特にクロージャでちょっとゴミが出る程度の話は。

なのであんま神経質にやることはないんですが、↑のSubscribeToTextのようなそんな手間もかからないし、UIとかシーンの初期化時にいっぱい登録される可能性があるようなものでライブラリ的な部分でカバーできる質のものならば、少しだけ気を使ってあげると気は安らぐかもしれません。

ReactiveCommand

ReactiveCommandは.NET版のReactiveProeprtyにあった、最後のパーツなんですが、どうなんでしょうね、本来はViewModelのレイヤーのためなんですが、UnityだとPresenterにUI要素がセリ出してきてるのでイマイチベンリかどうか分からなくて入れてなかったんですが。一応、こんな風に使えます。

public class Player
{
   public ReactiveProperty<int> Hp;
   public ReactiveCommand Resurrect;
 
   public Player()
   {
        Hp = new ReactiveProperty<int>(1000);
 
        // If dead, can not execute.
        Resurrect = Hp.Select(x => x <= 0).ToReactiveCommand();
        // Execute when clicked
        Resurrect.Subscribe(_ =>
        {
             Hp.Value = 1000;
        }); 
    }
}
 
public class Presenter
{
    public Button resurrectButton;
 
    Player player;
 
    void Start()
    {
      player = new Player();
 
      // If Hp <= 0, can't press button.
      player.Resurrect.BindTo(resurrectButton);
    }
}

buttonのinteractableとonClickが抽象化されたもの、って感じですね。

その他

リリースノートから。

Add : ReactiveCommand
Add : MainThreadDispatcher.StartUpdateMicroCoroutine, StartFixedUpdateMicroCoroutine, StartEndOfFrameMicroCoroutine
Add : Scheduler.MainThreadFixedUpdate, MainThreadEndOfFrame
Add : ToYieldInstruction(cancellationToken)
Add : Observer.Create(onNext/onNext, onError/onNext, onCompleted) overload
Add : IReadOnlyReactiveProperty.SkipLatestValueOnSubscribe
Add : Observable.WhenAll overload (IObservable<Unit>(params IObservable<Unit>[] sources), this becomes breaking changes)
Add : Observable.FromMicroCoroutine
Add : Observable.AsSingleUnitObservable
Add : Observable.SubscribeWithState
Add : Observable.CreateWithState
Add : Disposable.CreateWithState
Improvement : Use MicroCoroutine on `ObserveEveryValueChanged`, `EveryUpdate`, `EveryFixedUpdate`, `EveryEndOfFrame`, `NextFrame`, `TimerFrame`, `IntervalFrame`, `DelayFrame`, `SampleFrame`, `ThrottleFrame`, `ThrottleFirstFrame`, `TimeoutFrame`
Improvement : Performance improvement for Observable.Range, Repeat when scheduler is Scheduler.Immediate
Improvement : Use Time.unscaledDeltaTime in IgnoreTimeScaleMainThreadScheduler
Fix : ReadOnlyReactiveProperty(source, initialValue) does not publish initial value on subscribe
Fix : IReadOnlyCollection has set indexer
Fix : Ambigious property of IReactiveCollection.Count, Indexer
Fix : Throw invalid error when ObservableWWW.LoadFromCacheOrDownload failed.
Breaking Changes : Added IReadOnlyReactiveProperty.HasValue
Breaking Changes : AsyncConvertsion scheduler to Scheduler.MainThread on WebGL build(WebGL doesn't support ThreadPool)
Other : Update UniRxAnalyzer 1.4.0.1 https://www.nuget.org/packages/UniRxAnalyzer

ToYieldInstructionはUniRx 5.0 - 完全書き直しによるパフォーマンス向上とヒューマンリーダブルなスタックトレース生成で説明しているのですが、Unity 5.3以降のCustomYieldInstuctionを応用したもので、IObservableをコルーチンで処理できるようにするやつで、結構お薦め機能です。MicroCoroutineで回すための補助にもなりますし。

SchedulerにMainThreadFixedUpdateとMainThreadEndOfFrameを足しました。ObserveOnやTimerなどで、その辺の細かい制動をしたい方にどうぞ。

(ReadOnly)ReactivePropertyへのSkipLatestValueOnSubscribe拡張メソッドの追加。これは、(UniRxの)ReactivePropertyはSubscribe時に必ず値をプッシュするようになってるんですが、そういった初期値を無視したいって局面は少なからずあるんですよね。Rx.NET用のReactivePropertyでは、コンストラクタでReactiveProeprtyModeとして、None | RaiseLatestValueOnSubscribe | DistinctUntilChanged を指定できるようなデザインを選んでいるのですが(というのも、Viewにデータバインディングするため構築時の初期値はnullであることが確定している、というシチュエーションが割とあるため)、UniRxのReactivePropertyではSubscribe側が選ぶというデザインにしています。この辺はフレームワークの性質の違いに合わせてるのですが、ともあれ、初期値を無視したい場合は rxProp.SkipLatestValueOnSubscribe().Subscribe() としてもらえれば。

Observable.WhenAllを、IObservable[Unit][]が相手の場合はIObservable[Unit]を返すようにしました。これは、別にUnit[]が返されても何の意味もないからというのと、それによって余計な配列確保をしないという最適化も入れています。この方が絶対に良いんですが、しかし戻り値の型が変わってしまったので破壊的変更にはなっています。最初から気づいておけば良かったですね、すびばせん。

AsSingleUnitObservableは LastOrDefault().AsUnitObservable() みたいな変換をかけるやつで、Async的な保証をかけるのにベンリというあれそれ。

あとは、んー、使ってる人は、うちの社内以外にないのでは疑惑も感じてますが、UniRxAnalyzerを更新してます。コンストラクタにIObservableを突っ込んでいた場合に誤検出していたのを修正しています。

これ、Visual Studio 2015を使って開発している人は絶対に入れたほうがいいですよ!Subscribe忘れて発火しないのに気づかなかったー、みたいなポカミスが圧倒的に防げますので。

まとめ

性能面でより気にせずにカジュアルに色々使えるようになった、というのはいいことかなー。性能面で問題出た際に「そういう使いかた想定してないから」といった却下の仕方って、あんましたくないですからね。聞いてていいものでは全くない。デザインとしてカジュアルに使えるようになっているなら、性能もちゃんと担保していかないし、そういうのが頻発するならライブラリの設計が悪い。と、思ってるので、今回のでよりちゃんと自然に使えるようになったかな、と。ObserveEveryValueChangedは個人的には最高にクールな機能だと思ってるので、気兼ねなく使って欲しいし、やっと本当に気兼ねなく使えるようになりました。

ObservableUpdateTrigger(UpdateAsObservable), Observable.EveryUpdate, Observable.EveryGameObjectUpdate とUpdateのハンドリングも3択、性能特性も三者三様。混乱との対話!別に特に何をレコメンドすることもなく、まあ素直に書くならUpdateTriggerが素直でよく。自身のUpdateループで周りますしね。EveryUpdateはMicroCoroutineなので性能特性的には良さげ、どうせAddTo(this)するならループのライフサイクルもUpdateTriggerと別に変わりはしないし(UpdateTriggerだとDisableでUpdateが回らなくなるので、まぁその辺で挙動に違いは出る)。EveryGameObjectUpdateはMainThreadDispatcherのSubjectに積まれるもので、UpdateTriggerが使える状況なら非推奨かな、あんまりSubjectに頻繁にAdd, Removeするのは性能特性的に悪手なので。UpdateTriggerもSubjectが駆動するのですが、性質的にグローバルじゃないのでAdd, Removeは局所化されるからそこまででは、に通常はなるでしょう、的な。

そんなこんなで、少なくともRxが性能面のネックでー、と言われるのは悔しい話なので、大きいものから小さいものまで、最適化ネタは常に考えてます。利用事例としても、結構ヒットしてる某社の某ゲーム(とは)や最近でた前作に続いてヒットの予感のする某ゲーム(とは)など、かなり使いこなしてる事例もあって(個人的にはとても感動した!)、ちゃんと実用的といってもいいレベルになってると思われます。弊社の開発中タイトルである黒騎士と白の魔王でもガッツリ使っているので、ご興味ある方は中途採用は絶賛募集中です:) 当たり前ですがドッグフーディングは凄く大事で、さすがにデカいバグは出てこないにしても軽微なものはちょいちょい上がってくるので、日々、堅牢さは担保されているな、とかかんとか。あと、使いすぎてるほどに使いすぎてるので、常に性能面でネックになってはいけない、性能面でネックになってはいけない、とマントラを唱えるプレッシャーになってるのもいいことです、多分きっと。

今回のアップデートでツメが甘かった案件としてはAsyncOperation.AsObservableやObservableWWWが内部的にまだFromCoroutine利用なので、FromMicroCoroutineに可能なら差し替えようかな、と。効果のほどとしては、やっぱり場合によりけりですが、初期化とかで大量に回る時は大きく変わるかも。しれない。ともあれ次回アップデートにご期待を。ただyield return wwwやasyncOperationした場合とyield return nullでisDoneチェックする場合とで、戻ってくるタイミングが異なるので、そこのルールを統一させないとかなあ。

SerializableDictionary - Unityで高速に辞書復元するためのライブラリ

という、ScriptableObjectとかJsonUtilityとか、そもそもSerializeFieldとかでシリアライズできるDictionaryを作りました。

もともとDictionaryはシリアライズできないのですが、ISerializationCallbackReceiverを用いてシリアライズ/デシリアライズのタイミングでKeyの配列、Valueの配列に戻してやるなどで保存すること自体は全然可能でした。のですが、速度的には問題あるな、というのに直面しました。

その前に、JSONから復元するのがまず遅かった。じゃあMsgPackやProtobufに変更したら速いかといえば、別にそこまでそうではなかった。これはつまり、C#のレイヤーで大量の何かを舐めて何かを作るという行為そのものが遅い。ではScriptableObject化すればどうだろう、確かにデシリアライズのプロセスがUnityネイティブ(実体は不明)と化して、確かに速い。が、そこからDictionaryに変換してやるのをC#で書いたらやっぱりそこが遅い。

遅い、というとアレで、量次第ですけどね。今回、量がやたら多かったので結構かなり相当引っ張られてた。初期化のタイミングなどで大量のDictionaryを捌くような場合に、無視できない程度に結構引っかかる遅さを醸し出してる。結局、配列からであっても、C#のレイヤーで大量の何かを舐めて何かを作るという行為そのものが遅い。という悲しい現実をつきつけられるのであった。

というわけで、SerializableDictionaryはDictionaryの内部構造をシリアライズすることで、ネイティブプロセスのみで完結して爆速で復元します。

SerializableDictionaryではSerializableDictionary, SerializableLookup(MultiDictinary), SerializableTupleの3つを提供します。今のとこアセットストアに公開するつもりはそんなないので、使いたい場合はソースコードをZipでダウンロードするなりGitで落とすなりしてプロジェクトに投げ込んでください。

SerializableDictionary

例えばキーがint、値がstringの辞書を保存したい場合は、まず、継承したクラスを作ります。

[Serializable]
public class IntStringSerializableDictionary : SerializableDictionary<int, string>
{
 
}

わざわざ継承しなきゃいけない理由は、ジェネリックな型はシリアライズできないからです!しょうがないね。別にゆうてそんなに大量の型があるわけでもないでしょうし、素直にそれぐらいは作りましょう。あとは、普通に使えば普通にシリアライズ可能になってます。メデタシメデタシ。

インスペクタに表示するためのPropertyDrawerも用意してあります(こちらも定義しないとインスペクタに何も表示されなくて不安になる)。使う場合は、SerializableDictionaryPropertyDrawerを継承した型を一つ作って、そこに属性でひたすらぶら下げます。

#if UNITY_EDITOR
 
[UnityEditor.CustomPropertyDrawer(typeof(IntStringSerializableDictionary))]
[UnityEditor.CustomPropertyDrawer(typeof(IntDoubleSerializableDictionary))]
[UnityEditor.CustomPropertyDrawer(typeof(IntIntStringSerializableDictionary))]
public class ExtendedSerializableDictionaryPropertyDrawer : SerializableDictionaryPropertyDrawer
{
 
}
 
#endif

これを定義すれば、インスペクタ上では

image

なんとKeyとValueの確認しか用意されてなくて、エディット不能!ただのDump!うーん、気が向いたらエディット可能にします。そのうち(多分やらない)。

複数キーの辞書

Int + Intの組み合わせでキーにしたいとか、辞書にはよくあるケースです。そして、そういったよくあるケースではKeyにTupleを使うことが多いです。が、UnityにはTupleはありません。UniRxにTupleがあります、が、それはシリアライズ可能ではありません(Genericだからねー、structなので継承もできない)。と、いうわけで、辞書のキーにしたいよね専用にSerializableTupleを用意しておきました。使う場合はもちろんまずは継承してジェネリックを消すとこからはじめます。

[Serializable]
public class IntIntTuple : SerializableTuple<int, int>
{
    public IntIntTuple()
    {
 
    }
 
    public IntIntTuple(int item1, int item2)
        : base(item1, item2)
    {
 
    }
}
 
[Serializable]
public class IntIntStringSerializableDictionary : SerializableDictionary<IntIntTuple, string>
{
 
}

あとは普通にキーに使ってもらえれば、普通に使えます。ちょっと手間ですが、そこまで多いわけでもないでしょうし我慢できる範囲内。だといいかな。

SerializableLookup

ILookupは、Keyに対してValue側が複数になっている辞書です。Dictionary[Key, Value[]] みたいなイメージ。通常のILookupはLINQのToLookup経由でしか作成できない、Readonlyなシロモノです。これ、非常に便利な型でして、よく使います。ToLookupしらない人は覚えましょう。ただ、勿論シリアライズできないのでAddを加えたSerializableLookupを用意しました(Removeはありません!つまりBuilderのほうがイメージ近いかもしれません。Removeがない理由は実装しててバグッたからとりあえず消してるだけなのでそのうち入れるかもしれないかもしれない)

使い方はDictionaryと同様。

[Serializable]
public class IntIntSerializableLookup : SerializableLookup<int, int>
{
}
 
#if UNITY_EDITOR
 
[UnityEditor.CustomPropertyDrawer(typeof(IntIntSerializableLookup))]
public class ExtendedSerializableLookupPropertyDrawer : SerializableLookupPropertyDrawer
{
 
}
 
#endif

ちなみに中身は面倒くさいんでSerializableDictionaryの一部を改変して辻褄合わせてるだけなので、実効速度的な意味ではToLookupで生成したものに比べるとやや劣るかなー、といったところ。まぁハッシュキーの衝突具合とかにもよるので、いうほどそこまでではないと思います。実装の雑さは気にしてるのでそのうち直したい(絶対やらない)

TrimExcess

ListにせよDictionaryにせよ、任意個数をAdd可能なものは、内部である程度余分なバッファを持っています。しかし、ScriptableObjectなどにしてAssetBundleに載せたい場合は、その後の追加なども特になく個数は固定である可能性も少なくないはずです。と、いうわけで、TrimExcessメソッドを呼ぶことで余分なバッファを切り落とすことができます。もし個数が固定であることが見えているなら、事前にSerialize前に呼んであげておくことで、メモリ節約につながります。

Unityでシリアライズ可能なもの

内部構造の話なのですが、その前にUnityでシリアライズ可能なものの制限についておさらい。

  • [Serializable]のついた非ジェネリックな具象型
  • UnityEngine.Objectを継承した型
  • public、または[SerializeField]のついたインスタンスフィールド
  • int, float, double, bool, stringなどのプリミティブなデータ型
  • 配列、もしくはList[T]

Dictionaryに非対応なのは勿論ですが、Nullable[T]に非対応が割と痛かったりするかな!また、トップレベル以外でnullをサポートしていなかったりして、ちょっと複雑な型を作った場合、nullを入れたと思ったら全部0が入った謎データに置き換わっていた、とかが生じます。それらの制限の回避策としては、SerializableDictionaryと同じようにSerializableNullableのようなそれっぽい似非な型を自前で作ってあげればなんとかなります。nullのほうも同様にNullableClass(なんじゃそりゃ)を作ってあげることにより、nullかそうでないかの区別を可能にできます。面倒くさくはあるんですが、どうしても必要な場合はそうして回避できなくもないよ、ということで。

SerializableDictionaryは、Dictionaryの内部構造を、Unityでシリアライズ可能な範囲(ようするにひたすら単純な配列まみれにする)に修正することで実現しています。オリジナルのDictionary自体はdotonet/corefxのものです。

ハッシュコードを永続化することの安全性

ハッシュコードを永続化することは、推奨されないことが明言されています。というのも、そのオブジェクトに対するハッシュコードが一意であるかが、どのスコープまで保たれるかというのは、全くもって不明瞭だからです。参照型などはアプリケーション起動毎に異なってなにの役にも立たなくなる、では数字は?文字列は?保証はないんですねー。

実用的な意味では、問題ないと判断しても構わないと思っています。しかし、まず、monoと.NET Frameworkのような環境が違うもので生成したもの同士の互換性はないと考えたほうがいいでしょう(実際ない)。また、.NET Framework内だけでも、バージョンが異なれば、違うハッシュアルゴリズムが使われることにより異なるハッシュコードが使われる可能性は全然あります。今後も、Unityのバージョンアップ+(もしあるのなら)monoのバージョンアップが発生した際などは、互換性が崩れる可能性があります。最悪そうなった場合は、任意のComparerを挟み込めるようになっているので、そこで互換性を保ったハッシュコードを返してやることにより、一応大丈夫とはいえます。一応。

とはいえまあ、実用的な意味では大丈夫でしょう。タブンネ。まぁしかし、この辺グレーゾーンなきらいもあるから、Unity公式でサポートってのは難しいんじゃないかなあ、というのはしょうがないかなー。

まとめ

Unityって結局どこで動いているものなのよ、ってのを改めて突きつけられた感じがしました。C++のエンジンがあくまでも主だな、と。また、シリアライズを通して考えると、一見不思議なMonoBehaviourやpublic fieldなども納得がいくように見えてきて、ようするにネイティブとの境界線を接続している場所なんですね。COMとdynamicでやり取りするように、ネイティブレイヤーとフィールドでやり取りする。そう思えば、何もかも腑に落ちてきた気がします(悟り!)。

C#のレイヤーでいかに仕事をさせないかがキモで、そのためにC#を書く。ってのも、まぁ悪くない話だし、Unityアプリケーションとしての整合感やパフォーマンスが最も求めるべきことなのだ。というのは認識しておきたいな、なんて改めて思わさせられました。

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 アセットコンテストというのに応募していたのですが結果発表……。

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

同期(風)コードと対比させた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でイベントを扱う際のパターン集は、またそのうちにでも!

Unityのコルーチンの分解、或いはUniRxのMainThreadDispatcherについて

この記事はUnity Advent Calendar 2014のための記事になります。昨日はkomiyakさんのUnity を使いはじめたばかりの頃の自分に伝えたい、Unity の基本 【2014年版】でした。いやー、これはまとまってて嬉しい情報です。ところでカレンダー的には穴開けちゃってます(遅刻遅延!)、すみません……。

さて、今回の内容ですが、私の作っているUniRxというReactive Programming(バズワード of 2014!)のためのライブラリを、最近ありがたいことに結構使ってみたーという声を聞くので、Rxの世界とUnityの世界を繋ぐ根幹である、MainThreadDispatcherと、その前準備に必要なコルーチンについて書きます。

Coroutine Revisited

コルーチンとはなんぞや。なんて今更ですって!はい。とりあえず、Unityは基本的にシングルスレッドで動いています。少なくともスクリプト部分に関しては。Unityのコルーチンは、IEnumeratorでyield returnすると、その次の処理を次フレーム(もしくは一定秒数/完了後などなど)に回します。あくまでシングルスレッド、ということですね。挙動について。簡単な確認用スクリプトを貼っつけて見てみると……

void Start()
{
    Debug.Log("begin-start:" + Time.frameCount);
    StartCoroutine(MyCoroutine());
    Debug.Log("end-start" + Time.frameCount);
}
 
IEnumerator MyCoroutine()
{
    Debug.Log("start-coroutine:" + Time.frameCount);
 
    yield return null;
    Debug.Log("after-yield-null:" + Time.frameCount);
 
    yield return new WaitForSeconds(3);
    Debug.Log("end-coroutine:" + Time.frameCount);
}

呼ばれる順番とframeCountを考えてみようクイズ!意外と引っかかるかもしれません。答えのほうですが……

begin-start:1
start-coroutine:1
end-start:1
after-yield-null:2
end-coroutine:168

となります。最後の秒数のフレームカウントはどうでもいいとして、start-coroutineが呼ばれるのはend-startの前ってのがちょっとだけヘーってとこかしら。IEnumerator自体はUnity固有の機能でもなく、むしろC#の標準機能で、通常は戻り値を持ってイテレータを生成するのに使います(Pythonでいうところのジェネレータ)

// 偶数のシーケンスを生成
IEnumerable<int> EvenSequence(int from, int to)
{
    for (int i = from; i <= to; i++)
    {
        if (i % 2 == 0)
        {
            yield return i;
        }
    }
}
 
void Run()
{
    var seq = EvenSequence(1, 10);
 
    // シーケンスはforeachで消費可能
    foreach (var item in seq)
    {
        Debug.Log(item);
    }
 
    // あるいはEnumeratorを取得し回す(foreachは↓のコードを生成する)
    // Unityでのコルーチンでの利用され方はこっちのイメージのほうが近い
    using (var e = seq.GetEnumerator())
    {
        while (e.MoveNext())
        {
            Debug.Log(e.Current);
        }
    }
}

Unityのコルーチンとしてのイテレータの活用法は、戻り値を原則使わず(宣言がIEnumerator)、yield returnとyield returnの間に副作用を起こすために使うということですね。これはこれで中々ナイスアイディアだとは思ってます。

言語システムとしてはC#そのままなので、誰かがIEnumeratorを消費しているということになります。もちろん、それはStartCoroutineで、呼んだ瞬間にまずはMoveNext、その後はUpdateに相当するようなタイミングで毎フレームMoveNextを呼び続けているようなイメージ。

擬似的にMonoBehaviourで再現すると

public class CoroutineConsumer : MonoBehaviour
{
    public IEnumerator TargetCoroutine; // 何か外からセットしといて
 
    void Update()
    {
        if (TargetCoroutine.MoveNext())
        {
            var current = TargetCoroutine.Current;
            // 基本的にCurrent自体はそんな意味を持たないで次フレームに回すだけ
            if (current == null)
            {
                // next frame
            }
            // ただしもし固有の何かが返された時はちょっとした別の挙動する
            if (current is WaitForSeconds)
            {
                // なんか適当に秒数待つ(ThreadをSleepするんじゃなく挙動的には次フレームへ)
            }
            else if (current is WWW)
            {
                // isDoneになってるまで適当に待つ(ThreadをSleepするんじゃなく挙動的には次フレームへ)
            }
            // 以下略
        }
    }
}

こんな感じでしょうか!yield returnで返す値が具体的にUnityのゲームループにおいてどこに差し込まれるかは、UnityのマニュアルのScript Lifecycle Flowchartの図を見るのが分かりやすい。

nullが先頭でWaitForEndOfFrameは末尾なのね、とか。yield returnで返して意味を持つ値はYieldInstruction、ということになっているはずではあるんですが、実際のとこWWWはYieldInstructionじゃないし、YieldInstruction自体はカスタマイズ不能で自分で書けるわけじゃないんで(イマイチすぎる……)なんだかなぁー。Lifecycle Flowchartに書かれていない中でyield可能なのはAsyncOperationかな?

もしイテレータの挙動について更に詳しく知りたい人は、私の以前書いたスライドAn Internal of LINQ to Objectsの14Pを参照してくださいな。

UniRx.FromCoroutine

というわけかで(一旦)コルーチンの話はおしまい。ここからはUniRxの話。UniRxについてはneue cc - A Beginners Guide to Reactive Extensions with UniRxあたりをどうぞ。UniRxはFromCoroutineメソッドにより、コルーチンをUniRxの基盤インターフェースであるIObservable<T>に変換します。

// こんなのがあるとして
IEnumerator CoroutineA()
{
    Debug.Log("a start");
    yield return new WaitForSeconds(1);
    Debug.Log("a end");
}
 
// こんなふうに使える
Observable.FromCoroutine(CoroutineA)
    .Subscribe(_ => Debug.Log("complete"));
 
// 戻り値のあるバージョンがあるとして
IEnumerator CoroutineB(IObserver<int> observer)
{
    observer.OnNext(100);
    yield return new WaitForSeconds(2);
    observer.OnNext(200);
    observer.OnCompleted();
}
 
// こんなふうに合成もできる
var coroutineA = Observable.FromCoroutine(CoroutineA);
var coroutineB = Observable.FromCoroutine<int>(observer => CoroutineB(observer));
 
// Aが終わった後にBの起動、Subscribeには100, 200が送られてくる
var subscription = coroutineA.SelectMany(coroutineB).Subscribe(x => Debug.Log(x));
 
// Subscribeの戻り値からDisposeを呼ぶとキャンセル可能
// subscription.Dispose();

IObservable<T>になっていると何がいいかというと、合成可能になるところです。Aが終わった後にBを実行する、Bが失敗したらCを実行する、などなど。また、戻り値を返すことができるようになります。そして、コルーチンに限らず、あらゆるイベント、あらゆる非同期がIObservable<T>になるので、全てをシームレスに繋ぎ合わせることができる。そこが他のライブラリや手法と一線を画すRxの強みなんです、が、長くなるのでここでは触れません:)

また、MonoBehaviour.StartCoroutineを呼ばなくてもコルーチンが起動しています。これは結構大きな利点だと思っていて、というのも、コルーチンを使うためだけにMonoBehaviourにする必要がなくなる。やはり普通のC#クラスのほうが取り回しが良いので、MonoBehaviourにする必要がないものはしないほうがいい。けれど、コルーチンは使いたい。そうした欲求に応えてくれます。

更にFromCoroutine経由にするとEditor内部では通常は動かせないコルーチンを動かすことができます!(これについては後で説明します)

といった応用例はそのうちやるということで、とりあえずFromCoroutineの中身を見て行きましょう。

// Func<IEnumerator>はメソッド宣言的には「IEnumerator Hoge()」になる
public static IObservable<Unit> FromCoroutine(Func<IEnumerator> coroutine, bool publishEveryYield = false)
{
    return FromCoroutine<Unit>((observer, cancellationToken) => WrapEnumerator(coroutine(), observer, cancellationToken, publishEveryYield));
}
 
// ↑のはWrapEnumeratorを介してこれになっている
public static IObservable<T> FromCoroutine<T>(Func<IObserver<T>, CancellationToken, IEnumerator> coroutine)
{
    return Observable.Create<T>(observer =>
    {
        var cancel = new BooleanDisposable();
 
        MainThreadDispatcher.SendStartCoroutine(coroutine(observer, new CancellationToken(cancel)));
 
        return cancel;
    });
}
 
// WrapEnumeratorの中身は(オェェェェ
static IEnumerator WrapEnumerator(IEnumerator enumerator, IObserver<Unit> observer, CancellationToken cancellationToken, bool publishEveryYield)
{
    var hasNext = default(bool);
    var raisedError = false;
    do
    {
        try
        {
            hasNext = enumerator.MoveNext();
        }
        catch (Exception ex)
        {
            try
            {
                raisedError = true;
                observer.OnError(ex);
            }
            finally
            {
                var d = enumerator as IDisposable;
                if (d != null)
                {
                    d.Dispose();
                }
            }
            yield break;
        }
        if (hasNext && publishEveryYield)
        {
            try
            {
                observer.OnNext(Unit.Default);
            }
            catch
            {
                var d = enumerator as IDisposable;
                if (d != null)
                {
                    d.Dispose();
                }
                throw;
            }
        }
        if (hasNext)
        {
            yield return enumerator.Current; // yield inner YieldInstruction
        }
    } while (hasNext && !cancellationToken.IsCancellationRequested);
 
    try
    {
        if (!raisedError && !cancellationToken.IsCancellationRequested)
        {
            observer.OnNext(Unit.Default); // last one
            observer.OnCompleted();
        }
    }
    finally
    {
        var d = enumerator as IDisposable;
        if (d != null)
        {
            d.Dispose();
        }
    }
}

WrapEnumeratorの中身が長くてオェェェって感じなんですが何やってるかというと、元のコルーチンを分解して、Rx的に都合のいい形に再構築したコルーチンに変換してます。都合のいい形とは「キャンセル可能」「終了時(もしくは各yield時)にObserver.OnNextを呼ぶ」「全ての完了時にObserver.OnCompletedを呼ぶ」「エラー発生時にObserver.OnErrorを呼ぶ」を満たしているもの。コルーチン自体がC#の標準機能のままで、なにも特別なことをしていないなら、別に自分で回す(enumerator.MoveNextを手で呼ぶ)ことも、何も問題はない、わけです。

そんなラップしたコルーチンを動かしているのがMainThreadDispatcher.SendStartCoroutine。今のMainThreadDispatcher.csは諸事情あって奇々怪々なんですが、SendStartCoroutineのとこだけ取り出すと

public sealed class MainThreadDispatcher : MonoBehaviour
{
    // 中略
 
    /// <summary>ThreadSafe StartCoroutine.</summary>
    public static void SendStartCoroutine(IEnumerator routine)
    {
#if UNITY_EDITOR
        if (!Application.isPlaying) { EditorThreadDispatcher.Instance.PseudoStartCoroutine(routine); return; }
#endif
 
        if (mainThreadToken != null)
        {
            StartCoroutine(routine);
        }
        else
        {
            Instance.queueWorker.Enqueue(() => Instance.StartCoroutine_Auto(routine));
        }
    }
 
    new public static Coroutine StartCoroutine(IEnumerator routine)
    {
#if UNITY_EDITOR
        if (!Application.isPlaying) { EditorThreadDispatcher.Instance.PseudoStartCoroutine(routine); return null; }
#endif
 
        return Instance.StartCoroutine_Auto(routine);
    }
}

if UNITY_EDITORのところは後で説明するのでスルーしてもらうとして、基本的にはInstance.StartCoroutine_Autoです。ようはMainThreadDispatcherとは、シングルトンのMonoBehaviourであり、FromCoroutineはそいつからコルーチンを起動しているだけなのであった。なんだー、単純。汚れ仕事(コルーチンの起動、MonoBehaviourであること)をMainThreadDispatcherにだけ押し付けることにより、それ以外の部分が平和に浄化される!

コルーチンの起動が一極集中して、それで実行効率とか大丈夫なの?というと存外大丈夫っぽいので大丈夫。実際、私の会社ではこないだ一本iOS向けにゲームをリリースしましたがちゃんと動いてます。しかしそうなるとStartCoroutineはMonoBehaviourのインスタンスメソッドではなく、静的メソッドであって欲しかった……。

その他、SendStartCoroutineはスレッドセーフ(他スレッドから呼ばれた場合はキューに突っ込んでメインスレッドに戻ってから起動する)なのと、UnityEditorからの起動を可能にしています(EditorThreadDispatcher.Instance.PseudoStartCoroutine経由で起動する)。なので、普通にStartCoroutineを呼ぶ以上のメリットを提供できているかな、と。

UnityEditorでコルーチンを実行する

Editorでコルーチンを動かせないのは存外不便です。WWWも動かせないし……。UniRxではFromCoroutine経由で実行すると、内部でMainThreadDispatcher.SendStartCoroutine経由になることにより、Editorで実行できます。使い方は本当にFromCoroutineしてSubscribeするだけ、と、通常時のフローとまるっきり一緒です。ここで毎回エディターの時は、通常の時は、と書き分けるのはカッタルイですからね。汚れ仕事はMainThreadDispatcherが一手に引き受けています。そんな汚れ仕事はこんな感じの実装です。

class EditorThreadDispatcher
{
    // 中略
 
    ThreadSafeQueueWorker editorQueueWorker= new ThreadSafeQueueWorker();
 
    EditorThreadDispatcher()
    {
        UnityEditor.EditorApplication.update += Update;
    }
 
    // 中略
 
    void Update()
    {
        editorQueueWorker.ExecuteAll(x => Debug.LogException(x));
    }
 
    // 中略
 
    public void PseudoStartCoroutine(IEnumerator routine)
    {
        editorQueueWorker.Enqueue(() => ConsumeEnumerator(routine));
    }
 
    void ConsumeEnumerator(IEnumerator routine)
    {
        if (routine.MoveNext())
        {
            var current = routine.Current;
            if (current == null)
            {
                goto ENQUEUE;
            }
 
            var type = current.GetType();
            if (type == typeof(WWW))
            {
                var www = (WWW)current;
                editorQueueWorker.Enqueue(() => ConsumeEnumerator(UnwrapWaitWWW(www, routine)));
                return;
            }
            else if (type == typeof(WaitForSeconds))
            {
                var waitForSeconds = (WaitForSeconds)current;
                var accessor = typeof(WaitForSeconds).GetField("m_Seconds", BindingFlags.Instance | BindingFlags.GetField | BindingFlags.NonPublic);
                var second = (float)accessor.GetValue(waitForSeconds);
                editorQueueWorker.Enqueue(() => ConsumeEnumerator(UnwrapWaitForSeconds(second, routine)));
                return;
            }
            else if (type == typeof(Coroutine))
            {
                Debug.Log("Can't wait coroutine on UnityEditor");
                goto ENQUEUE;
            }
 
        ENQUEUE:
            editorQueueWorker.Enqueue(() => ConsumeEnumerator(routine)); // next update
        }
    }
 
    IEnumerator UnwrapWaitWWW(WWW www, IEnumerator continuation)
    {
        while (!www.isDone)
        {
            yield return null;
        }
        ConsumeEnumerator(continuation);
    }
 
    IEnumerator UnwrapWaitForSeconds(float second, IEnumerator continuation)
    {
        var startTime = DateTimeOffset.UtcNow;
        while (true)
        {
            yield return null;
 
            var elapsed = (DateTimeOffset.UtcNow - startTime).TotalSeconds;
            if (elapsed >= second)
            {
                break;
            }
        };
        ConsumeEnumerator(continuation);
    }
}

ようは、UnityEditor.EditorApplication.updateでジョブキューを回しています。コルーチン(Enumerator)を手動で分解して、EditorApplication.updateに都合の良い形に再編しています。yield return nullがあったらキューに突っ込んで次のupdateに回すことで、擬似的にStartCorotineを再現。WaitForSecondsだったらリフレクションで内部の秒数を取ってきて(ひどぅい)ぐるぐるループを展開。などなど。

仕組み的には単純、なんですが結構効果的で便利かな、と。ユーザーは全くそれを意識する必要がないというのが一番いいトコですね。

ちなみにアセットストアからダウンロードできるバージョンでは、まだこの仕組みは入ってません(すびばせん!)。GitHubの最新コードか、あとは、ええと、近いうちにアップデート申請しますので来年には使えるようになっているはずです。。。

まとめ

コルーチンをコルーチンたらしめているのは消費者であるStartCoroutineであって、IEnumerator自体はただのイテレータにすぎない。なので、分解も可能だし、他の形式に展開することもできる。

UniRx経由でコルーチンを実行すると「色々なものと合成できる」「(複数の)戻り値を扱える」「キャンセルが容易」「MonoBehaviourが不要」「スレッドセーフ」「エディターでも実行可能」になる。いいことづくめっぽい!Reactive Programmingの力!そんな感じに、UniRxはなるべくシームレスにRxの世界とUnityの世界を繋げるような仕組みを用意しています。是非ダウンロードして、色々遊んでみてください。

VS2015のRoslynでCode Analyzerを自作する(ついでにUnityコードも解析する)

Visual Studio 2015 Previewが発表されました!この中にはC# 6.0やRoslynも含まれていて、今から試すことができます。C#の言語機能は他の人が適当にまとめてくれるので私はノータッチということで、新機能であるRoslynで拡張を作っていきましょう。

Roslynによる拡張は、ン年前に最初のPreviewが出た時は、Visual Studioの解析エンジン自体がRoslynになるから簡単にアレもコレも出来るぜ!と夢いっぱいのこと言ってましたが、実のところ最終的に現在(VS2015 Preview)ではかなり萎んでしまいました。「Code Refactoring」と「Diagnostic with Code Fix」だけです。何ができるかは、まぁ名前から察しということで、あんま大したことはできないです。がっくし。とはいえ、しかし全然使いドコロはあるし簡単に作れはするので、とにかく見て行きましょう。

下準備としてVS2015 Previewのインストールの他に、Visual Studio 2015 Preview SDK.NET Compiler Platform SDK Templates、そして.NET Compiler Platform Syntax Visualizerを入れてください。

Diagnostic with Code Fix

今回は「Diagnostic with Code Fix」を作ります。まずテンプレートのVisual C#→Extensibilityから「Diagnostic with Code Fix(NuGet + VSIX)」を選んでください。NuGet + VSIXというのが面白いところなんですが、とりあえずこのテンプレート(はサンプルになってます)をビルドしましょう(Testプロジェクトは無視していいです)。そして、ReferencesのAnalyzers(ここがVS2015から追加されたものです!)からAdd Analyzerを選び、さっきビルドしたdllを追加してみてください。

するとコード解析が追加されて、クラス名のところにQuick Fixが光るようになります。

サンプルコードのものはMakeUpperCaseということで、クラス名に小文字が含まれていたら警告を出す&全部大文字に修正するQuickFixが有効になります。

つまりDiagnostic with Code Fixは、よーするに今までもあったCode Analysis、FxCopです。ただし、Roslynによって自由に解析でき、追加できます。また、ReferencesのAnalyzersに追加できるということで、ユーザーのVisual Studio依存ではなく、プロジェクト内に直接含めることができます。追加/インストールはdllをNuGetで配ることが可能(だからVSIX + NuGetなんですね、もちろんVSIXでも配れます)。より気軽に、よりパワフルにコード解析が作れるようになったということで、地味に中々革命的に便利なのではないでしょうか?

このまま、そのサンプルコードのMakeUpperCaseの解説、をしてもつまらないので、続けて実用的(?)なものを一個作りました。

namespaceの修正

うちの会社ではUnityを使ってモバイルゲーム開発を行っていますが、LINQもガリガリ使います。その辺のことはLINQ to GameObjectによるUnityでのLINQの活用にも書いたのですが、困ったことに標準UnityではLINQ to Objectsを使うとAOTで死にます。Unity + iOSのAOTでの例外の発生パターンと対処法で書いたように対処事態は可能なんですが、最終的に標準LINQを置き換える独自実装をSystem.LinqExネームスペースに用意することになりました。で、それを使うには「using System.LinqEx;」する必要があります。「using System.Linq;」のかわりに。むしろ「using System.Linq;」はAOTで死ぬので禁止したいし、全面的に「using System.LinqEx;」して欲しい。すみやかに。どうやって……?

そこでDiagnostic with Code Fixなんですね。既存コードの全てに検査をかけることもできるし(ソリューションエクスプローラーから対象プロジェクトを右クリックしてAnalyze→Run Code Analysis)、書いてる側からリアルタイムに警告も出せるし、ワンポチでSystem.LinqExに置き換えてくれる。このぐらいなら全ファイルから「using System.Linq;」を置換すりゃあいいだけなんですが、リアルタイムに警告してくれるとうっかり忘れもなくなるし(CIで警告すればいいといえばいいけど、その前に自分で気づいて欲しいよね)、もっと複雑な要件でも、RoslynでSyntaxTreeを弄って置き換えるので、テキスト置換のような誤爆の可能性があったり、そもそも複雑で警告/置換不能、みたいなことがなくなるので、とても有益です。

というわけで「using System.Linq;」を見つけたら「using System.LinqEx;」に書き換える拡張を作りましょう!(うちの会社にとっては)実用的で有益で、かつ、はぢめての拡張のテーマとしてもシンプルで作りやすそうでちょうどいいですね!

DiagnosticAnalyzer

コード解析はDiagnosticAnalyzer、コード置換はCodeFixProviderが担当します。必要なファイルはこの2ファイルだけ(シンプル!)、コード置換が不要ならDiagnosticAnalyzerだけ用意すればOK。というわけで、以下がDiagnosticAnalyzerのコードです。

using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.CSharp;
using Microsoft.CodeAnalysis.CSharp.Syntax;
using Microsoft.CodeAnalysis.Diagnostics;
using System.Collections.Immutable;
 
namespace UseLinqEx
{
    [DiagnosticAnalyzer(LanguageNames.CSharp)]
    public class UseLinqExAnalyzer : DiagnosticAnalyzer
    {
        // この辺はテンプレートのままに適当に書き換え
        public const string DiagnosticId = "UseLinqEx";
        internal const string Title = "System.Linq is unsafe in Unity. Must use System.LinqEx.";
        internal const string MessageFormat = "System.Linq is unsafe in Unity. Must use System.LinqEx."; // 同じの書いてる(テキトウ)
        internal const string Category = "Usage"; // Categoryの適切なのってナンダロウ
 
        internal static DiagnosticDescriptor Rule = new DiagnosticDescriptor(DiagnosticId, Title, MessageFormat, Category, DiagnosticSeverity.Warning, isEnabledByDefault: true);
 
        public override ImmutableArray<DiagnosticDescriptor> SupportedDiagnostics { get { return ImmutableArray.Create(Rule); } }
 
        // namespaceを引っ掛ける
        public override void Initialize(AnalysisContext context)
        {
            // なにをRegisterすればいいのか問題、テンプレではRegisterSymbolActionですが、
            // SymbolActionにはなさそうだなー、と思ったら他のRegisterHogeを使いましょう
            // ここではRegisterSyntaxNodeActionでSyntaxKind.UsingDirectiveを呼びます
            // SyntaxKindの判定はRoslyn Syntax Visualizerに助けてもらいましょう
            context.RegisterSyntaxNodeAction(Analyze, SyntaxKind.UsingDirective);
        }
 
        static void Analyze(SyntaxNodeAnalysisContext context)
        {
            // Nodeの中身はSyntaxKindで何を選んだかで変わるので適宜キャスト
            var syntax = (UsingDirectiveSyntax)context.Node;
            if (syntax.Name.NormalizeWhitespace().ToFullString() == "System.Linq")
            {
                var diagnostic = Diagnostic.Create(Rule, syntax.GetLocation());
                context.ReportDiagnostic(diagnostic);
            }
        }
    }
}

SupportedDiagnosticsより上のものは見た通りのコンフィグなので、まぁ見たとおりに適当に弄っておけばいいでしょう。コード本体はInitializeです。ここで、対象のノードの変更があったら起こすアクションを登録します。で、まずいきなり難しいのは、何をRegisterすればいいのか!ということだったりして。そこで手助けになるのがSyntax Visualizerです。入れましたか?入れましたよね?View -> Other Window -> Roslyn Syntax Visualizerを開くと、あとはエディタ上で選択している箇所のSyntaxTreeを表示してくれます。例えば、今回の対象であるusingの部分を選択すると「using System.Linq;」は……

と、いうわけで、たかがusingの一行ですが、めっちゃいっぱい入ってます。Node(でっかいの), Token(こまかいの), Trivia(どうでもいいの)というぐらいに覚えておけばいいでしょう(適当)。さて、というわけでusingの部分はUsingDirectiveであることが大判明しました。これ以外にもとにかくSyntaxTreeの操作は、何がどこに入ってて何を置換すればいいのかを見極める作業が必要なので、Syntax Visualizerはマストです。めっちゃ大事。めっちゃ助かる。超絶神ツール。

あとは、まぁ、見たまんまな感じで、これで警告は出してくれます。WarningじゃなくてErrorにしたいとか、Infoにしたいとかって場合はRuleからDiagnosticSeverityを変えればOK。

CodeFixProvider

続いてCodeFixProviderに行きましょう。まずはコード全体像を。

using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.CodeActions;
using Microsoft.CodeAnalysis.CodeFixes;
using Microsoft.CodeAnalysis.CSharp;
using Microsoft.CodeAnalysis.CSharp.Syntax;
using System.Collections.Immutable;
using System.Composition;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
 
namespace UseLinqEx
{
    [ExportCodeFixProvider("UseLinqExCodeFixProvider", LanguageNames.CSharp), Shared]
    public class UseLinqExCodeFixProvider : CodeFixProvider
    {
        public sealed override ImmutableArray<string> GetFixableDiagnosticIds()
        {
            // このDiagnosticIdでAnalyzerと起動するCodeFixProviderが紐付けられてる
            return ImmutableArray.Create(UseLinqExAnalyzer.DiagnosticId);
        }
 
        public sealed override FixAllProvider GetFixAllProvider()
        {
            return WellKnownFixAllProviders.BatchFixer;
        }
 
        public sealed override async Task ComputeFixesAsync(CodeFixContext context)
        {
            // ドキュメントのルート
            var root = await context.Document.GetSyntaxRootAsync(context.CancellationToken).ConfigureAwait(false);
 
            var diagnostic = context.Diagnostics.First(); // 警告だしてるとこ
            var diagnosticSpan = diagnostic.Location.SourceSpan; // の、ソース上の位置みたいなの
 
            // ↑を使って、目的のモノを見つける独自コードを書く!
            // 何が何だか分からないので、ウォッチウィンドウで手探るに書きまくって探し当てるといいでしょふ
 
            // "UsingDirectiveSyntax UsingDirective using System.Linq;" が見つかる
            var usingDirective = root.FindNode(diagnosticSpan);
 
            // で、作って登録する
            var codeAction = CodeAction.Create("ReplaceTo System.LinqEx", c => ReplaceToLinqEx(context.Document, root, usingDirective, c));
            context.RegisterFix(codeAction, diagnostic);
        }
 
        static Task<Document> ReplaceToLinqEx(Document document, SyntaxNode root, SyntaxNode usingDirective, CancellationToken cancellationToken)
        {
            // たんなるusingDirectiveでも、中にはキーワード・スペース、;や\r\nが含まれているので、
            // 純粋に新しいusingを作って置換するだけだと、付加情報がうまく置換できない可能性が高い
            // ので、(面倒くさくても)既存ノードからReplaceしていったほうが無難
            var linqSyntax = usingDirective.DescendantNodes().OfType<IdentifierNameSyntax>().First(x => x.ToFullString() == "Linq");
            var linqEx = usingDirective.ReplaceNode(linqSyntax, SyntaxFactory.IdentifierName("LinqEx"));
 
            // ルートのほうにリプレースリプレース
            var newRoot = root.ReplaceNode(usingDirective, linqEx);
            var newDocument = document.WithSyntaxRoot(newRoot); // ルート差し替えでフィニッシュ
 
            return Task.FromResult(newDocument);
        }
    }
}

ここでの作業は、変更対象のノードを見つけることと、差し替えることです。ノードを見つけるための下準備に関しては、とりあえずサンプルコードのまんま(diagnostic/diagnosticSpan)でいいかな、と。そこから先は独自に探し出す必要があります。今回はUsingDirectiveを見つけたかったんですが、幸いルートからのFindNode一発で済みました、楽ちん。あとは置換するだけです。

置換に関しては、コード上に書いたように、大きい単位で新しいSyntaxNodeを作って差し替える、のはやめたほうがいいです。そうするとトリビアを取りこぼす可能性が高く、うまく修正かけられなかったりします。面倒くさくても、置き換えたいものをピンポイントに絞って置換かけましょう。ノードを探索するにはLINQ to XMLスタイルでのDescendantsやAncestors、ChildNodesとかがあります。LINQ to SyntaxTreeってところで、この辺はまさにLINQ to XMLとは何であるのか。ツリー構造に対するLINQ的操作のリファレンスデザインだと捉えることができるって感じですね。

さて、置換といっても、Roslynのコードは全てイミュータブル(不変)なので、戻り値をうまく使ってルートに伝えていく必要があります。Replace一発では済まないのです。これは面倒くさいんですが、まぁ慣れればこんなものかなー、と思えるでしょう、多分きっと。

ともあれ、これで出来上がりました!ちなみにデバッグはVsixプロジェクトをデバッグ実行すれば、拡張ロード済みの新しいVSが立ち上がる&アタッチされているので、サクッとデバッグできます。これは相当楽だし助かる(いかんせん慣れないRoslynプログラムは試行錯誤しまくるので!)。また、生成物に関してはAnalyzersにdllを手配置もいいですが、ビルドプロジェクト自体に.nupkg生成が含まれているので、そいつを使ってもいいでせう。その辺のことはテンプレートに入ってるReadMe.txtに書いてあるので一回読んでおくといいかな。

Unityで使う

新しいVSが出ると拡張が対応してくれるか、が最大の懸念になるのですが、なんとVisual Studio Tools for Unity(VSTU/旧UnityVS)は初日から対応してくれました!まさにMicrosoft買収のお陰という感じで、非常に嬉しい。遠慮無くVisual Studio 2015 Preview Tools for Unityを入れましょう。VSTUについてはVisual Studio Tools for Unity(UnityVS) - Unity開発におけるVisual Studioのすすめを見てね。

基本的にはUnityのプロジェクトにも全く問題なくAnalyzerを追加できて解析できます。素晴らしい!んですが、問題が一点だけあります。それはVSTUはUnity側に何か変更があった時に.csprojを自動生成するんですが、その自動生成によってせっかく追加したAnalyzerも吹っ飛びます。Oh……。

という時のためにVSTUはProject File Generationという仕組みを用意してくれています。これによってプロジェクトとソリューションの自動生成をフックできます(ちなみに実例として、うちの会社ではソリューションにサーバーサイドとか色々なプロジェクトをぶら下げてるのでソリューション自動生成を抑制したり、Unityプロジェクト側にT4テンプレートを使った自動生成コードを入れているので、VSTUのcsprojの自動生成時に.ttファイルを復元してやったり、とか色々な処理を入れてます)

今回は自動生成で消滅するAnalyzerを復元してやる処理を書きましょう。Editor拡張として作るので、Editorフォルダ以下にProjectFileHook.csを追加し、以下のコードを追加。

using System.IO;
using System.Linq;
using System.Text;
using System.Xml.Linq;
using UnityEditor;
 
[InitializeOnLoad]
public class ProjectFileHook
{
    // necessary for XLinq to save the xml project file in utf8
    private class Utf8StringWriter : StringWriter
    {
        public override Encoding Encoding
        {
            get { return Encoding.UTF8; }
        }
    }
 
    static ProjectFileHook()
    {
        SyntaxTree.VisualStudio.Unity.Bridge.ProjectFilesGenerator.ProjectFileGeneration += (string name, string content) =>
        {
            // ファイルがない場合はスルー(初回生成時)
            if (!File.Exists(name)) return content;
 
            // 現在のcsprojをnameから引っ張ってきてAnalyzerを探す
            var currentContent = XDocument.Load(name);
            var ns = currentContent.Root.Name.Namespace;
            var analyzers = currentContent.Descendants(ns + "Analyzer").ToArray();
 
            // content(VSTUが生成した新しいcsprojにAnalyzerを注入)
            var newContent = XDocument.Parse(content);
            newContent.Root.Add(new XElement(ns + "ItemGroup", analyzers));
 
            // したのを返す
            using (var sw = new Utf8StringWriter())
            {
                newContent.Save(sw);
 
                return sw.ToString();
            }
        };
    }
}

nameにファイルパス、contentにVSTUが生成した新しいcsprojのテキストが渡ってくるので、それを使ってモニョモニョ処理。csprojはXMLなので、LINQ to XML使ってゴソゴソするのが楽ちんでしょう。

これでUnityでもRoslynパワーを100%活かせます!やったね!

まとめ

あんだけ盛大に吹聴してたわりには、コード解析とリファクタリングだけかよ……、という感はなきにしも非ずですが、そのかわりすっごく簡単に作れる、追加できる仕組みを用意してくれたのは評価できます(えらそう)。かなり便利なので、早速是非是非遊んでみるといいんじゃないかな、とオモイマス。

ところで今回の例、CodeFixProviderはナシにしてAnalyzerだけにして、AnalyzerのレベルをWarningではなくDiagnosticSeverity.Errorにすることで、「LINQ禁止」を暗黙のルールじゃなくコンパイル不可能レベルで実現できます。拡張メソッドを明示的に呼び出せば回避できますが、ルールにプラスしてEnumerableの静的メソッドも殺せば、もう完全に死亡!恐ろしい恐ろしい。あ、勿論やらないでくださいね!

LINQ to GameObjectによるUnityでのLINQの活用

Unityで、LINQは活用されているようでされていないようで、基本的にはあまりされていない気配を非常に感じます。もったいない!というわけじゃないんですが、以前に私の勤務先と別の会社さんとで勉強会/交流会したのですが、そこで作ったスライドがあるので(若干手直ししたものを)公開します。LINQについて全くの初心者の人向けにLINQの良さを説明しようー、みたいな感じです、でもちょびっとだけ踏み込んだ内容もね、みたいな。勉強会自体は5月ぐらいにやったので、ずいぶんと公開まで開いてしまった……。

LINQ in Unity from Yoshifumi Kawai

その私の勤務先(まどろっこしい言い方だ……)グラニでは会社間での勉強会は大歓迎なので、もし、やりたい!という人がいらっしゃいましたら是非是非私のほうまでー。オフィスは六本木にあるのでその周囲ほげkmぐらいまでなら出張りますです(他のオフィスを見てみたい野次馬根性)。私の持ちネタとしてはC#, LINQ, Rxぐらいならなんでもどこまでもいけます。

さて、そんなわけでLINQは有益です、という話なんですが(AOT問題はスライドに書きましたが頑張れば解決できる!)、LINQを活用するためにはデータソースを見つけなきゃいけません。逆にデータソースさえ見つかれば幾らでも使えます。今回(本題)着目したのはGameObjectのヒエラルキーで、これを丸々とLINQと親和性の高い形で探索/操作できるアセット「LINQ to GameObject」を作りました。GitHubでソースコード、Unity Asset StoreではFREEで公開しています。

実際のトコロ、多分、みんな絶対に作って手元に持ってるちょっとしたユーティリティ、です。これもまた一つの俺々ユーティリティ。ちょっと違うところがあるとしたら、このライブラリは、ツリー階層構造へのLINQスタイルの操作ということで、LINQ to XMLからインスパイアされていて、API体系を寄せています。既に実績があり、そして実際に良さが保証されている(LINQ to XMLは非常に良いのです!問題はXMLを操作する機会がJSONに奪われてしまって近年少なくなってしまっただけでLINQ to XML自体は非常に良い!)APIとほぼ同一なので、ある程度のクオリティが担保されている、ということでしょうか。

ツリー探索とLINQ

探索のAPIは図にすると、以下の様な形になっています。

起点を元に親方向(Parent/Ancestors)か、子孫方向(Child/Children/Descendants)か、兄弟方向(ObjectsBeforeSelf/ObjectsAfterSelf)かに並んでいるGameObjectをIEnumerable<GameObject>として列挙します。

// 以下の様な形に抽出される
origin.Ancestors();   // Container, Root
origin.Children();    // Sphere_A, Sphere_B, Group, Sphere_A, Sphere_B
origin.Descendants(); // Sphere_A, Sphere_B, Group, P1, Group, Sphere_B, P2, Sphere_A, Sphere_B
origin.ObjectsBeforeSelf(); // C1, C2
origin.ObjectsAfterSelf();  // C3, C4

これはXPath(今となっては懐かしい響き!)の「軸」と同じもので、考えられる全ての列挙方向/方法を満たしています。特徴的なのは全てがIEnumerable<GameObject>になることで、LINQ to Objectsとシームレスに繋がり、フィルタやコレクションの変形を連続して行うことができます。

// 子孫方向のゲームオブジェクトの近いものトップ5を配列に固める(ただforeachするだけなら配列にしなくていい/しないほうがいいよ!)
var nearObjects = origin.Descendants()
    .OrderBy(x => (x.transform.position - this.transform.position).sqrMagnitude)
    .Take(5)
    .ToArray();
 
// 子孫方向の全ゲームオブジェクトのうちtagが"foobar"のオブジェクトを破壊
origin.Descendants().Where(x => x.tag == "foobar").Destroy();
 
// 自分を含む子ノードからBoxCollider2Dを抽出
var colliders = origin.ChildrenAndSelf().OfComponent<BoxCollider2D>();
 
// 全ての方法を組み合わせで満たせる、例えば兄弟方向に下のノードの全子孫はObjectsAfterSelf + Descendants
// これは ObjectsAfterSelf().SelectMany(x => x.Descendants()) のシンタックスシュガー
origin.ObjectsAfterSelf().Descendants();

SelectしてOrderByして、あれやこれや、なども自由に幾らでも行えます。また、繋がった状態での定形操作ということで、LINQ to GameObjectでは更にIEnumerable<GameObject>に対してDestoryとOfComponentという拡張メソッドを用意しています。それとちなみに性能面でも余計な中間コレクションを作らないため、(理屈上は)優位です。この理屈上ってのが、まぁ、あんまり踏み込みません:)

階層へのオブジェクトの追加

ところで利用法ですが、全てのメソッドはGameObjectへの拡張メソッドとして実装しています!暴力的に大雑把に!なので

using Unity.Linq;

としてもらえれば、全部のメソッドがにょきにょきっと生えます。メソッド一覧はリファレンスにあります。

オブジェクトを追加する、というのは階層を意識して追加していくわけですが、素でやるとparentにアタッチしてsiblingを弄って、というのは非常にカッタルイ話で絶対にみんな何とかゆーてぃりてぃを持っているとは思うのですが、LINQ to GameObjectにもあります。これも同様にLINQ to XMLと同じAPIを採用し、全ての方向/方法を網羅しています。

var root = GameObject.Find("root"); 
var cube = Resources.Load("Prefabs/PrefabCube") as GameObject; 
 
// Addは子の末尾に追加
// Parentの設定の他にレイヤーの統一とlocalPosition/Scale/Rotationを調整します
// 追加された子はCloneされていて、戻り値はそのCloneされたものを返します
var clone = root.Add(cube);
 
// 兄弟方向、自分の下に追加
// オブジェクトを追加する際は配列で渡せば複数一気に追加され、クローンされたオブジェクトをListで受け取れます
var clones = root.AddAfterSelf(new[] { cube, cube, cube });  
 
// 他にAddFirst(子の先頭に追加)とAddBeforeSelf(兄弟方向、自分の上)がある
// 追加の向きとしてはこれで全パターンでしょう!
 
// ついでに(?)Destoryの拡張メソッドもあり
// nullかどうかのチェック + 一旦階層から外してDestoryします
root.Destroy();

Addなんていう超汎用的くさい名前をGameObjectへの拡張メソッドにするってのがすっごく極悪なんですが、まぁいっか、みたいな。いいんですかね、いや、いいでしょう、はい、多分、うん。

LINQ to XMLはツリーへのLINQ的操作のリファレンスデザイン

LINQ to BigQuery - C#による型付きDSLとLINQPadによるDumpと可視化で、LINQの定義を

LINQがLINQであるためにはクエリ構文はいらない。Query Providerもいらない。LINQ to XMLがLINQなのは何故?Parallel LINQがLINQであるのは何故?Reactive ExtensionsがLINQであるのは何故?linq.jsがLINQであるのは何故?そこにあるのは……、空気と文化。

LINQと名乗ること自体はマーケティングのようなもので、形はない。使う人が納得さえすれば、LINQでしょう。そこにルールを求めたがる人がいても、ないものはないのだから規定しようがないよ?LINQらしく感じさせる要素をある程度満たしてればいい。FuncもしくはExpressionを使ってWhereでフィルタしSelectで射影する(そうすればクエリ構文もある程度は使えるしね)。OrderBy系の構文はOrderBy/OrderByDescending/ThenBy/ThenByDescendingで適用される。基本的な戻り値がシーケンスっぽい何かである。うん、だんだん満たせてくる。別に100%満たさなくても、70%ぐらい満たせばLINQらしいんだよ。

極論言えば私がLINQだって言ってるんだからLINQなのですが(何か文句ある?)、多くの人には十分納得してもらえると考えています

と、かなり乱暴な感じに「勝手に」定義しましたが(つまり今回のLINQ to GameObjectも私がLINQだって言ってるんだからLINQなのだ!)、実際、LINQ to GameObjectからはLINQらしさを感じ取れるんじゃないかと思います。何故か?当然理由はあるし、そうなるように意識してデザインしてます。

LINQ to XMLとは何であるのか。ツリー構造に対するLINQ的操作のリファレンスデザインだと捉えることができます。ツリー構造はLINQになる、そのガイドライン。LINQ to Objectsと非常に相性の良い探索の抽象化。もちろん、それ自体はXML向けだけど、「軸」を意識すればJSONにも適用できるし、そして、LINQ to GameObjectにも適用できた。

もしツリーを見かけたら、そこにLINQがなかったら、同じように作ることができるし、そうすればLINQの全てのメリットを甘受できる。データソースを発見していくこと。これは視点の問題で、そう捉えれば見えるようになる。それがLINQをただ漠然と使うことから一歩踏み出せるんじゃないのかな。

ユニットテスト

今回はじめてUnity Test Toolsをちょろっとだけ使ってみました。UniRxではファイルをリンクとしてコピーして普通の.NET上、Visual Studio上のMSTestで動かすという荒っぽいことをしてて、それはそれで楽ちんでいいんですけど、GameObjectへの操作とかUnityEngineに依存するものはさすがに無理で、今回のライブラリは100%それなので困ったなー、と。で、そこで、Unity Test Toolsの出番だったわけですね。

うん、まあ、普通ですね!いや、普通で、普通に悪くないです。ロジックのテストには全然いい。便利だよ。で、アサーションは普通にNUnitなんですが、私はAssert.AreEqualとかAssert.Thatとか嫌いなのです!嫌いなのでふつーのC#用にはChaining Assertionというライブラリを作って/公開しているんですが、それのUnity版を用意しました。

AssetStoreに投稿するほどのものかなーってことで、とりあえずLINQ to GameObjectのリポジトリ内にあるだけなんですが、気になる人はEditor/ChainingAssertion.csをどうぞ。この.csファイル一個だけです。これで

[Test]
public void Children()
{
    Origin.Children().Select(x => x.name)
        .IsCollection("Sphere_A", "Sphere_B", "Group", "Sphere_A", "Sphere_B");
 
    Origin.Children("Sphere_B").Select(x => x.name)
        .IsCollection("Sphere_B", "Sphere_B");
 
    Origin.ChildrenAndSelf().Select(x => x.name)
        .IsCollection("Origin", "Sphere_A", "Sphere_B", "Group", "Sphere_A", "Sphere_B");
 
    Origin.ChildrenAndSelf("Sphere_A").Select(x => x.name)
        .IsCollection("Sphere_A", "Sphere_A");
}

みたいな感じにテスト書けます/書きました。

まとめ

LINQは良い。LINQを使うにはIEnumerableが必要。LINQ to GameObjectはそのIEnumerableを作り出すので、UnityでよりLINQを活用できる!ので使いましょう。

(ところでObjectsAfterSelfはAfterSelfのほうが良いですね……すっごく失敗した……次のバージョンで変更するかも、というかします、はい……)。

あとLINQ to XMLの大きな要素として関数型構築があるんですが、勿論LINQ to GameObjectにも用意しました!ただ、実用性は(LINQ to XMLと違ってUnityの性質上)ビミョーなので、ここでは紹介しません。とりあえずとにかく、「ツリーを上下左右に探索できて」「ツリーの上下左右に追加できて」「関数型構築できれば」ツリーへのLINQ。と言えます、きっと。

それとUniRxなんですが、スライドで少し触れましたが、uFrameというUnity用のフレームワークに採用されて同梱されるようになりました。結構いい具合に躍進してきてるんで、UniRxも是非是非チェックを。ブログは全然書いてないんですが(!)機能拡充はずっと続けているんで、また近いうちに何か書きませう。

Unity + iOSのAOTでの例外の発生パターンと対処法

Unity、はUnity3Dのほうの話ですが、それで開発していてiOS実機にデプロイして確認すると、以下の様なエラーに悩まされると思います!

System.ExecutionEngineException: Attempting to JIT compile method

ひぎぃ!怖い!これはiOSはネイティブコードしか許可していないので、MonoのAOT(Ahead-Of-Time)コンパイラ経由でネイティブコード変換されるんですが、それの関係で色々な制限があるからなのですね。さて、制限があるのはshoganaiんですが、引っかかるのは痛いです、めっちゃ痛いです、辛いです。

というわけで、どういうコードを書けば発生するのか、というのを並べてみました。どうすれば発生するのか分かれば、自然に避けられますからね。そのうえで、幾つかのものはちょっとしたハックで防げるので、それも述べます。あとは、一々実機で確認なんてやってられないので、効率のよい確認方法などなども紹介します。

Unity 4.5で少し改善されたとか言ってましたが別にあんま改善されてる気配なくて以下のコードは4.5.1で確認取って全部片っ端から死にますんで安心してください、悲しい。

Interlocked.CompareExchange

正確にはInterlocked.CompareExchange<T>が死にます。以下のコードは即死。

// ExecutionEngineException: Attempting to JIT compile method '(wrapper native-to-managed)' while running with --aot-only
var a = "hoge";
Interlocked.CompareExchange<string>(ref a, "hugahuga", "hoge");

ExecutionEngineExceptionの中でもnative-to-managedと出ているものは対処方法が明確で、そもそもUnityのトラブルシューティングのiOSのところにも書いてあります。デリゲートに[MonoPInvokeCallback]が必要だ、と。つまりそういうことで、mscorlib.dll内のメソッドなので手が出せないので、100%死ぬ運命にあります、南無。対処方法は使わないこと。(実際にはそれだけじゃなさそうですが、中のことで分からないのでとりあえずそういうことにしておこふ)

ただし、実はCompareExchangeにはintやdoubleなどを受け取るオーバーロードがあって、そちらは大丈夫です。問題なのは<T>のオーバーロードだけなのです。しかもCompareExchangeにはobjectを受け取るオーバーロードもあるので、そちらを使うことによりT的なものも一応回避することが可能。どうしても使いたい場合は安心してどうぞ。

// これは大丈夫!
object a = "hoge";
var v = Interlocked.CompareExchange(ref a, "hugahuga", "hoge");

ちなみにInterlocked.CompareExchange<T>は意外なところでも使われていて、というか、VS2010以降のコンパイラでeventをコンパイルすると、eventの実装がInterlocked.CompareExchange<T>を用いたものになっています。なのでプラグインとしてdllを作ってUnityに読み込ませると、これに引っかかって死にます。回避方法はなし。event使うのやめましょう、Actionで我慢しましょう。なお、Unity内だけで使う分には古いコードが吐かれるので問題ないです。(あとufcppさんからコメント貰いましたが、add/deleteといったカスタムイベントアクセサを定義すれば回避できるもよふ)

動的コード生成

Reflection.Emitとか、この辺は当たり前だ!ですね。

Expression<Func<string>> expr = () => "hoge";
 
// System.ExecutionEngineException: Attempting to JIT compile method '(wrapper dynamic-method) System.Runtime.CompilerServices.ExecutionScope:lambda_method (System.Runtime.CompilerServices.ExecutionScope)' while running with --aot-only.
expr.Compile();

Expressionも構築まではOKだけどCompileはNG。悩ましいのは一般的にC#で高速化を測る場合(特にシリアライザ)って動的コード生成+キャッシュをよく使います。neue cc - C#での動的なメソッド選択における定形高速化パターンとかneue cc - Expression Treeのこね方・入門編 - 動的にデリゲートを生成してリフレクションを高速化をミテネ。が、動的コード生成が使えないと低速なリフレクションのみかぁ、うーん、萎える。といったかんぢ。こういうのが積み重なってC#が遅いとか言われると心外だなぁ、UnityのC#は正直、うーん、ねぇ……。

PropertyのReflection

そんなわけでリフレクション。これがひじょーに悩ましくて、どこまでが死んでどこまで大丈夫なのかがひじょーーーーーに分かりづらい!さて、実は意外と行けますが、そして意外と死にます。

// こんなクラスがあるとして
public class MyClass
{
    public int MyInt { get; set; }
    public string MyStr { get; set; }
}
 
// ----
 
var mc = new MyClass() { MyStr = "hoge", MyInt = 100 };
var propInfo = typeof(MyClass).GetProperty("MyStr");
 
// SetValueは大丈夫
propInfo.SetValue(mc, "hugahuga", null);
 
// GetValueは死ぬ
// System.ExecutionEngineException: Attempting to JIT compile method '(wrapper delegate-invoke) System.Reflection.MonoProperty/Getter`2<>:invoke_string__this___MyClass ()' while running with --aot-only.
var v = propInfo.GetValue(mc, null);

(GetValueは死ぬ、って書きましたがUnity 4.5.1 + iOS7.1で試したら死ななかった、↑が死んだのはmono2.6.7でした)。なんだこの非対称ってところですが、実際そうだからshoganai。そしてGetValueは実は簡単に回避できます。GetGetMethodでメソッドを取得して、それをInvokeすればいい。

var mc = new MyClass() { MyStr = "hoge", MyInt = 100 };
var propInfo = typeof(MyClass).GetProperty("MyStr");
 
// こうすればGetもできる
var v = propInfo.GetGetMethod().Invoke(mc, null);
Debug.Log(v);

というわけで、これでシリアライザも作ることができます。例えばこんな感じの簡易シリアライザ。

public static void Run()
{
    var format = Serialize(new MyClass { MyInt = 100, MyStr = "hoge" });
 
    var v = Deserialize<MyClass>(format);
    Debug.Log(v.MyStr + ":" + v.MyInt);
}
 
public static string Serialize<T>(T obj)
{
    // JSON、ではない
    var sb = new StringBuilder();
    foreach (var item in typeof(T).GetProperties())
    {
        sb.Append(item.Name + ":" + item.GetGetMethod().Invoke(obj, null));
        sb.Append(",");
    }
    return sb.ToString();
}
 
public static T Deserialize<T>(string format)
{
    var obj = Activator.CreateInstance<T>();
    var type = typeof(T);
 
    foreach (var item in format.Split(new[] { ',' }, StringSplitOptions.RemoveEmptyEntries).Select(x => x.Split(':')))
    {
        var key = item[0];
        var value = item[1];
 
        var propInfo = type.GetProperty(key);
        if (propInfo == null) continue;
 
        // 型の変換は超絶手抜き:)
        if (propInfo.PropertyType == typeof(int))
        {
            propInfo.SetValue(obj, int.Parse(value), null);
        }
        else
        {
            propInfo.SetValue(obj, value, null);
        }
    }
 
    return obj;
}

これはインチキなテキスト形式にシリアライズしてますが、例えばJSONにシリアライズ・デシリアライズとかできるようにすれば、ひじょーに有益でしょう。ベタリフレクションとかC#er的には萎えるんですが、まぁその辺はshoganaiということで諦めるぐらいはできる。諦めます。

InterfaceとGenericsとStruct

この3つが組み合わさることによって端的に言えば、死ぬ。

// こんなインターフェイスとメソッドがあるとして
public interface IMyInterface
{
    void MyMethod<T>(T x);
}
 
public class MyImpl : IMyInterface
{
    public void MyMethod<T>(T x)
    {
    }
}
 
IMyInterface intf = new MyImpl();
intf.MyMethod("hogehoge"); // 死なない
 
// System.ExecutionEngineException: Attempting to JIT compile method 'MyImpl:MyMethod<int> (int)' while running with --aot-only.
intf.MyMethod(100); // 死ぬ

ジェネリクスのメソッドをインターフェイスで受けて、構造体を渡すと死にます。死にます。クラスなら死なないんですけどねー。さて、しかしこの現象は回避する術があります。

// どこでもいいし呼び出さなくてもいいから、使う構造体の型を呼ぶコードをどっかに書いておく
static void _CompilerHint()
{
    new MyImpl().MyMethod(default(int));
}
 
void Awake()
{
    IMyInterface intf = new MyImpl();
    intf.MyMethod(100); // ↑により死なない
}

実体で実際に使う型を用いて呼び出してるコードを書いておくと死なずに済みます。実際に呼び出す必要はなくて、とにかく書いてあればいいです。イメージとしてはコンパイラにヒントを与えるような感じ。なのでまぁ、1. インターフェイスで受けないようにする 2.受けなきゃならないシチュエーションがあるなら(まぁそりゃあるよね)どっかに定義沢山書きだしておく。ことにより神回避。オマジナイのようでいて実際効果あるからshoganai。

あと、ジェネリクスはメソッドじゃなくてインターフェイスのほうがTなら死にません。IMyInterface<T>みたいなほう。

LambdaとGenericsとStruct

Genericsのラムダ作って構造体渡すと死にます、例によって渡すのがクラスなら死にません。

// こんなメソッドがあるとして
static void Death<T>()
{
    var act = new Action<T>(_ => { Debug.Log("hoge"); }); // ここではまだ死なない
 
    // System.ExecutionEngineException: Attempting to JIT compile method '<Death>b__0<int> (int)' while running with --aot-only.
    act(default(T)); // 呼び出すと死ぬ
}
 
// こんなコード呼び出しすると死ぬ
Death<int>();

こんな入り組んだコード書かないって?いや、案外このパターンに当てはまっちゃったりしたりするんですよ。特にライブラリ書いたりする人だとラムダ式の使いどころによっては、こういうパターンになりがちで頭抱えます。解決策はラムダ式使うのやめよう!じゃあなくて、簡単な解決策があります。

static void Death<T>()
{
    var _dummy = 0;
    var act = new Action<T>(_ => 
    {
        Debug.Log("hoge");
 
        _dummy.GetHashCode(); // なんでもいいから外側の変数をキャプチャする
    });
 
    act(default(T)); // 死なない
}

ラムダ式は外側の変数をキャプチャするかしないかによって、生成されるコードが変わってきます。そこがミソで、勿論キャプチャしないほうが本来は効率がいいんですが、AOTで死んでしまっては元も子もない。キャプチャすることによってAOTで死なないコードが生成されます、というわけで、入り組んだシチュエーションでラムダ式使いたい場合は意図的に外側の変数をキャプチャすることで回避できます。これは思いついた時は思わず叫んじゃいましたね!マジで!(そんだけこの問題に悩まされてたんですよ……)

参照型で死ぬ

型引数がクラスなら死ぬことはない、と思っていた時もありました。残念ながら、死ぬ時があるんですねぇー。いや、正確にはclass+structで死ぬ、なんですが、struct+structだと死なないのが癪。これは後述しますがLINQのSumがクラスで死ぬ理由が分からなくて再現コード作ってたらこうなったって感じです。よくわからないけど、こうなった。

public static void Run()
{
    // 参照型でメソッドを呼ぶ
    // System.ExecutionEngineException: Attempting to JIT compile method 'Method2<int, object> ()' while running with --aot-only.
    Method1<object>();
}
 
// 1型引数でメソッドを呼ぶ際に片方が値型
public static void Method1<T1>()
{
    Method2<int, T1>();
}
 
// 2型引数で戻り値がある(戻り値の型はなんでもいいけどvoidはダメ)
static string Method2<T1, T2>()
{
    return "";
}

ちなみにMethod1<int>みたいに、struct渡すんなら動くんですよね、逆にこれは。クラスだと死ぬ。どうしてこうなるのか、ちょっとこれはよくわからないですね、ともかくクラスでも油断すると死ぬということはよくわかりました、あべし。

LINQで死ぬ

UnityのiOSビルドで使うとエラーになるLINQ拡張メソッドのメモといった記事もありますが、実際死にます。これは濡れ衣みたいなものなんですけどねぇ、別にLINQが悪いわけじゃないし、それでLINQ使わない!LINQ禁止!とか絶対言って欲しくないです。LINQのないC#なんてC#じゃないです。C#の魅力の8割はLINQなのですから。と、それは置いておいて、実際幾つかのLINQのメソッドは死にます。

例えばAverage(selector)。

// System.ExecutionEngineException: Attempting to JIT compile method 'System.Linq.Enumerable:<Average`1>m__20<int> (long,int)' while running with --aot-only.
Enumerable.Range(1, 3).Average(x => x);

なんで死ぬのかというと、ソース見れば簡単に分かります。Unity-Technologies/monoからEnumerable.csの該当行を見ると

return source.Select (selector).Average<int, long, double> ((a, b) => a + b, (a, b) => (double) a / (double) b);

お分かりかな?そう、「LambdaとGenericsとStruct」のところで見たように、Genericsのメソッドの中で値型のラムダが放出されてます。そう、結構あるんですよ、Genericsのメソッドの中にラムダを埋めてしまうのって。さて、で、これは死にます。具体的に死んだ箇所は、エラー履歴の一番上のat…のとこ見れば

// at System.Linq.Enumerable.Average[Int32,Int64,Double] (IEnumerable`1 source, System.Func`3 func, System.Func`3 result) [0x00000] in <filename unknown>:0

privateメソッドのAverage(↑でいうAverage[int, long, double]のとこ)で死んでるのが分かります。基本的に呼び出すタイミングで死ぬのでfunc(total, element)ってとこが死亡地点だと推測付きます。

これの対処方法は?ないよ!System.Core.dllの中のコードだから手が出せません。もはや使わないしか選択できません!もしくは、自前実装してAverageSafeとかって拡張メソッドを用意するとか、ですかねえ。それも悪くはないと思います、shoganaiし。

で、実はこの問題は当然mono本体は気付いていて、mono 2.8では改善されています。該当コミットを見れば、ラムダ使って共通化されてるコードがコピペに置き換えられてます:) これがAOTセーフだ!みたいな。はい、ライブラリには苦労してもらいましょう、使う側が快適ならそれで、それがいいのです。

残念ながら現在のUnity(4.5.1)のmonoは2.6で、しかも2.8へのUpgradeは蹴られてます。3.0(そう、monoの最新はもう遠いところにある、Unity置いてかれすぎ)へのアップデートは、もしやる気があるとしても大仕事になるだろうから、当面は来そうにないですねえ。でもmono 2.8で改善されたのって4年前なんですよね、4年前から変わらずLINQ(の一部)が使えないUnity……、残念です。とりあえずダメ元でEnumerable.csだけでもバージョンあげてくれ!ってFeedbackを出したので、よければVoteしてください。Upgrade Enumerable.cs for avoid AOT Problem of LINQ(Average etc…)

XamarinのほうはAOTに関しても先を行っているようで、詳しくはXamarinの中の人である榎本さんのインサイドXamarin(6)の真ん中辺りに書いてあります。最新のXamarinと昔のXamarinと、そしてUnityとではAOTの制限がそれぞれ微妙に違っててなんとも。しかしXamarin、じゅる、いいなぁ……。

UnityのLINQでは他にも明らかに使えないメソッドがあって、例えばThenBy。

// System.ExecutionEngineException: Attempting to JIT compile method 'System.Linq.OrderedEnumerable`1<int>:CreateOrderedEnumerable<int> (System.Func`2<int, int>,System.Collections.Generic.IComparer`1<int>,bool)' while running with --aot-only.
Enumerable.Range(1, 3)
    .OrderBy(x => x)
    .ThenBy(x => x)
    .ToArray();

死にます。これも最新のmonoでは解決しています、該当ソース

#if FULL_AOT_RUNTIME
			var oe = source as OrderedEnumerable <TSource>;
			if (oe != null)
				return oe.CreateOrderedEnumerable (keySelector, comparer, false);
#endif

これは「InterfaceとGenericsとStruct」のとこに書いた制限を回避してます。IOrderedEnumerableというインターフェイスのままCreateOrderedEnumerableを呼ぶと死ぬので、OrderedEnumerableにキャストして具象型に戻すことによってうまく動くようにしています。ThenByは便利なので使いたいものですねえ(まぁ富豪な処理なのでゲーム向けかと言われるとビミョーですが)

最後に、Sumは参照型でも死にます。逆に値型だと生き残れます。

// System.ExecutionEngineException: Attempting to JIT compile method 'System.Linq.Enumerable:Sum<object, int> (System.Collections.Generic.IEnumerable`1<object>,System.Func`3<int, object, int>)' while running with --aot-only.
Enumerable.Empty<object>().Sum(x => (int)x);

これは「参照型で死ぬ」パターン、ソースコードの該当箇所を見ると……

public static int Sum<TSource> (this IEnumerable<TSource> source, Func<TSource, int> selector)
{
	Check.SourceAndSelector (source, selector);
 
	return Sum<TSource, int> (source, (a, b) => checked (a + selector (b)));
}
 
static TR Sum<TA, TR> (this IEnumerable<TA> source, Func<TR, TA, TR> selector)
{
	TR total = default (TR);
	long counter = 0;
	foreach (var element in source) {
		total = selector (total, element);
		++counter;
	}
 
	return total;
}

これねえ、なんで参照型で死ぬのか本当にさっぱり分からなかったけど、とりあえずSum<TSource, int>で二回層掘ってるのが死因っぽいです、片方はint固定で、二階層目は戻り値TRっていう。Max, Minが参照型のみ死ぬもの同じようなコードだった。これで死ぬとかもはや理不尽さしか感じなくて怖い怖い。ちなみにmonoの最新版のコードではメソッドは一回層で重複上等のハイパーコピペになってます(勿論それによりExceptionは発生しなくなる)、それでいいです、はい、ほんと。

そんなわけでLINQは一部の地雷メソッドに注意しながら使う!まぁ、それはそれでいいんですが(地雷が怖いから使わないってのはNG)、やっぱ地雷が埋まってるのは怖い。というわけで、Unityのmonoのランタイムが新しくなってくれるのが一番なのですが現実はそれを待ってはいられないので、mono本体のEnumerable周辺コードを頂いて、名前空間だけ、例えばSystem.LinqExとかにして、基本そちらをusingするようにするっていう風にして回避するのがいいんじゃないかしら、というか私はそうしてます。この辺は名前空間の切り分けだけでなんとかなる拡張メソッドの良さですね。

Enumで死ぬ

簡単にはEnumの配列をToArrayすると観測できる!

// こんなEnumがあるとして
public enum MyEnum
{
    Apple
}
 
// ToArrayで問答無用で死ぬ
// System.ExecutionEngineException: Attempting to JIT compile method '(wrapper managed-to-managed) MyEnum[]:System.Collections.Generic.ICollection`1.CopyTo (UniRx.MyEnum[],int)' while running with --aot-only.
new[] { MyEnum.Apple }.ToArray();

(wrapper managed-to-managed)ってのが目新しくていいですね!これの対処方法は、元が配列とかListだと印象的にヤヴァいので空のイテレータに変えてやります、それもご丁寧にジェネリクスじゃないIEnumeratorを経由することで、なんとなく回避できます。

public static class AotSafeExtensions
{
    // こんなメソッドを用意しておくと
    public static IEnumerable<T> AsSafeEnumerable<T>(this IEnumerable<T> source)
    {
        var e = ((IEnumerable)source).GetEnumerator();
        using (e as IDisposable)
        {
            while (e.MoveNext())
            {
                yield return (T)e.Current;
            }
        }
    }
}
 
// 死なない!
new[] { MyEnum.Apple }.AsSafeEnumerable().ToArray();

ヤヴァそうな香りがしたらAsSafeEnumerableを呼ぶ、という対処療法で勝つる。かなぁ……?

実機を使わないでAOTのテストする方法

ここまでで例外の発生パターンと対処法は終わり。じゃあ実際、こういった問題をどう検出するか、ひたすら実機テスト?というのも辛い。で、AOT自体はmono本体にもあって、そして現在のUnityはmono 2.6相当です。というわけでmono 2.6でAOTを動かせばいいんじゃろ?mono –full-aot hoge.exeと書くだけで、iOS実機とほぼほぼ同等のAOT例外が検出できます(この記事の範囲だとInterlocked.CompareExchange以外は同じ)。MonoBehaviourとかは無理ですがロジック系だったらNUnitでユニットテスト書いて、回すことで自動テスト可能になります。

実際、私はこの記事を書くにあたって、Windows + Visual Studio 2013でC#を書いて.exe(ConsoleApplication)作って、それを会社の同僚の作ってくれたexeを渡すとfull-aotで実行して結果表示してくれるウェブサービスに突っ込んで延々と動作確認してました。超捗る。むしろ同僚が神だった。実機とかやってられない。そもそもUnity書くのもVisual Studioじゃなきゃ嫌だ(UnityVS - Unity開発におけるVisual Studio利用のすすめ)。

UniRx

なんで延々と調べたかというと、今、私はUniRx - Reactive Extensions for Unityというライブラリを作っていて、というか実際アセットストアにも既に公開されているんですが(無料です!)、例によってiOSで動かなくて!で、重い腰を上げて調べたのでした。パターンさえ分かってしまえば、まあ十分対応できる範囲ですねー、というわけでバシバシと動かなくなる箇所を殺してる最中です。

UniRx自体はブログ記事をそのうち書く書く詐欺で(一応、ちょっとだけ発表した時の資料はある)、ええと、AOTの対処が終わったら書く!というのと、【第1回】UnityアセットまみれのLT大会でLTするつもりなので、そちらでもよろしくお願いします。というか是非いらしてください、お話しませう。

Visual Studio Tools for Unity(UnityVS) - Unity開発におけるVisual Studioのすすめ

追記:Microsoftが買収してVisual Studio Tools for Unityとして無料でリリースされました、やったね!

Unityで開発するにあたってエディタは何を使っていますか?といったら、勿論Microsoft Visual Studio!というわけで、VSとUnityを統合してコーディング&デバッグを可能にしてくれるUnityVSの紹介をしたいと思います。とにかく素晴らしいので、超オススメ。ちなみに最新のVS2013にも勿論対応していますよ。

ちょうどUnite Japan 2014で、UnityVS作者のJb Evain氏が「Unityゲーム開発へのVisual Studio導入」というセッションを行い、勿論喜んで聞きに行った!感動した!ので、その講演をベースに紹介したいと思います。講演聞く前からUnityVSは使っていたのですが、改めて超良いなー、と、むしろもっと利用者を増やさなければ!という義務感がですね、はい。

Unityのスクリプト開発環境

UnityScriptの、じゃなくて.csとか.jsを何で書くか、のお話。

  1. MonoDevelop

標準同梱、色々なプラットフォームで動くし、ちゃんとIDEなので決して悪くない。が、Unityについてくるバージョンは古い場合が多く、そのためバグが残っていたり。また、日本語の入力が大変問題が多く叫ばれていたりする。

  1. 外部のテキストエディタ

SublimeやVim、Emacsなど。特にSublimeはよく使われているよう。非常に軽快で悪くない、とはいえ、IDEの持つ多くの機能を当然持っていないわけで、機能としては劣るといえる。SublimeはSublimeSocketAssetを入れると補完(弱)とかエラー表示とかは少しまかなえるようですけれど、Vimとかもそうですね、頑張れはするのだけれど、最終的にIDEに及ぶかというと、まあ、頑張れる。頑張れはする。

  1. Unityエディタ上のスクリプトエディタ

UnIDEとか。カジュアルな編集にはベンリだけれど専用に使っていけるか、というと……。

  1. Visual Studio

Windows専用。有料、安くない。とはいえ機能は最強。

そんなわけでVisual Studioを選べるのなら選ぶべき!なのだけれど、単純にUnityのエディタとしてVSを使うと幾つかの問題がある。

VSはMicrosoft .NET用でありUnity向けではない
VSのプロジェクト構造はUnityとミスマッチがある
2つのツール(Unity-VS)間でやりとりが取れない
デバッガーが動かせない(動かしにくい)

と、いった問題をUnityVSは解決し、両者を完全に統合してくれる。

UnityVSの機能

  • Unity Project Explorer

勿論、VSのソリューションエクスプローラーも使えますが、このUnity Project ExplorerはUnityのProjectウィンドウと同じ見た目を提供してくれるので、こっちのほうが選びやすい場合はこっちが使えます。

  • C#/Boo/UnityScriptのシンタックスハイライト/入力補完。

C#だけじゃなくてBoo, UnityScriptにも対応。普段はC#使ってても、AssetStore経由でBooやUnityScriptを取得することもあるだろうし(Booはあるのかなあ?)、全対応は良いこと素晴らしい。日本語のコメントを使うことも出来るし、入力補完等はVisual Studioそのものなので完璧。

  • メソッド生成ウィンドウ

MonoBehaviourのOnMouseEnterなどの雛形が直ちに作れるので、ついつい忘れがち/ミスりがちな名前をリファレンスからコピペったりしなくても作れる。画像のような大きなウィンドウの他に、その場でテキストベース補完でササッと作れるQuick MonoBehaviours Windowもあり。

  • リファレンスの統合

クラス名やメソッド名を選択してヘルプ→Unity API Reference、もしくはショートカットキーでVS上でその場でリファレンスが引けます(VS内ウィンドウで開かれる)。ベンリベンリ。

  • デバッグ

F5押すだけでデバッガをUnityにアタッチできる、当然動いてる最中はVSのデバッグ機能がフルに利用可。ローカルウィンドウもウォッチウィンドウも、ステップ実行も全て。(ただしCoroutineの中では挙動がかなり怪しくなるのでそこだけは注意)。

  • 外部DLLサポート

外部DLLを参照した場合でも、デバッガでしっかりとステップ実行できてとても嬉しい。あと、Visual Studioで開発できることの嬉しさに、サーバーサイド(C#/ASP.NET)とUnityのプロジェクトを同じソリューションに突っ込めるところがあったりします。移動が簡単だし、通信用データや幾つかのロジックが共有できたりする。サーバーサイドはPHPで開発なんておかしいよ!全部C#で書くんだよもん。例えば以下のような構成を取ってみる。

私がCTOを務めるグラニは、現在のところウェブベースのソーシャルゲーム(神獄のヴァルハラゲート、今CMやってますん)を提供していて、それはC# 5.0 + Windows Server 2012(IIS 8.0) + ASP.NET MVC 5で動いていたりします。サーバーサイドをC#で開発するのは得意領域なので、そのままにクライアントサイドとC#で融和出来れば、開発効率相当良い……!といったことがUnityVSならシームレスに実現できて素晴らしい。

実際、そうしたサーバーAPIをC#で書いて、そのメタデータを元にUnityの通信クライアントを自動生成、送受信データはサーバー側とクライアント側で共有するの前提にしたWebAPIフレームワークLightNodeというのを作ってます。(ところで絶賛エンジニアも募集してます←求人←宣伝)。

共有用のクラスライブラリは、UnityVS入れるとUnity用プロファイルで作れるのも嬉しい。

ちなみに、見た目上はVisual Studioのソリューションに収まっているとはいえ、Unityのシステム的に、VSのプロジェクト参照は無効なのは注意(参照してもUnity側でリロードすると消えちゃうの)。ルール通り、Assets下にDLLを配置する必要があります。それに関してはUnityVSのドキュメントDLL Debuggingで触れられてますが、ビルド後にAssetsにDLLを配置するように仕込むと良いもよう。例えば以下のようなものをShare.csprojに足してやればOK。

<!-- 実際にはDebugビルドとRelaseビルド分けるのもいれよふね -->
<Target Name="AfterBuild">
    <ItemGroup>
        <CopySource Include="bin\Debug\*.*" />
    </ItemGroup>
    <Copy SourceFiles=" @(CopySource)" DestinationFolder="$(SolutionDir)\Assets\External\" SkipUnchangedFiles="true" OverwriteReadOnlyFiles="true" />
</Target>

利用感としては、まぁまぁ悪くないです。メソッドの参照に飛ぶとDLLのメタデータを参照しに行ってしまうのが残念ではありますが。あとは.slnの場所がUnityプロジェクト下になってしまって構成がいびつなのがちょっとだけ辛い、かな?その辺の生成はカスタマイズできる - Project File Generationっぽいんだけど、上手くいかなくて今のところ断念中。

ということで

Unityの開発って結構Macで行ってる人が多いのですね。実にイマドキ……。それでも、スクリプティング環境はVisual Studioがベストだと思うんだなあ。VMにWindows入れてでもなんでも使ったほうが圧倒的に捗る、はず。少なくともエディタで苦労するよりは百億倍。ちなみにRemote Debuggingもあるようですよ。

それでもVisual Studioは高い……?うーん、そもそもUnity Proのほうがずっと高いんですが(VS Proは単品をふつーに買って6万ぐらい、色々な購入モデルがあるので実質もっと安くはなるかな)、それはおいといて、私がBuild Insiderに寄稿した記事でスタートアップ企業にマイクロソフト製品の開発ライセンスが無償提供されるBizSparkプログラム活用のススメ(タイトルクソ長い!)で紹介しているBizSparkというプログラムでは、設立5年未満の企業ならVisual Studioを無償で利用することが可能です。実際BizSparkはとても良い、助かる、助かってた。学生さんならDreamSparkという同様の学生支援プログラムがあります。

C#自体、非常に良い言語なのですが、Visual Studioと合間れば相乗効果で数倍数十倍に更に良くなるので、(たとえUnityのC#が古いバージョンだったりiOSのせいでAOTで苦しんだりしつつも)、良きC#生活を満喫して欲しい/したいところですねー。

| Next

Search/Archive

Category

Profile


Yoshifumi Kawai
Microsoft MVP for Developer Technologies(C#)

April 2011
|
July 2020

Twitter:@neuecc
GitHub:neuecc
ils@neue.cc