ほげほげー

C#メインにプログラミング周りから日常のあれこれとかを不定期に書いていきます。

Unity での開発でも async/await を使おう

いつも通り久しぶりに記事を書きます。

Unity で async/await を使うようになってから大変捗っているので覚書。

async/await 以前の非同期

従来の C# では非同期を取り扱うためには色々面倒な事をする必要がありました。

例えば delegateBeginInvoke, EndInvoke を使う。

例えば System.Threading.Thread を使う。

例えば System.Threading.ThreadPool を使う。

などなど。まぁ歴史的経緯もあってほんとに色々あります。

どれでやるにしても本当に簡単な処理を非同期にする時以外は、
メインスレッドに返したり、非同期の連鎖があったりと、
大抵の場合非常にコードが汚くなりがちです。

Unity ではもちろん UI に触るときはメインスレッドからしか受け付けていないので、
別スレッドで処理した場合はメインスレッドに何らかの方法でディスパッチする必要があります。*1*2

async/await 以後の非同期

async/await は C# 5.0(.NET Framework 4.5) から導入された演算子です。

戻り値を void, Task, Task<T> にして、async キーワードを付ける事で、
Task, Task<T> を戻り値にしているメソッドを await 演算子で待機できるようになります。*3

普通のメソッド呼び出しと同じように書けるようになるので非常に読みやすい。

await NanikaOmotaiAsync();
var result = await NanikaOmotaiGetAsync();
await NanikaOmotaiSetAsync(result);

適切に Task で処理されていれば、await で待機した場合、非同期処理後は元のスレッドで処理を再開するので、
重たい処理の間だけ別スレッドで処理して、それ以外の処理は呼び出し元のスレッドで処理する形になります。

f:id:tyhe:20160404220239p:plain

重い処理が MainThread を専有しなくなるので UI が固まることはありません。

Unity で非同期をするには

  • 昔ながらの手段を使う
  • コルーチンを使う(後述)
  • UniRx を使う
  • MinimumAsyncBridge を使う(本記事で紹介)

Unity のコルーチンは非同期なのか

非同期だけどシングルスレッドなので注意しないとUIが固まります。

f:id:tyhe:20160404220034p:plain

StartCoroutine で登録された IEnumerator 全てに対して1フレーム毎に MoveNext を呼び出します。
また、全てのMoveNext の処理が終わるまでの間1フレームが終わりません。
そのため、30fpsのゲームの場合は30ms、60fpsのゲームの場合は15ms超える処理となってしまうとFPSが落ちてしまいます。

コルーチンは基本的に重たい処理を複数フレームに小分けにして処理をするもの、
と考えておくと良いですね。

コルーチンで目にする yield が何なのかについては以下の記事を読んでいただくと比較的分かりやすいと思います。

tyheeeee.hateblo.jp

Unity で async/await を使って開発する

やっと本題。

Unity Script では使えません。残念でした。

しかしながら、Assets/Dlls 以下に設置する dll については別。

dll の作成時には async/await を使おうと思えば使えるのです。
ただし、Unity で使える .NET Framework は 3.5 Subset 相当なので当然 Task クラスやら何やらはありません。

ではどうするか?

自前で Awaiter パターン を実装した Task クラスを実装すれば良いのです。

Awaiter パターンについて詳しくは他の方の記事を見ていただくとして、
ここでは簡単に。

class Hoge
{
    public HogeAwaiter GetAwaiter();
}

class HogeAwaiter
{
    public bool IsCompleted { get; }
    public void OnCompleted(Action continuation) { }
    public T GetResult() { }
}

これらのメソッドを持っているオブジェクトなら実は何でも await 出来ます。
そして拡張メソッドでも OK なので、本当になんでも。

しかしながら、イチから実装するのは大変なので、ここでは株式会社オレンジキューブが公開している
MinimumAsyncBridge を使います。

github.com

NuGet Package も公開されているのでパッケージマネージャコンソールからインストールできます。

Install-Package MinimumAsyncBridge

では早速。

プロジェクトを作成して、対象のフレームワーク .NET Framework 3.5 にする

f:id:tyhe:20160405004041p:plain

参照を右クリックして NuGet パッケージの管理を開く

f:id:tyhe:20160405002422p:plain

MinimumAsyncBridge の最新版をインストールする

f:id:tyhe:20160405002534p:plain

MinimumThreadingBridge も一緒にインストールされるのでインストールする

f:id:tyhe:20160405002629p:plain

これでできました。

あとは何も意識すること無く普通に async/await が使えます。

f:id:tyhe:20160405004242p:plain

さて、何で使えるのかというと、

  • async/await をコンパイルした時に吐かれる IL 自体には何の拡張も無い
  • GetAwaiter 関連さえ実装されていれば async/await は使える

ということは、Unity でも Dlls に入れる分には特に問題なく使えるのです。

その辺の軽い説明はこちらを参照。

[http://tyheeeee.hateblo.jp/entry/2015/06/12/Visual_Studio_2015%E3%81%A8_Unity%E3%81%A8:embed:cite]

特に問題なく使えると言ったな。あれは嘘だ。

async/await 対応は実は完璧ではなくて、
Release ビルドをすると IL2CPP でのビルドに失敗します。

Release ビルド時に特定の最適化がされる事に起因するっぽいのですが、
IL2CPP そのものに修正が入らないとどうにもならないので、まぁ Debug ビルドで我慢。

バグとしては挙げているので、まぁそのうち・・・きっと直らないと思います。

とはいえ、最適化入っても入らなくてもそんな致命的な程には変わらないので、 async/await の利便性を享受するために甘んじて受け入れましょう。

2016/12/27 追記

Release ビルド時に [StructLayout(LayoutKind.Sequential)] 属性が付与される事に起因しているっぽい。 https://unity3d.com/jp/unity/whats-new/unity-5.3.5 の Fixes に書かれている IL2CPP: 基本クラスが StructLayout 属性を持たないときに、StructLayout 属性を持つC# クラスのための適切なC++コードを生成(767367) で直ってるかもしれません(未確認)

これが原因だと分かっていれば無理矢理やってやれない事も無くて、
Mono.Cecil あたりを使って dll 読み込んで属性をちょちょいと外して書き戻してあげれば良かったりします。

実際現在開発中のゲームではそれで動作しています。

Unity の今後について

先日 Xamarin が Microsoft に買収されて、ARM 版の Mono がオープンソース化されたことで、
Unity の C# 最新仕様への対応が飛躍的に進むんでないかと期待していたりします。

そして .NET Foundation への加入も表明し、これから加速していきそうな予感がします。

Unityが.NET Foundationに参加 - Unity Technologies Blog

これからの Unity に期待ですね。

C# 6.0 対応はやく

*1:UI をメインスレッドからしかさわれないのは他のフレームワークでもでも大体同様です。WPF や WinForms でもそう。

*2:UI ではない WWW クラスも AssetBundle をダウンロードする機能を積んでいるからか、メインスレッドで触らないといけないのはホントひどい話

*3:厳密には GetAwaiter が使えるクラスなら何でも