Archive - Unity

Unite Tokyo 2019でC# Structの進化の話をしてきました

Unite Tokyo 2019にて、「Understanding C# Struct All Things」と題して登壇してきました!動画は後日Unity Learning Materialsに公開される予定です。

【Unite Tokyo 2019】Understanding C# Struct All Things from UnityTechnologiesJapan002

C#成分強めなので、Unityに馴染みのない人でも読んで楽しめる内容になっていると思います。とはいえ勿論、Uniteでのセッションであるということに納得してもらえるようUnity成分もきちんと取り入れています。というわけで、どちらの属性の人にも楽しんでいただければ!

structに関する機能強化、実際めっちゃ多いんですが、それをカタログ的に延々と紹介してもつまらないので、そうしたカタログ紹介っぽさが出ないように気を配ってます。あと、あんましそういうのでは脳みそに入ってこないというのもあるので。

応用例的なところのものは、ないとつまらないよねーということで色々持ってきたのですが、もう少し説明厚くしても良かったかなー感はありました。セッション内でも雰囲気で流した感じありますしね。とはいえ尺とか尺とか。まぁ雰囲気を分かってもらえれば(structは色々遊べるよ、という)いい、と割り切った面もあるにはあります。詳しくは資料を熟読してください!

Span/NativeArrayの説明も厚くしたくはあったんですが、structそのものの本題からは若干外れるので見送り。あと尺とか尺とか。

今年のUnite、もの凄くいいイベントでした。神運営とはこのことか……。そしてDOTSが熱い。めっちゃやるやる詐欺なので、いい加減本当にそろそろDOTSに手を出して楽しみたいですます!

あと、MessagePack-CSharp v2はいい加減そろそろ出るはず予定です、ちなみにSystem.Memoryとかにめっちゃ依存しているので、MessagePack-CSharpを入れるとSpanとかSystem.Runtime.CompilerServices.Unsafeとかが解禁されます(依存ライブラリとして同梱する予定なので)。いいのかわるいのか。まあ、いいでしょふ。未来未来。

CIや実機でUnityのユニットテストを実行してSlackに通知するなどする

前回(?)CircleCIでUnityをテスト/ビルドする、或いは.unitypackageを作るまででは、ユニットテストに関する部分がうまく行ってなくて放置でした。放置でいっかな、と思ってたんですが、改めてユニットテストをCIでがっつり実行したい、というかIL2CPPのテストをがっつしやりたい。という切実な要望が私の中で発生したので(N回目)、改めて取り組んでみました。

さて、オフィシャルな(?)ユニットテストのコマンドラインの実行の口は、Writing and executing tests in Unity Test Runnerの最後の方のRunning from the command lineの節に書いてありました(コマンドライン引数のほうのマニュアルにはリンクすら張ってなかったので気づかなかった……!)。つまり、こんなふうにやればいい、と。

Unity.exe -runTests -testResults C:\temp\results.xml -testPlatform StandaloneWindows64

そうすると、テストが失敗しても正常終了して(?) results.xml に結果が入ってるからそっち見ればOK、と。んー、いや、何か違うような。「Run all in player」で出てくるGUI画面も意味不明だし、Editor上のTest Runnerはいい感じなのだけれど、ビルドしてのテストだとイマイチ感がめっちゃ否めない。

と、いうわけで、なんとなく見えてきたのは、テストはUnity Test Runnerでそのまま書きたいしエディタ上でPlay Modeテストもしたい。それをそのままCIや実機でテストできるように、表示やパイプラインだけをいい具合に処理するビルドを作る何かを用意すればいいんじゃないか、と。

RuntimeUnitTestToolkit v2

ちょうどUnity Test Runnerがイマイチだった頃に作った俺々テストフレームワークがありました。ので、それを元にして、Unity Test RunnerのCLI/GUIのフロントエンドとして機能するようにリニューアルしました。コード的には全面書き換えですね……!

Unity Test RunnerのPlayModeで動くテストがあれば、それだけで他に何もする必要はありません。例えばこんなやつがあるとして

image

メニューのほうで適当にターゲットフレームワークとかIL2CPPがどうのとかを設定してもらって

image

BuildUnitTestを押すと、こんなような結果が得られます。

image

比較的ヒューマンリーダブルなログ!WindowsでもIL2CPPビルドができるようになったのがとっても捗るところで、検証用の小さめプロジェクトなら1分あればコード編集からチェックまで行けるので、リフレクションのキワイ部分をごりごり突いてもなんとかなる!昔のiOSでビルドして動かしてをやってたのは本当に死ぬほど辛かった……。

これはHeadless(CUI)でビルドしたものですが、GUIでのビルドも可能です。

image

イケてる画面かどうかでは微妙ですが、機能的には十二分です。Headlessだと上から下まで全部のテストを実行しちゃいますが、GUIだとピンポイントで実行するテストを選べるので(ただしメソッド単位ではなくクラス単位)、テストプロジェクトが大きくなっている場合はこっちのほうが便利ですね。

さて、Headlessでビルドしたものは、もちろんCIでそのまま実行できます。

image

これはNGが出ている例ですが、ちゃんと真っ赤にCIのパイプラインが止まるようになってます。止まればもちろんCIの通知設定で、Slackでもなんでもどこにでもサクッと飛ばせます。実に正しい普通で普遍なやり方でいいじゃないですか。はい。というわけでやりたかったことが完璧にできてるのでめでたしめでたし。

Linux ContainerとUnity

相変わらずCircleCIで色々トライしているのですが、Linuxコンテナ + Unityでの限界、というかUnityのLinux対応が後手に回ってる影響をくらってビミョーという現実がやっと見えてきました。まず、そもそもにLinux + IL2CPPはまだサポートされてないので、CI上でIL2CPPビルドしたものを実行してテスト、みたいなのはその時点でできない。残念。しゃーないのでWindows + IL2CPPビルドを作って、実行だけ手元でやるのでもいっか、と思ったらそもそもLinuxでIL2CPPビルドができない。なるほど、そりゃそうか、って気もしますが悲しみはある。

と、いうわけで、コンテナベースでやるとどうしてもLinuxの上でのパターンが中心になってしまうので、Unityだと結構厳しいところはありますよねえ、という。

さて、CircleCIの場合は(有料プランでは)Mac VMも使えるので、多少コンフィグの書き方も変わってきますが(マシンセットアップ部分が面倒くさくなる!)、動かせなくもないんちゃうんちゃうんといったところです。或いはAzure DevOpsなどを使えばWindowsマシンが使えるので、こちらもUnityのインストールなどのセットアップは必要ですが、安心感はありますね。どちらにせよWindowsでしかビルドできないもの(Hololensとか)もあるので、ちょっとちゃんと考えてみるのはいいのかなあ、と思ってます。

何れにせよ、VMでやるんだったらそりゃ普通にできますよね、という当たり前の結論に戻ってくるのが世の中きびすぃ。とりあえず私的にはIL2CPPビルドが実行できればいいので、Linux + IL2CPP対応をどうかどうか……。

RandomFixtureKit

ユニットテスト用にもう一個、RandomFixtureKitというライブラリを作りました。こちらは .NET Core用とUnity用の両対応です。

なにかというと、オブジェクトにランダムで適当な値を自動で詰め込むという代物です。当然リフレクションの塊で、これのIL2CPP対応に、先のRuntimeUnitTestToolkitが役に立ちました。

APIも単純でFixtureFactory.Createで取り出すだけ。

// get single value
var value = FixtureFactory.Create<Foo>();
 
// get array
var values = FixtureFactory.CreateMany<Bar>();
 
// get temporal value(you can use this values to use invoke target method)
var (x, y, z) = FixtureFactory.Create<(int, string, short)>();

テスト書いていてダミーのデータを延々と書くの面倒くせー、という局面はめっちゃあって、別に賢い名前なんて必要なくて(例えばAddressにはそれっぽい住所、Nameにはそれっぽい人名を入れてくれるとか)、全然ランダム英数でもいいから詰めてくれればそれでいいの!というところにピッタリはまります。

実用的には、私はシリアライザの入れ替えとか(なぜか)よくやるんですが、旧シリアライザと新シリアライザで互換性なくて壊れたりしないように、相互に値を詰めたりとかして、同一の結果が得られることを確認したりします。そのときに、dllをなめて対象になる数百の型を取って、RandomFixtureKitを使って、適当な値を詰めた上で、一致を比較するユニットテストを用意するとかやったりします。

面白い機能としては、ランダムな値ではなくて、エッジケースになり得る値だけを詰めるモードを用意しています。

たとえばintだったらint.MinValue, MaxValue, 0, -1, 1を。コレクションだったらnull, 長さ0, 長さ1, 長さ9の中からランダムで詰める、といったものですね。

こういうキワいデータが入ったときにー、みたいなことは想定しなきゃいけないし、テストも書いておかなきゃなのは分かってるけれど、毎回データ変えて流すのクソ面倒くさいんですよね(私はシリアライザを(なぜか)よく書くので、本当にこういうデータをいっぱい用意する必要が実際ある)。ので、CreateManyで1000個ぐらい作って流し込んでチェックすれば、多少はケースが埋まった状態になるでしょうというあれそれです。使ってみると意外と便利ですよ。

ところで

ゴールデンウィークの最終日なのですが、ほとんど何もやってない!始まる前は、MessagePack-CSharpやMagicOnionのタスクを潰しつつ、Pure C#のHTTP/2 Clientを作ってMagicOnionを強化するぜ、とか息巻いていたのですが全然できてない。副産物というか横道にそれたユニットテスト関連を仕上げて終わりとか、なんと虚しい……。

できなかった理由の半分はSwitchでCelesteを遊び始めたらめちゃくちゃハマって延々とやり続けちゃったせいなのですが、まぁそれはそれで面白いゲームをたっぷり楽しめたということで有意義なのでよしということにしておきます。

MagicOnionは6月4日に勉強会をやります。というわけで、やる気もかなりあるし、アップデートネタも溜まっているんですが、実際にアップデートはできてないので(Issueのヘンジはちゃんとやってます!)、GWでガッと手を入れておきたかったんですが、うーん、まぁ明けてからやりまうす。色々良い感じになっていると思います。いやほんと。

CircleCIでUnityをテスト/ビルドする、或いは.unitypackageを作るまで

死ぬほどお久しぶりです!別にインターネット的には沈黙してるわけじゃなくTwitterにもいるし、会社(Cysharp)関連で露出あるかもないかもというわけで、決して沈黙していたわけでもないはずですが、しかしブログは完全に放置していました、あらあら。

C#的にも色々やっていて、CloudStructuresのv2を@xin9leさんとともにリリースしたり、多分、今日に詳細を書くつもりですがMicroBatchFrameworkというライブラリをリリースしたり、Ulidというライブラリをリリースしてたり、まぁ色々やってます。ちゃんと。実際。今月はそのMicroBatchFramework関連で、AWS .NET Developer User Group 勉強会 #1に登壇しますし。リブートしたMagicOnionも来月勉強会開催予定だったりで、めっちゃやる気です。

さて、そんなやる気に満ち溢れている私なのですが(実際Cysharpもいい感じに動き出せているので!お問い合わせフォームないけどお問い合わせ絶賛募集中!)、ブログは放置。よくないね。というわけで表題の件について。

目的と目標

CIの有効性について未だに言う必要なんてなにもないわけですが、しかし、.unitypackageを手作業で作っていたのです。今まで。私は。UniRxとかMessagePack-CSharpの。そして死ぬほど面倒くさいがゆえに更新もリリースも億劫になるという泥沼にハマったのです。やる気が満ち溢れている時は手作業でもやれるけれど、やる気が低下している時でも継続してリリースできなければならないし、そのためにCIはきっちりセットアップしておかなければならないのです。という真理にようやく至りました。なんで今さらなのかというと、私がアプリケーション書くマンであることと、CIとかそういうのは全部、部下に丸投げして自分は一切手を付けてこなかったマンだからです。しかし会社のこともあるので、いい加減にそれで済まなくなってきたので(今更やっとようやく)真面目に勉強しだしたのですね……!

で、CIにはCircleCIを使います。なんでCircleCIなのかというと、一つはUnity Cloud Buildだとunitypackageを作れない(多分)というのが一つ。もう一つは、私が.NET CoreのCIもCircleCIに寄せているので、統一して扱えるといいよねというところです。また、Linuxの他にMacでのビルドもできるので(有料プラン)、iOSに、とかも可能になってくるかもしれませんしね。あと、単純にCircleCIが昨今のCIサービスで王者なので、長いものに巻かれろ理論でもある。でも私自身も最近使っていてかなり気に入ってるので、実際良いかと良いかと。コンテナベースで記述するのがとても小気味よいわけです、モダンっぽいし。

ゴールは

  • リポジトリの一部ソース郡から.unitypackageを作る
  • EditorでUnitTestを行う
  • IL2CPP/Windowsでビルドする(↑のUnitTestのIL2CPP版を吐く)

となります。普通はAndroidやiOSビルドがしたいって話だと思うのですが、私はライブラリAuthorなので、まずそっちの要求のほうが先ということで(そのうちやりたいですけどね!)。Editorテストだけじゃなくて、IL2CPPで動作するか不安度もあるので、そっちのexeも吐ければ嬉しい。できればIL2CPPビルドのものも、ヘッドレスで起動して結果レポーティングまでやれればいいん&ちょっと作りこめばそこまで行けそうですが、とりあえずのゴールはビルドして生成物を保存するところまでにしておきましょう。そこまで書いてると記事長くなるし。

認証を通してUnityをCircleCI上で動かす

CircleCIということでコンテナで動かすんですが、まぁUnityのイメージを持ってきてbatchmodeで起動して成果を取り出すという、それだけの話です。適当にUnityのコマンドライン引数とにらめっこすれば良い、と。

コンテナイメージに関しては、幸い誰か(gablerouxさん)がgableroux/unity3d/tagsに公開してくれていて、綺麗にタグを振ってくれています。コンテナの良いところっていっぱいあると思いますが、コンテナレジストリが良い具合に抽象化されたファイル置き場として機能するのも素敵なとこですねえ。また、こうして公開してくれていれば、社内CIのUnityインストール管理とかしないで済むのも良いところです。大変よろしい。

で、Unityの実態は /opt/Unity/Editor/Unity にあるので、それを適当に -batchmode で叩けばいいんでしょって話ですが、しかし最大の関門はライセンス認証。それに関してはイメージを公開してくれているgablerouxさんのGabLeRoux/unity3d-ci-exampleや、そして日本語ではCircleCIでUnityのTest&Buildを雰囲気理解で走らせたに、手取り足取り乗っているので、基本的にはその通りに動かせば大丈夫です。

ただ、ちょっと情報が古いっぽくて、今のUnityだともう少し手順を簡単にできるので(というのを試行錯誤してたら苦戦してしまった!)、少しシンプルになったものを以下に載せます。

まず、ローカル上でライセンスファイルを作る必要があります。これはdockerイメージ上で行います。また、ここで使うイメージはCIで実際に使うイメージと同じバージョンでなければなりません。バージョン変わったらライセンス作り直しってことですね、しょーがない。そのうちここも自動化したくなるかもですが、今は手動でやりましょう。

docker run -it gableroux/unity3d:2018.3.11f1 bash
cd /opt/Unity/Editor
./Unity -quit -batchmode -nographics -logFile -createManualActivationFile
cat Unity_v2018.3.11f1.alf

イメージを落としてきて、 -quit -batchmode -nographics -logFile -createManualActivationFile でUnityを叩くと Unity_v***.alf という中身はXMLの、ライセンスファイルの元(まだuseridもpasswordも入力してないので、テンプレみたいなものです)が生成されます。こいつを、とりあえず手元(ホスト側)に持ってきます。docker cpでコンテナ->ホストにファイルを移動させてもいいんですが、まぁ1ファイルだけなのでcatしてコピペして適当に保存でもOK。

次にhttps://license.unity3d.com/manualを開いて、上記のalfファイルを上げると Unity_v2018.x.ulf ファイルがもらえます。これが実体です。生成過程でUnityのサイトにログインしているはずで、そのuserid/passwordが元になって、ライセンスファイルの実体が生成されました。中身はXMLです。

で、これは大事な情報なのでCircleCI上のEnvironment Variablesで秘匿しよう、という話になるんですが、改行の入った長いXMLなので、そのまんま中身をコピペるとファイルが、たいていどこか壊れて認証通らなくなります(散々通らないでなんでかなぁ、と悩みました!)。とはいえファイルそのものをリポジトリに上げるのはよろしくないので、CircleCIでUnityのTest&Buildを雰囲気理解で走らせたにあるとおり、暗号化したものをリポジトリに追加して、Environment VariablesにはKeyを追加しましょう。

openssl aes-256-cbc -e -in ./Unity_v2018.x.ulf -out ./Unity_v2018.x.ulf-cipher -k ${CIPHER_KEY}

${CIPHER_KEY}は、適当な文字列に置き換えてもらって、そしてこれをCircleCI上のEnvironment Variablesにも設定します。ファイルの置き場所は、とりあえず私は .circleci/Unity_v2018.x.ulf-cipher に置きました、CIでしか使わないものなので。

またはマルチラインキーの場合は base64を使うことが推奨されているようです => Encoding Multi-Line Environment Variables。こちらのほうが良さそうですね。

あとは .circleci/config.ymlを書くだけ、ということで、最小の構成はこんな感じになります。

version: 2.1
executors:
  unity:
    docker:
      # https://hub.docker.com/r/gableroux/unity3d/tags
      - image: gableroux/unity3d:2018.3.11f1
jobs:
  build-test:
    executor: unity
    steps:
      - checkout
      - run: openssl aes-256-cbc -d -in .circleci/Unity_v2018.x.ulf-cipher -k ${CIPHER_KEY} >> .circleci/Unity_v2018.x.ulf
      - run: /opt/Unity/Editor/Unity -quit -batchmode -nographics -silent-crashes -logFile -manualLicenseFile .circleci/Unity_v2018.x.ulf || exit 0
workflows:
  version: 2
  build:
    jobs:
      - build-test

-nographicsにすることでそのまま叩けるのと、-manualLicenseFileでライセンスファイルを渡してやるだけです。 認証する際の || exit 0 がお洒落ポイントで、認証が正常に済んでもexit code 1が返ってくるという謎仕様なので、とりあえずこのステップは強制的に正常終了扱いにしてあげることで、なんとかなります。なんか変ですが、まぁそんなものです。世の中。

まぁしかしGabLeRoux/unity3d-ci-exampleの(無駄に)複雑な例に比べれば随分すっきりしたのではないでしょうか。いやまぁ、Unityのイメージ作ってもらってるので感謝ではあるのですけれど、しかしサンプルが複雑なのは頂けないかなあ。私はサンプルは限りなくシンプルにすべき主義者なので。

.unitypackageを作る

バッチモードでは -executeMethod により特定のstatic methodが叩けるので、それでunitypackageを作るコードを用意します。 今回は Editor/PackageExport.cs に以下のようなファイルを。

using System;
using System.IO;
using System.Linq;
using UnityEditor;
using UnityEngine;
 
// namespaceがあると動かなさそうなので、グローバル名前空間に置く
public static class PackageExport
{
    // メソッドはstaticでなければならない
    [MenuItem("Tools/Export Unitypackage")]
    public static void Export()
    {
        // configure
        var root = "Scripts/CISample";
        var exportPath = "./CISample.unitypackage";
 
        var path = Path.Combine(Application.dataPath, root);
        var assets = Directory.EnumerateFiles(path, "*", SearchOption.AllDirectories)
            .Where(x => Path.GetExtension(x) == ".cs")
            .Select(x => "Assets" + x.Replace(Application.dataPath, "").Replace(@"\", "/"))
            .ToArray();
 
        UnityEngine.Debug.Log("Export below files" + Environment.NewLine + string.Join(Environment.NewLine, assets));
 
        AssetDatabase.ExportPackage(
            assets,
            exportPath,
            ExportPackageOptions.Default);
 
        UnityEngine.Debug.Log("Export complete: " + Path.GetFullPath(exportPath));
    }
}

ちょっとassetsを取るところが長くなってしまっているのですが、.cs以外をフィルタするコードを入れています。たまに割と入れたくないものが混ざっていたりするので。あとは、CIではライセンス認証のあとに、これを叩くコマンドと、artifactに保存するコマンドを載せれば良いでしょう。

version: 2.1
executors:
  unity:
    docker:
      # https://hub.docker.com/r/gableroux/unity3d/tags
      - image: gableroux/unity3d:2018.3.11f1
jobs:
  build-test:
    executor: unity
    steps:
      - checkout
      - run: openssl aes-256-cbc -d -in .circleci/Unity_v2018.x.ulf-cipher -k ${CIPHER_KEY} >> .circleci/Unity_v2018.x.ulf
      - run: /opt/Unity/Editor/Unity -quit -batchmode -nographics -silent-crashes -logFile -manualLicenseFile .circleci/Unity_v2018.x.ulf || exit 0
      - run: /opt/Unity/Editor/Unity -quit -batchmode -nographics -silent-crashes -noUpm -logFile -projectPath . -executeMethod PackageExport.Export
      - store_artifacts:
          path: ./CISample.unitypackage
          destination: ./CISample.unitypackage
workflows:
  version: 2
  build:
    jobs:
      - build-test

完璧です!

コマンドに関しては普通にWindowsのUnity.exeで試してから挑むのがいいわけですが、一つWindowsには難点があって、ログが標準出力ではなく %USERPROFILE%\AppData\Local\Unity\Editor\Editor.log にしか吐かれないということです。というわけで、Editor.logを開いてにらめっこしながらコマンドを作り込みましょう。めんどくせ。

EditorでUnitTestを行う

基本的に -runEditorTests をつけるだけなのですが、注意点としては -quit は外しましょう。ついてると正常に動きません(はまった)。

version: 2.1
executors:
  unity:
    docker:
      # https://hub.docker.com/r/gableroux/unity3d/tags
      - image: gableroux/unity3d:2018.3.11f1
jobs:
  build-test:
    executor: unity
    steps:
      - checkout
      - run: openssl aes-256-cbc -d -in .circleci/Unity_v2018.x.ulf-cipher -k ${CIPHER_KEY} >> .circleci/Unity_v2018.x.ulf
      - run: /opt/Unity/Editor/Unity -quit -batchmode -nographics -silent-crashes -logFile -manualLicenseFile .circleci/Unity_v2018.x.ulf || exit 0
 
      - run: /opt/Unity/Editor/Unity -quit -batchmode -nographics -silent-crashes -noUpm -logFile -projectPath . -executeMethod PackageExport.Export
      - store_artifacts:
          path: ./CISample.unitypackage
          destination: ./CISample.unitypackage
 
      - run: /opt/Unity/Editor/Unity -batchmode -nographics -silent-crashes -noUpm -logFile -projectPath . -runEditorTests -editorTestsResultFile ./test-results/results.xml
      - store_test_results:
          path: test_results
workflows:
  version: 2
  build:
    jobs:
      - build-test

editorTestsResultFile で指定し、store_test_resultsに格納することでCircleCI上でテスト結果を見ることができます。

と、思ったんですが、なんかテスト周りは全体的にうまく動かせてないんで後でまた調べて修正します……。或いは教えてくださいです。

IL2CPP/Windowsでビルドする

なぜWindowsかというと、私がWindowsを使っているからというだけなので、その他のビルドが欲しい場合はそれぞれのビルドをしてあげると良いんじゃないかと思います!

いい加減コンフィグも長くなってきましたが、-buildWindows64Playerでビルドして、zipで固めてぽんということです。

version: 2.1
executors:
  unity:
    docker:
      # https://hub.docker.com/r/gableroux/unity3d/tags
      - image: gableroux/unity3d:2018.3.11f1
jobs:
  build-test:
    executor: unity
    steps:
      - checkout
      - run: openssl aes-256-cbc -d -in .circleci/Unity_v2018.x.ulf-cipher -k ${CIPHER_KEY} >> .circleci/Unity_v2018.x.ulf
      - run: /opt/Unity/Editor/Unity -quit -batchmode -nographics -silent-crashes -logFile -manualLicenseFile .circleci/Unity_v2018.x.ulf || exit 0
 
      - run: /opt/Unity/Editor/Unity -quit -batchmode -nographics -silent-crashes -noUpm -logFile -projectPath . -executeMethod PackageExport.Export
      - store_artifacts:
          path: ./CISample.unitypackage
          destination: ./CISample.unitypackage
 
      - run: /opt/Unity/Editor/Unity -batchmode -nographics -silent-crashes -noUpm -logFile -projectPath . -runEditorTests -editorTestsResultFile ./test-results/results.xml
      - store_test_results:
          path: test_results
 
      - run: /opt/Unity/Editor/Unity -quit -batchmode -nographics -silent-crashes -noUpm -logFile -projectPath . -buildWindows64Player ./bin-win64/CISample.exe
      - run: apt-get update
      - run: apt-get install zip -y
      - run: zip -r CISampleWin64Binary.zip ./bin-win64
      - store_artifacts:
          path: ./CISampleWin64Binary.zip
          destination: ./CISampleWin64Binary.zip
workflows:
  version: 2
  build:
    jobs:
      - build-test

これで一旦は希望のものは全てできました!

以上な感じが最終結果になります。

CircleCIでUnityビルドはプロダクトで使えるか

今回の例のようなライブラリ程度だと、リソースもほとんどないしリポジトリも全然小さいんでいいんですが、実プロダクトで使えるかというと、どうでしょう。まずリポジトリのサイズの問題で、次にビルド時間の問題で。クソデカい&高級マシンでも焼き上がり1時間は普通とか、そういう世界ですものね。常識的に考えてこれをクラウドでやるのは難しそう。オンプレのCircleCI Enterpriseだったら行けそうな気もしますが、どうでしょうねえ。しかしJenkinsマンやるよりは、こちらのほうが夢があるのと、実際うまくクラスタを組めば、ばかばかコンテナ立ち上げて同時並列でー、というビルドキュー長蛇列で待ちぼうけも軽減できたりで、良い未来は感じます。試してみたさはあります、あまりJenkinsに戻りたくもないし。

一回構築してみれば、ymlもそこそこシンプルだし、(ライセンス認証以外は)ymlコピペで済むので、Unity Cloud Build使わなくてもいいかなー、色々自由にできるし。っていうのはあります。というわけで、是非一緒にUnityでCircleCI道を突き進んでみましょう:) 今回はAndroidビルドやiOSビルドという面倒くさいところには一切手を付けてませんが、まぁほとんどビルドできてるわけで、やりゃあできるでしょう。いや、でもiOSとか死ぬほど面倒くさ(そう)なので、そのへんよしなにやってくれつつマシンパワーもそこそこ用意してくれるUnity Cloud Buildは偉い。

ところでこのブログ、ymlのシンタックスハイライトがない模様。やべー。このブログのメンテこそが一番最重要な気がしてきた。

UniTask(UniRx.Async)から見るasync/awaitの未来

C# Advent Calendar 2018大遅刻会です。間に合った。間に合ってない。ごめんなさい……。今回ネタとして、改めてコード生成に関して、去年は「動的」な手法を解説した - Introduction to the pragmatic IL via C#ので、現代的な「静的」な手法について説明してみよう、と考えていたのですが、そういえばもう一つ大遅刻がありました。

7月にUniTask - Unity + async/awaitの完全でハイパフォーマンスな統合という記事を出して、リリースしたUniTaskですが、その後もちょこちょこと更新をしていて、内部実装含め当初よりもかなり機能強化されています。といった諸々を含めて、Unity 非同期完全に理解した勉強会で話してきました。

Deep Dive async/await in Unity with UniTask(UniRx.Async)

9月!更新内容の告知もしてなければ、この発表のフォローアップもしてない!最近はこうした文章仕事がめっちゃ遅延するようになってしまいました、めっちゃよくない傾向です。来年はこの辺もなんとかしていきたい。

と、いうわけで、予定を変えてUniRx.Asyncについて、というか、それだとUnity Advent Calendarに書けよって話になるので、UniRx.Asyncは独自のTask生態系を作っている、これは.NET Core 2.1からのValueTaskの拡張であるIValueTaskSourceに繋がる話なので、その辺を絡めながら見ていってもらえると思います。

Incremental Compilerが不要に

告知が遅延しまくっている間にUnity 2018.3が本格リリースされて、標準でC# 7.xに対応したため、最初のリリース時の注釈のような別途Incremental Compiler(preview)を入れる必要がなくなりました。Incremental Compiler、悪くはないのですが、やっぱpreviewで怪しい動きもしていたため、標準まんまで行けるのは相当嬉しいです。というわけで今まで敬遠していた人も早速試しましょう。

new Progress[T] is Evil

これは普通の.NETにも言える話なのですが、C#のasync/awaitの世界観では進捗状況はIProgress[T]で通知していくということになっています(別にAction[T]でよくね?そっちのほうが速いし、説はある)。進捗はReport(T value)メソッドで通知していくことになりますが、こいつは必ずSynchronizationContext.Post経由で値を送ります。これがどういうことかというと、Unityだとfloatを使う、つまりIProgress[float]で表現する場合が多いはずですが、なんと、ボックス化します。(If T is a value type, it will get boxed here.)じゃねーよボケが。アホか。これはオプションで回避不能なので、new Progress[T]は地雷だと思って「絶対に」使わないようにしましょう。

代わりにUniRx.AsyncではProgress.Createを用意しました。これはSynchronizationContextを使いません。もしSyncContext経由で同期したいならマニュアルでやってくれ。Unityの場合、進捗が取れるシチュエーションはメインスレッド上のはずなので、ほとんどのケースでは不要なはずです。

こういった、あらゆる箇所での.NET標準の余計なお世話を観察し、Unityに適した形に置き直していくことをUniRx.Asyncではやってるので、async/await使うならUniRx.Asyncを使ったほうがいいのです。標準のも、今の時代で設計するならこうはなってないと思うんですけどね、まぁ時代が時代なのでshoganai。

コルーチンの置き換えとして

コルーチン、或いはRxでできた処理は、改めて全部精査して、全てasync/awaitで実装できるようにしました。

Add UniTask.WaitUntil
Add UniTask.WaitWhile
Add UniTask.WaitUntilValueChanged
Add UniTask.WaitUntilValueChangedWithIsDestroyed
Add UniTask.SwitchToThreadPool
Add UniTask.SwitchToTaskPool
Add UniTask.SwitchToMainThread
Add UniTask.SwitchToSynchronizationContext
Add UniTask.Yield
Add UniTask.Run
Add UniTask.Lazy
Add UniTask.Void
Add UniTask.ConfigureAwait
Add UniTask.DelayFrame
Add UniTask.Delay(..., bool ignoreTimeScale = false, ...) parameter

概ね名前からイメージ付くでしょう、イメージ通りの挙動をします。こんだけ用意しておきゃほとんど困らないはず(逆に言えば、標準のasync/awaitには何もありません)

ちなみにSwitchTo***は、最初のVisual Studio Async CTP(お試しエディション)に搭載されていたメソッドで、すぐに廃止されました。というのも、async/awaitが自動でスレッド(SynchronizationContext)をコントロールするというデザインになったからですね。あまりにも最初期すぎる話なのでこの辺の話が残っているものも少ないのですが、ちゃんと岩永さんのブログには残っていたので大変素晴らしい。

UniRx.Asyncでは不要なオーバーヘッドを避けるため(そもそも特にUnityだとメインスレッド張り付きの場合のほうが多い)、自動でSynchronizationContextを切り替えることはせず、必要な場合に手動で変更してもらうというデザインを取っています。というか、今からasync/await作り直すなら絶対こうなったと思うんだけどなぁ、どうなんでしょうねぇ。ちょっとSynchronizationContextに夢見すぎだった時代&Windows Phone 7(うわー)とかの要請が強すぎたせいっていう時代背景は感じます。

Everything is awaitable

考えられるありとあらゆるものをawait可能にしました。AsyncOperationだけじゃなくてWWWやJobHandle(そう、C# Job Systemもawaitできます!)、そしてReactivePropertyやReactiveCommand、uGUI Events(button.OnClickAsyncなど)からMonoBehaviour Eventsまで。

さて、AsyncOpeartionなど長さ1の非同期処理がawait可能なら、そらそーだ、って話なのですが、イベントがawait可能ってどういうこっちゃ、というところはあります。

// ようするところこんな風に待てる
async UniTask TripleClick(CancellationToken token)
{
    await button.OnClickAsync(token);
    await button.OnClickAsync(token);
    await button.OnClickAsync(token);
    Debug.Log("Three times clicked");
}

コレに関してはスライドに書いておきましたが、「複雑なイベントの合成」をする際に、Rxよりも可読性良く書ける可能性があります。

Rxは「複雑なイベントハンドリング」を簡単にするものじゃなかったの!?という答えは、YesでもありNoでもありで、複雑なものは複雑で、難しいものは難しいままです。イベントハンドリングは手続き的に記述出来ない(イベントコールバックが飛び飛びになる)ため、コールバックを集約させて合成できるRxが、素のままでやるより効果的だったわけですが、async/awaitはイベントコールバックを手続き的に記述できるため、C#のネイティブのコントロールフロー(for, if, whileなど)や自然な変数の保持が可能になります。これは関数合成で無理やり実現するよりも、可読性良く実現できる可能性が高いです。

単純なものをasync/awaitで記述するのは、それはそれで効率やキャンセルに関する対応を考慮しなければならなくて、正しく処理するのは地味に難易度が高かったりするので、基本的にはRxで、困ったときの必殺技として手段を知っている、ぐらいの心持ちが良いでしょう

async UniTask TripleClick(CancellationToken token)
{
    // 都度OnClick/token渡しするよりも最初にHandlerを取得するほうが高効率
    using (var handler = button.GetAsyncClickEventHandler(token))
    {
        await handler.OnClickAsync();
        await handler.OnClickAsync();
        await handler.OnClickAsync();
        Debug.Log("Three times clicked");
    }
}

↑こういう色々なことを考えるのが面倒くさい。

Exception/Cancellationの扱いをより強固に

UniTaskでは未処理の例外はUniTaskScheduler.UnobservedTaskExceptionによって設定されている未処理例外ハンドラによって処理されます(デフォルトはロギング)。これは、UniTaskVoid、或いはUniTask.Forgetを呼び出している場合は即時に、そうでない場合はUniTaskがGCされた時に未処理例外ハンドラを呼びます。

async/awaitが持つべきステータスは「正常な場合」「エラーの場合」「キャンセルの場合」の3つがあります。しかし、async/awaitならびにC#の伝搬システムは、正常系は戻り値、異常系は例外の二択しかないため、「キャンセルの場合」の表現としてawaitされた元にはOperationCanceledExceptionが投げられます。よって、例外の中で、OperationCanceledExceptionは「特別な例外」です。デフォルトではこの例外が検出されて未処理の場合は、未処理例外ハンドラを無視します。何もしません。キャンセルは定形の処理だと判断して、無視します。

また、例外を使うためパフォーマンス上の懸念もあります。そこで、UniTask.SuppressCancellationThrowを使うことで、対象のUniTaskが例外の発生源であれば(throw済みで上の階層に伝搬されたものではない)、例外の送出ではなく、Tupleでの戻り値としてキャンセルを受け取り、例外発生のコストを抑えることができます。これはイベントハンドリングなどの場合に有用です、が、正しく使うことは内部をかなりのレベルで理解していないといけないため、ぶっちゃけムズい。ただたんにSuppressCancellationThrowを使うだけでパフォーマンスOKというわけにはいかんのだ。というわけで、どうしてもパフォーマンス的に困ったときのための逃げ道、ぐらいに思っておいてください。

UniTaskTracker

とはいえなんのかんのでTaskがリークしてしまったり、想像以上に多く起動してしまっていたりもあるでしょう。UnityのEditor拡張でトラッキングウィンドウを用意したので、すべて追跡できます。

image

こういうのRxにも欲しいわー。そうですね、なんか実装方法は考えてみようかとは思いますが一ミリも期待しないで待たないでください。

IValueTaskSourceでWhenAllを進化させる

.NET CoreのC#はTaskとValueTaskに分かれているわけですが、面倒くせーから全てValueTaskでいーじゃん、というわけにはいきません(なお、私の意見は全部ValueTaskでいいと思ってます、というのも使い分けなんて実アプリ開発でできるわけないから)。そうはいかない一番大きな理由はWhenAllで、このTaskで最も使われる演算子であろうWhenAllは、Taskしか受け取らないので、Taskへの変換が必要になってきます。せっかくValueTaskなのにモッタイナイ。じゃあValueTask用のWhenAllを作ればいいじゃん、というとそれも無理で、Task.WhenAllはTaskのinternalなメソッドに依存して最適化が施されているので、外部からはどうしても非効率的なWhenAllしか作れない仕様になっています(クソですね!)。

が、しかし、そもそもWhenAllってあんま効率的じゃなくないっすか?というのがある。と、いうのも、配列を受け取るAPIでも、まず保守的にコピーしてるんですよね。可変長引数でWhenAll(new[]{ foo, bar, baz })みたいに渡してもコピーされてるとか馬鹿らしい!あと、WhenAllの利用シーンでもう一つ多いのが WhenAll(source.Select(x => x.FooAsync()))のような、元ソース起点に非同期メソッドを呼んで、それを全部待つ、みたいなシチュエーション。なんかねー、別に配列作んなくてもいいじゃん、みたいな気になるんですよね。

と、そこでIValueTaskSourceの出番で、Task(ValueTaskですが)の中身を完全に自分の実装に置き換えることができるようになった、のがIValueTaskSourceです。よって、真に効率的なValueTaskに最適化されたうえで↑のような事情を鑑みたWhenAll作れるじゃん、って。思ったわけですよ。

そこでMagicOnionでは(UniRx.Asyncじゃないのかよって、IValueTaskSourceはUnityの話じゃないですから!)ReservedWhenAllPromiseというカスタムなWhenAllを用意してみました。

var promise = new WhenAllPromise(source.Length);
foreach (var item in source)
{
    promise.Add(item.FooAsync());
}
await promise.AsValueTask();

のように書けます。つまり何かと言うと、WhenAllに必要なのは「個数」で、個数が最初から確定しているなら、それを渡せばいいし、WhenAll自体の駆動に配列は必要ないので、随時Addしてあげてもいいわけです。これで、一切配列を使わない効率的なWhenAllが実装できました。めでたし。

他にも型が異なるTaskをawaitするのにValueTupleで受け取りたい、というのをTask.WhenAllを介さずにその個数に最適化したWhenAllを用意するとか、やりたい放題にめっちゃ最適化できるわけです。

と、いうのも踏まえて、(サーバーサイドC#における)アプリケーションのTaskの定義はValueTaskで統一しちゃっていいと思うし、そのかわりに幾つかの最適化したValueTask用のWhenAllを用意しましょう。というのが良い未来なんじゃないかなー、って思ってます。(このValueTask用のWhenAllのバリエーションはCysharpとして作ったらOSSで公開するので、こちらは期待して待っててください!)

まとめ

UniRx.AsyncナシでUnityにasync/awaitを持ち込んで使いこなすのはかなりの無理ゲーなので、よほどUnity以外で使い込んできた経験がある、とかでなければ、素直に使って頂ければと思います。また、そうでなくてもUnity向けに完全に作り直しているUniTaskの存在価値というのは、スライドのほうで十分理解してもらえてるのではとも思っています。

別にCLRの実装は至高のものだ!ってこたぁ全然なくて、時代とかもあるんで、後の世に作り直されるこたぁ往々にめっちゃある。Microsoftのハイパーエンジニアが練りに練ったものだろうがなんだろうが、永遠に輝き続けるコードなんてあんまなく、時代が経ちゃあどれだけ丁寧に作られたものでも滅びるんです。人間もプログラムも老化には逆らえない(WPFなんて何年前のUIフレームワークなんでしょう!)。というわけで、あんまり脳みそ固くせず、自分の意志で時々に見直して考えてみるといいんじゃないでしょうか。(古の)Microsoftよりも(現代の観点では)私のほうが正しい、とか自信持って言っておきましょう。

さて、UniRx.Asyncは(UniRxも)まだまだ完成しきってるとは言えない、のにドキュメント放置、更新放置で例によって半年ぐらい来てしまったのですが、その間は株式会社Cysharpを設立しましたであったり、MagicOnionのリブートであったり、結構わたわたしてしまったところがありなのですが、ようやく諸々落ち着いてきたので、また腰据えて改善に取り組んでいきたいと思います。まぁドキュメントが全然足りないんですけど(UniRx.Asyncの機能は、かなり膨大なのです……)。

C#的にも、自分でTaskの全域を見つめ直して作り直すという経験を通して得られたものも多かったので、今回の記事もそうですが、Unity関係なくasync/awaitを使っていく上で使える話は色々出せていければというところですね。ではまた次回の更新の時まで!次こそはすぐブログ書きますから!

Memory Management of C# with Unity Native Collections

と、題してECS完全に理解した勉強会で登壇してきました。

Memory Management of C# with Unity Native Collections from Yoshifumi Kawai

ECSは今後力を入れていきたい分野で、LTドリブン開発ということで、登壇するからにはやってこにゃ!という意気込みだったのですが諸々が諸々で色々間に合わずだったので、ややお茶を濁した展開になってしまいました。なむ。それは別として、これ自体は結構いい話なんじゃないかとは思います。

制約には必ず理由があるはずで、UnityやECSが持つ制約(それは時にC#らしくない、という難癖に繋がる)も、その理由をちゃんと紐解けば合理的な判断に見えるはずです。そこを示していきたいな、というのが今回の発表の流れです。時間的都合もあってECS成分が薄くなってしまいましたが、意味や繋がりは分かってもらえたはずです。私はCoreCLRのアプローチもUnityのアプローチも、どっちもいいと思ってるしどっちも面白く感じられているので、両者を見ながらC#の可能性を広げていきたいですね。

まるでC++というか原始時代に回帰してると言えなくもないんですが、表面のレイヤーはmanagedなC#であることに変わりないし、なるべくその表面のレイヤーを増やす努力は続いていると思われます!ただ、一昔前では、そこC++がー、とかそこはランタイムがー、で賄っていた部分がC#で実装するものとして表に出てきたんですね。これ自体はいいことなのですが、故に、使いこなすための知識としては、回帰してます。(Spanはunsafeまみれじゃないぞ、と言いたいかもしれませんが、Unsafe.***はunsafeマークのついてない実質unsafeなので、むしろより悪質です)。

時代は変わっていくし、C#らしさも変わっていくわけなので、そこは「面白く思うこと」が何より大事だし、変わったものには素直に従って深く追求していく姿勢が大事。乗り遅れず、最前線でやっていきましょう!

UniTask - Unity + async/awaitの完全でハイパフォーマンスな統合

Unityでasync/await使えてハッピー。が、しかしまだ大々的に使われだしてはいないようです。理由の一つとして、Unityが標準でサポートする気が全くなさそう。少なくとも、Unityがフレームワークとしてasync/awaitには何一つ対応していない。async/awaitという道具立てだけじゃあ何もできないのです、フレームワークとして何らかのサポートがなければ機能しないわけですが、なんと、何もない……。

何もないことの理由はわからないでもないです。パフォーマンス面で不満/不安もありそうですし、マルチスレッドはC# Job System使ってくれというのは理にかなっている(私もそちらが良いと思います、つまりTaskのマルチスレッドな機能は原則使わない)。とはいえ、async/awaitは便利なので、このまま、便利だけど性能は微妙だから控えようみたいな扱い(あ、それ知ってる、LINQだ)になるのは嫌なのよね。まぁLINQは局所的なので使わないのは簡単なのだけど(実際、最近は私もあまりLINQ書いてないぞ!遅いからね!)、async/awaitは割と上位に伝搬していって汚染気味になるので、そもそも一度どこかで使うと使わない、という選択肢が割と取りづらいので、ならいっそむしろ超究極パフォーマンスのasync/awaitを提供すればそれで全部解決なのである。

という長ったらしい前置きにより、つまり超究極パフォーマンスのUnityのasync/await統合を提供するライブラリを作りました。場所は(面倒くさいので)UniRxに同梱です。というわけでなんと久しぶりにUniRxも更新しました……!(主にReactivePropertyが高速になりました、よかったよかった。PRとかIssueのチェックはこれからやります、いや、まず重い腰を上げたというのが何より大事なのですよ!)

GitHub/UniRx と、アセットストアに既に上がっています。

UniTask

何ができるか、について。

// この名前空間はasync有効化と拡張メソッドの有効化に必須です
using UniRx.Async;
 
// UniTask<T>をasyncの戻り値にできます、これはより軽量なTask<T>の置き換えです
// ゼロ(or 少しの)アロケーションと高速な実行速度を実現する、Unityに最適化された代物です
async UniTask<string> DemoAsync()
{
    // Unityの非同期オブジェクトをそのまま待てる
    var asset = await Resources.LoadAsync<TextAsset>("foo");
 
    // .ConfigureAwaitでプログレスのコールバックを仕込んだりも可能
    await SceneManager.LoadSceneAsync("scene2").ConfigureAwait(new Progress<float>(x => Debug.Log(x)));
 
    // 100フレーム待つなどフレームベースの待機(フレームベースで計算しつつTimeSpanも渡せます)
    // (次の更新でフレーム数での待機はDelayFrameに名前変えます)
    await UniTask.Delay(100); // be careful, arg is not millisecond, is frame count
 
    // yield return WaitForEndOfFrameのような、あるいはObserveOnみたいな
    await UniTask.Yield(PlayerLoopTiming.PostLateUpdate);
 
    // もちろんマルチスレッドで動作する普通のTaskも待てる(ちゃんとメインスレッドに戻ってきます)
    await Task.Run(() => 100);
 
    // IEnumeratorなコルーチンも待てる
    await ToaruCoroutineEnumerator();
 
    // こんなようなUnityWebRequestの非同期Get
    async UniTask<string> GetTextAsync(UnityWebRequest req)
    {
        var op = await req.SendWebRequest();
        return op.downloadHandler.text;
    }
 
    var task1 = GetTextAsync(UnityWebRequest.Get("http://google.com"));
    var task2 = GetTextAsync(UnityWebRequest.Get("http://bing.com"));
    var task3 = GetTextAsync(UnityWebRequest.Get("http://yahoo.com"));
 
    // 並列実行して待機、みたいなのも簡単に書ける。そして戻り値も簡単に受け取れる(これ実際使うと嬉しい)
    var (google, bing, yahoo) = await UniTask.WhenAll(task1, task2, task3);
 
    // タイムアウトも簡単にハンドリング
    await GetTextAsync(UnityWebRequest.Get("http://unity.com")).Timeout(TimeSpan.FromMilliseconds(300));
 
    // 戻り値はUniTask<string>の場合はstringを、他にUniTask(戻り値なし)、UniTaskVoid(Fire and Forget)もあります
    return (asset as TextAsset)?.text ?? throw new InvalidOperationException("Asset not found");
}

提供している機能は多岐にわたるのですが、

  • Unityの非同期オブジェクトをawaitできるように拡張(最速で動くように細心の注意を払って対応させています)
  • コルーチンやUniRxで出来るフレームベースのawaitサポート(Delay, Yield)
  • 戻り値をTupleで受け取れるWhenAll, どれが返ってきたかをindexで受け取れるWhenAny, 便利なTimeout
  • 標準のTaskよりも高速でアロケーションの少ないUniTask[T], UniTask, UniTaskVoid

となっています。で、何が出来るのかと言うと、ようはコルーチンの完全な置き換えが可能です。async/awaitがあります、っていう道具立てだけだと、何もかもが足りないんですね。ちゃんと機能するようにフレームワーク側でサポートさせてあげるのは必須なのですが、前述の理由(?)どおり、Unityはサポートする気が1ミリもなさそうなので、代わりに必要だと思える全てを提供しました。

Taskを投げ捨てよ

目の付け所がいかれているので、Taskを投げ捨てることにしました。Taskってなんなの?というと、asyncにする場合戻り値がTaskで強要される、という型。そして究極パフォーマンスの実現として、このTaskがそもそも邪魔。なんでかっていうと、歴史的経緯によりそもそもTaskは図体がデカいのです。異様に高機能なのは(TaskSchedulerがどうだのLongRunningがどうだの)、ただたんなる名残(或いは負の遺産)でしかない。アドホックな対応を繰り返すことにより(言語/.NET Frameworkのバージョンアップの度に)コードパス的に小さくはなっていったのですが(async/awaitするためだけには不要な機能がてんこ盛りなのだ!)、もういっそ全部いらねーよ、という気にはなる。

そこでC# 7.0です。C# 7.0からasyncの戻り値を任意の型に変更することが可能になりました。詳しくは言語仕様のAsync Task Types in C#に書いてありますが、Builderを実装することにより、なんとかなります。

というわけで、UniRx.Asyncでは軽量のTaskであるUniTaskと、そのためのBuilderを完全自前実装して、Unityに最適化されたasync/awaitを実現しました。

代わりにC# 7.0が必須のため、現状ではIncremental Compilerを導入する必要があります(現状のUnity 2017/2018はC# 6.0のため)

Incremental Compilerではなくても、恐らくUnity 2018の近いバージョンではC#のバージョン上がりそうな気配なので、先取りするのは悪くないでしょう。

PlayerLoop

UniRx.AsyncはUniRxに依存していません。そのため、GitHubのreleasesページではUniRxを含まないパッケージも提供しています。併せて使ったほうがお得なのは事実ですが、なしでも十分に機能します。

さて、UniRxではMainThreadDispatcherというシングルトンのMonoBehaviourにMicroCoroutine(というイテレータを中央管理するもの)を駆動してもらっていましたが、今回スタンドアロンで動作させるため、別の手段を取りました。それがPlayerLoopです(詳しくはテラシュールブログの解説が分かりやすい)。

これをベースにUpdateループをフックして、await側に戻す処理を仕掛けています。

Multithreading

掲げたのはNo Task, No SynchronizationContext。何故かというと、そもそもUnityの非同期って、C++のエンジン側で駆動されていて、C#のスクリプティングレイヤーに戻ってくる際には既にメインスレッドで動くんですよね。例えば AsyncOperation.completed += action とか。コルーチンのyield retunもそうですね、PlayerLoop側で処理されている。ようするに、本来SynchronizationContextすら不要なのです、全てメインスレッドで動作するので。

通常のC#はスレッドベースで、Windows FormsやWPF, ASP.NETなど諸々の事情を吸収するために存在していたわけですが、Unityだけで考えるなら完全に不要です。他のものにはないフレーム毎に駆動することと、本体がC#ではなくC++側にあるということが大きな大きな違いです。async/awaitやTask自体は汎用的にする必要があるため、それらの吸収層が必要(SynchronizationContext)なわけですが、当然ながらオーバーヘッドなので、取り除けるなら取り除いたほうが良いでしょう。そのために、UniTaskの独自実装も含めて、全てのコードパスを慎重に検討し、不要なものを消し去りました。

UniTaskはどちらかというとJavaScript的(シングルスレッドのための非同期の入れ物)に近いです。Taskは、そうした非同期の入れ物に加えてマルチスレッドのためなどなど、とにかく色々なものが詰まりすぎていて、あまりよろしくはない。非同期とマルチスレッドは違います。明確に分けたほうが良いでしょうし、UnityではC# JobSystemを使ったほうが良いので、カジュアルな用途以外(まぁラクですからね)ではマルチスレッドとしてのTaskの出番は少なくなるでしょう。

嬉しいこととして、スレッドを使わないのでWebGLでもasync/awaitが完全に動作します。

Rx vs Coroutine vs async/await

もう結論が出ていて、async/await一本でOK、です。まずRxには複数の側面があって、代表的にはイベントと非同期。そのうち非同期はasync/awaitのほうがハンドリングが用意です。そしてコルーチンによるフレームベースの処理に関してはUniTask.DelayやYieldが解決しました。ので、コルーチン→出番減る, async/await → 非同期, Rx → イベント処理 というように分離されていくと思われます。

C# Standard vs Unity

正直なところ私は別にUnityがC#スタンダードに添わなくてもいいと思ってるんですよね。繰り返しましが、Unityの本体はC++の実行エンジンのほうで、C#はスクリプティングレイヤーなので。C#側が主張するよりも、C++に寄り添うことを第一に考えたほうが、よい結果がもたらされると思っています。よりC#に、というならPure C#ゲームエンジンでないとならないですが、商業的にはほぼ全滅であることを考えると、Unityぐらいの按配が実際ちょうどいいのだろうな、と。理想もいいんですが、ビジネスとしての成功がないと全く意味がないので。

と、いうわけで、C# JobSystemは大歓迎だしBurst Compilerは最高 of 最高なわけですが(そしてECSなんてそもそもオブジェクト指向ですらなくなる)、さて、Task。UniTaskの有用性や存在意義については、よくわかってもらえたと思います!そのうえで、それを分かったうえでもノンスタンダードな選択を取るべきなのか論は、それ自体は発生して然りです。

まぁ、まずUnityだとそもそもC# 7.0が来たら片っ端からValueTask(という、TとTaskのユニオンがC# 7.0から追加された)に置き換え祭りは発生するでしょう。実際async祭りで組むと、「同期で動くTask」がどうしても多く発生してしまい、無駄なアロケーション感半端ないので、ValueTask主体のほうがよい。

更にその上で.NET Core 2.1ではValueTaskにIValueTaskSourceという仕掛けが用意されて、これは何かと言うと、やっぱりasync/awaitの駆動においてTaskを無視するための仕組みです(現状はSystem.IO.Pipelinesというこれまたつい先週ぐらいに出た機能のみ対応)。そう、別にUnityだけじゃなくて通常の.NETでもTaskはオーバーヘッドと認識されているのだ……。

つまりなんというか、そう、そもそもC#本流ですら割と迷走しているのだ……。存在すると思っているStandardなんてもはやないのだ……。てわけで、別にUniTask、いいんじゃない?とか思ってしまいますがどうでしょう。どうでしょうね、それはさすがにポジショントークすぎにしても。

ようはポリシーとして、asyncで宣言した際に、TaskにするかValueTaskにするかUniTaskにするかを迫られます。逆に言えばそれだけです。あれ、意外と人畜無害。そう、意外と人畜無害なのです。よし、なら、とりまやってみるか。いいんじゃないかな?別に最悪、一括置換で戻したり進めたり割と容易なので。あと、ちなみに、UniTaskがUnityでデファクトスタンダードになれば、尚更迷う必要性はなくなるので、むしろ是非みんなでデファクトスタンダードまで持っていきましょう:)

まとめ

非同期革命の幕開け!そもそもこれぐらいやらないと世論は動かない、というのもあるので、フルセットでどーんと凄い(っぽい)(実際凄い)のを掲示することにはめちゃくちゃ意味があります。UniTaskが流行っても流行らなくても、この掲示にはめちゃくちゃ意味があるでしょう。UniRx.Asyncが何を実現したかを理解することは非常に重要です、教科書に出ますよ!

それと、UniRx全然更新していなくてごめんなさい、があります。ごめんなさい。今回、ReactivePropertyのパフォーマンス向上を(ようやく)入れたり、今後はちゃんと面倒みていくのでまたよろしくおねがいします。

Open Collective/UniRxというところで寄付/スポンサー募集もはじめたので、よければ個人/企業で入れてくれると嬉しいですね……!今ならUniRxのGitHubページのファーストビューにロゴが出るので、特に企業などはアピールポイントです……!

C#(.NET, .NET Core, Unity, Xamarin)用の新しい高速なMessagePack実装

と、いうものを作りました。MessagePackのC#版です。以前に作ったZeroFormatterのコードをベースに、バイナリの読み書きをMsgPackのフォーマットに差し替えたものになります。MsgPackのライブラリはすでにあるじゃん(MsgPack-Cli)!ってことなんですが、パフォーマンスにかなり差があります。

JSON.NET(スタンダードで、豊富なAPIを持ってる)に対するJil(スピード特化、APIは必要十分はあるけれどJSON.NETほどではない)のようなものと思ってください。とはいえ、生のまま使っても問題は出ない(デフォルトのままで最高速が出るようにチューニングしてある)でしょうし、カスタマイズの口自体も十分用意してあります!詳しくは「拡張」の項で説明しますが、既に私自身が他のライブラリへの対応・インメモリデータベースの内部構造・RPCのシリアライゼーションフォーマットとして応用アプリケーションを作りまくっていて、それの要求に十分応えられるだけの拡張性があります。

今回のコードは、未来のアーキテクチャで実装された、C#のシリアライザ設計を一歩前進させる、隙のない代物になっています。というのは大げさでもなく、現代最先端のC#の設計技術を投下してあるので、世代的に今までのものとは、一つ二つ先を行ってます。C#でJSON以外のフォーマットのシリアライザを使おうと考えたら、もうこれ一択で悩まなくていいですよ。いや、ZeroFormatterとは悩んでください。

そう、ZeroFormatterは?というと、性能特性にクセがあるので、汎用フォーマットとしてはMsgPackのほうがずっと使いやすい、ですね。もちろん、無限大高速な性質はハマるシチュエーションではすごくハマると思いますよ!別にオワコンじゃないです!しかし、FlatBuffersが主流にはならないのと同じように、ハマるシチュエーションをきちんと考えたほうが良いかな、といったところはやっぱあります。使い勝手は工夫しましたが、どうしても、これ系のバイナリ形式そのもののクセは存在しちゃうので。

ところで、詳しくは圧縮の項で説明しますが、LZ4を内蔵したことにより、パフォーマンスを比較的維持したまま、更にファイルサイズを縮めることを可能にしています。これは、ただたんに出来上がったものを上からLZ4で圧縮しているのではなくて、MessagePack + LZ4のパイプラインを一体化して、LZ4のネイティブAPIを効率よく叩くことによって実現しています。また、lz4自体のオプションもシリアライザと併用して使うのに最適になるように調整してあります(コードもメモリプールを使って圧縮のために使う辞書のアロケーションをなくしたりなどの改造を入れてる)

Unity向けには、更にunsafeな拡張をONにしるとVector3[](など)のシリアライズがJsonUtilityの20倍高速化される拡張機能なども設けてます。これは超強力で、Meshなどの巨大データや大量の位置データのやり取りなどに役立つはずです。C#マジおせーからC++で書こうぜ、に最後の最後はなるにしても、それまでの遊び幅は大幅に拡張されるでしょう。

使いかた

Unity版はサイトのReleasesページから、.NETはNuGet経由で入れてもらうのがいいでしょふ。

APIのノリは完全に一緒で、静的関数のSerializeかDeserializeを呼ぶだけです。ただし対象クラスへの特別なマークが必要です。

// 属性をつけるのは「必須」です、これは堅牢性を高めるためです
[MessagePackObject]
public class MyClass
{
    // Keyは配列のindexとして扱います、これはバージョニングで重要です
    // Key名はIntかStringが選べて、Intの場合はArrayで、Stringの場合はMapでシリアライズされます
    [Key(0)]
    public int Age { get; set; }
 
    [Key(1)]
    public string FirstName { get; set; }
 
    [Key(2)]
    public string LastName { get; set; }
 
    // publicメンバーで不要なフィールドは明示的に[IgnoreMember]を付与する必要があります
    [IgnoreMember]
    public string FullName { get { return FirstName + LastName; } }
}
 
class Program
{
    static void Main(string[] args)
    {
        var mc = new MyClass
        {
            Age = 99,
            FirstName = "hoge",
            LastName = "huga",
        };
 
        // 基本的に Serialize/Deserialize を呼ぶだけの直感的で単純なAPIが全てです
        var bytes = MessagePackSerializer.Serialize(mc);
        var mc2 = MessagePackSerializer.Deserialize<MyClass>(bytes);
 
        // ToJsonメソッドによってバイナリを簡単に読みやすいJSON文字列に変換できます
        // これはデバッグ用途などで非常に役に立つでしょう!
        var json = MessagePackSerializer.ToJson(bytes);
        Console.WriteLine(json); // [99,"hoge","huga"]
    }
}

属性をつけるのが「必須」なのは煩わしいところですが、これは堅牢性を高めるためです。MsgPack-Cliとの機能面での最大の差はオブジェクトシリアライズの扱いで、MsgPack-CliはデフォルトでArray、かつ、何もマークしていないものもシリアライズ可能です。これは、プロパティが増えた時の挙動(バージョニング)が極めて危険で、全くよろしくない。そのため、そもそも必須扱いにしてプログラム実行時の限りなく早いタイミングで気づけるようにしています。

かわりにこの煩わしさは、Visual StudioのAnalyzerによってある程度緩和できるようにしています。

また、気楽にやりたい場合は、[MessagePackContract(keyAsPropertyName = true)]にすると、プロパティへの属性付けは不要で、プロパティ名をキーとして扱いMap形式でシリアライズします。JSONライクで手軽ですが、シリアライズ/デシリアライズにかかる時間と、バイナリサイズは肥大化します。ただしKeyに名前がついてるとデバッグ時の楽さはあがるのと、遅くなるといっても依然高速なので、「アリ」な選択ではあるでしょう。

後述しますが引数にFormatterResolverを渡すことによってシリアライザの挙動がカスタマイズできて、標準で用意している ContractlessStandardResolver を渡すと(あるいはSetDefaultResolverでデフォルト挙動を差し替えることも可能)、[MessagePackObject]属性の付与も不要になります。

MessagePackSerializer.Serialize(mc, MessagePack.Resolvers.ContractlessStandardResolver.Instance);

この場合もキー名を文字列としてMapでシリアライズします。Mapを使うので、バージョニングに対する不安もありません。このオプションを合わせた場合が、最もお気楽に使える、 JSON.NETとの互換性というか使用感は変わらない感じになるんじゃないかと思います。また、この場合は匿名型もシリアライズできます(デシリアライズはできない)。

と、色々ありますが、お薦めは明示的にMessagePackObjectをつけて、KeyをIntにすることです。ようするにデフォルトのままが最も最高の効率で最もお薦め、ということです!まぁContractlessStandardResolverも悪くはないです、特に後述するLZ4圧縮と組み合わせれば配列など気になるデッカいデータを処理する時にはきちんとキーを縮められるので、全然良いかなとは。

パフォーマンス/最適化

細かい機能は置いといて、まずパフォーマンスについて詳しく見ていきましょう!

オールスターで並べてみました。小さくて見えませんね、もう少し大きい図はGitHubのページにあるのでそちらを。とりあえず最強に速いです、ということで。

どんなケースが来ても、まぁ、速いデス。圧倒的に。で、速い理由というか他が遅い理由は無限大に説明できるんで、まぁいいでしょう。基本的にはZeroFormatterで行ったことがそのままあてはまってますが、それに加えてMessagePackの仕様に対する最適化と、ZeroFormatterよりも効率的なIL生成によって、なんか結果ZeroFormatterより速くなってしまってなんともかんとも……。

・一切無駄なオブジェクトを生成しない、最終的なbyte[]以外のアロケーションは一切なし
・シリアライズ時のbyte[]の拡張が必要な場合も、64K以下は効率的に内蔵の作業用メモリプールを使うためアロケーションなし
・Streamベースではなくbyte[]ベースのプリミティブAPIにより、Stream抽象による呼び出しオーバーヘッドを削減
・シリアライザのキャッシュ/ルックアップにジェネリクス型変数からの取り出しによるDictionary呼び出しコストを削減
・効率的なメモリプールの使用による作業領域のメモリ拡張の削減
・デリゲート経由ではなく直接、型をIL生成することによる余分な呼び出しコストの削減
・ILコード生成時にプリミティブに対する書き込み/読み込みは、プリミティブAPIを直接呼び出すコード生成によりメソッド呼び出しコスト削減
・ILコード生成時にMsgPackの固定範囲に収まっているキーは範囲分岐判定せず直接呼び出すコードを埋め込み
・コレクションのイテレートをIEnumerable抽象で扱わず、各コレクションそれぞれに対し個別に最適化
・プリミティブ配列に関しては更にジェネリクスも使わず各プリミティブ配列専用のビルトインシリアライザを用意
・ルックアップテーブル事前生成によるデシリアライズ時のタイプ判定コードを削除
・文字列など長さが必要な可変フォーマットに対するヒューリスティックな長さ判定によるコピーコスト削減
・全コードパスがジェネリクスで貫通していてボクシング一切なし
・IL生成ができない環境ではソースコード解析からの事前コード生成による対応

頭からつま先までギッチシと最適化してあるんで、これ以上の速いシリアライザを書くことは不可能でしょう。ってZeroFormatterの時にも言った気がするので説得力が微妙になくなってますが、今度の今度こそもうやれることは絶対にない、というレベルでありとあらゆる設計と技法を突っ込んだので、これがC#の性能限界でしょう、しかも今回はunsafeではなくてsafeなのです!(LZ4, Unityのunsafe拡張を除く)。unsafeがなくてもC#は速いんです。はい。これはMsgPackがBigEndianなのでunsafe使ってもうまみがあんまないから、非unsafeに倒してみたってところですんが。

IL生成がより効率的になったのは、ZeroFormatter以降に何故かILを書きまくる羽目になったせいか、私自身のIL書き能力が向上したことによる余裕によって、結構アグレッシブに生成時分岐で最適なコードを直接埋め込んでみたからです。やっただけ効果は出ますねえ、やはり。なるほど。

コレクションのイテレートに関しては、さすがに数多いので抽象化はしてるんですが、こんなジェネリクス型を用意しました。

public abstract class CollectionFormatterBase<TElement, TIntermediate, TEnumerator, TCollection> : IMessagePackFormatter<TCollection>
    where TCollection : IEnumerable<TElement>
    where TEnumerator : IEnumerator<TElement>

微妙に奇々怪々な内容になっていますが、これが最も速いコレクションのシリアライズ/デシリアライズをするために必要な抽象なのです。例えば、これなら各コレクション専用のstruct enumeratorを使うことができます。ただたんにIEnumerable<T>をforeachするだけじゃ遅くてやってられないのですよ。

というような細かいハックは沢山入ってるんですが、とはいえ基本的にはStreamを捨ててbyte[]ベースにしたというのが大きいですね。byte[]ベースなのストリーミングでのシリアライズ/デシリアライズができないのですが、例えば巨大配列のケースではプリミティブAPIと小シリアライザを使って対処するとか、逃げ口はそれなりに用意されてるので、超絶巨大な一個のオブジェクト、みたいなシチュエーションじゃなきゃ大概なんとかなるものです。

System.IO.Pipelinesが出たら、Pipelines版作ってもいいかな、とは思いますが。しかし、そっちがあればbyte[]版とかイラネー?っていうと、実際のところそんなこたぁなくて、In/Outがbyte[]で確定してる状況では、byte[]版のほうが良いでしょうね。System.IO.Pipelinesで作るとストリーミングでシリアライズ/デシリアライズできるので、その点は良くなると思うんですが、利用するフレームワークの口が大抵はbyteで空いてるんで、ほとんどのシチュエーションでbyte[]版のほうが良好ってことになりそうだとは思ってます。ので、別にそんな優先度も希望も高くは持ってません。XxxAsyncみたいな非同期APIも同じような話が言えて、細切れでawaitかけるような中身になってると、むしろ相当遅くなってしまいます。基本的にはガリッとバッファ確保してガッと書いて、ガッとFlushにしないとダメなのですよ。なので、まぁPipelines版は別ですが、ふつーの形で非同期APIを作る意味は全くないと思ってるんで、それはナシです。むしろそういうのがあると、そっちのほうが良いのかな、とユーザーに思わせてしまうのでAPI設計的に非常によろしくない。

ファイルサイズと圧縮

MessagePackのイケてるところは、型の表現力が非常に高いのに、バイナリサイズが小さくなるところ。一般的にオブジェクトへのシリアライズにはArrayフォーマットが使われて、これはProtoなどのTagで1バイト使用するより小さくなる。もちろん、Arrayを使うことはバージョニングに問題を抱えていないこともないですが、概ねNil埋めで大丈夫な範囲に収まるので許容できるのではないかと考えています。

が、それと圧縮は別問題で、やっぱ圧縮は圧縮で、かけると非常に縮むんですよね。でも当然圧縮は別途パフォーマンスロスを抱えてしまうわけで、と、そこでMessagePack for C#は最速を誇るlz4での圧縮を標準でサポートしました。LZ4は圧縮率はそこそこですが、圧縮/伸張が速い(特に伸張がヤバいぐらい速い)という特徴があります。これはMessagePackのユースケースにかなりハマるんじゃないでしょうか(圧縮率が重要なシチュエーションでは、lz4と同作者のZStandardというものがあって、これもバランス良くて素晴らしい)。

// 基本的に MessagePackSerializer のかわりに LZ4MessagePackSerialzier を呼ぶだけ
var bytes = LZ4MessagePackSerialzier.Serialize(mc);
var mc2 = LZ4MessagePackSerialzier.Deserialize<MyClass>(bytes);
 
// ToJsonメソッドによってバイナリを簡単に読みやすいJSON文字列に変換できます
// これはデバッグ用途などで非常に役に立つでしょう!
var json = LZ4MessagePackSerialzier.ToJson(bytes);
Console.WriteLine(json); // [99,"hoge","huga"]

んで、とにかく速い。ほとんど変わらないだけの圧縮/伸張速度なのにファイルサイズは激縮み!ただし、一応言っておくと圧縮はデータの内容によって全く効かないこともあれば、重複だらけデータなら効果はてきめんになったり(だからJSON+GZipで配列縮めると大量の同じような文字列キーが縮んでほぼ無視できるようになる)ということがあります。この試験データは重複多めなので、圧縮が効きやすいうえに効率も良いのでめっちゃ縮んでいるだけです。処理時間も複雑なデータであれば、このデータのようにあんま変わらない、などということはなく2倍ぐらいの差になるケースも出てきます(それでも他のシリアライザを単独で使うより速いというのが驚異的な話なのですが!)。この辺は相性とかモノ次第って面もありますが、実際リアルなデータ(現在開発中のゲーム)での色々寄せ集めて集合させた5Mぐらいのデータは800KBになりました、速度的にはx1.5がけぐらい。全然割に合います。

で、このLZ4圧縮はMsgPackで出来上がったデータに対して上からLZ4をかけてるわけではありません。まず、これ自体が正しいMsgPackデータになってます(なので他のMessagePackシリアライザにそのまま渡しても認識はできる、デシリアライズはできませんが、正しく実装されたシリアライザなら少なくとも(Bodyはbinaryですが)Dumpは可能)。MsgPackの仕様のExt領域を使って(TypeCode:99)、LZ4圧縮によるMsgPackという形でシリアライズしています。

なんでかというと、そもそもLZ4がbyte[]ブロックベースで動作する圧縮フォーマットなのです。(C#の)Streamとして使えるベンリAPIがあったりしますが、それはただのラッパーで、むしろかなり速度低下させる一因です。黙ってbyte[]ベースの最もプリミティブなLZ4のAPIを叩く。それが最高に速い。そして、つまりこれって今のMessagePack for C#の実装とめっちゃ相性が良い、こっちもbyte[]ベースですから。相性が良いのは良いとして、ただたんに左から右に流すだけだと、無駄なbyte[]コピーが発生しちゃうんですよね(最終サイズのbyte[]にリサイズするコストとかがどうしてもある)。どうせLZ4通すなら、別にその時点はただの中間地点なので、リサイズする必要はないんで、当然ノーリサイズでそのまま流す。リサイズするのはLZ4通した本当の最後の最後だけ。

それとLZ4の生デコンプレスAPIは、「復元後(圧縮前)のサイズを知っている」ことで、より効率的にデコンプレスできるようになっています。が、LZ4自身には復元後のサイズは埋まってません。なるほど。なるほど。なのでふつーに左から右に流すだけ圧縮だと、真の意味で効率的な復元は実現できません。そこでExt領域を使っている理由がでてきて、MessagePack for C#のLZ4統合では、復元後のサイズを先頭に埋め込んであります。それを使うことにより、真の最高速でのLZ4によるデコンプレスを実現してます。

なお、独断と偏見により64バイト以下はLZ4として圧縮せず素通しするようにしています。なので頻繁に送受信する軽量なデータは圧縮/伸張によるパフォーマンスの影響を一切受けません。これもExt領域を使った意味があって、素通しでもLZ4でも、そのまんまMsgPackとして扱えるんですね。どちらもValidなMsgPackなので、きっちり正しくクライアント側でハンドリングできるようになりました。

シリアライザの選択に悩まないと言いましたが、MessagePackSerializerを使うかLZ4MessagePackSerializerを使うかは、悩みますねえー。

イミュータブルオブジェクトへのデシリアライズ

デシリアライズ処理には通常publicなsetterを要求しますが、MessagePack C#はイミュータブルオブジェクトへのデシリアライズを可能にしています。これが出来ると、

[MessagePackObject]
public class Point
{
    [Key(0)]
    public int X { get; }
    [Key(1)]
    public int Y { get; }
 
    public Point((int, int) p)
    {
        this.X = p.Item1;
        this.Y = p.Item2;
    }
 
    [SerializationConstructor]
    public Point(int x, int y)
    {
        this.X = x;
        this.Y = y;
    }
}

KeyがIntの場合は引数の位置で、Stringの場合は名前(大文字小文字無視)でマッチさせます。ある程度「気を利かせてくれる」とかではなく、明確に仕様として設け、コンフィグの口を持っているところは目新しいんじゃないかと。そして、これ、実際便利です。

Union

Union(インターフェイスのシリアライズ/ポリモーフィズム)は2要素の配列として表現しています。一つ目が識別キー。二つ目が中身。

// mark inheritance types
[MessagePack.Union(0, typeof(FooClass))]
[MessagePack.Union(1, typeof(BarClass))]
public interface IUnionSample
{
}
 
[MessagePackObject]
public class FooClass : IUnionSample
{
    [Key(0)]
    public int XYZ { get; set; }
}
 
[MessagePackObject]
public class BarClass : IUnionSample
{
    [Key(0)]
    public string OPQ { get; set; }
}
 
// ---
 
IUnionSample data = new FooClass() { XYZ = 999 };
 
// serialize interface.
var bin = MessagePackSerializer.Serialize(data);
 
// deserialize interface.
var reData = MessagePackSerializer.Deserialize<IUnionSample>(bin);
 
// use type-switch of C# 7.0
switch (reData)
{
    case FooClass x:
        Console.WriteLine(x.XYZ);
        break;
    case BarClass x:
        Console.WriteLine(x.OPQ);
        break;
    default:
        break;
}

これ、C# 7.0の型でswitchできるのと相性良いんですよね。便利で良くなったと思います。

拡張

今回、デフォルトでやたら拡張パッケージがあります。

Install-Package MessagePack.ImmutableCollection
Install-Package MessagePack.ReactiveProperty
Install-Package MessagePack.UnityShims
Install-Package MessagePack.AspNetCoreMvcFormatter

ImmutableCollectionやReactivePropertyをシリアライズ可能にするやつ。UnityShimsはUnityと相互通信する際のVector3とかとそのシリアライザ。AspNetCoreMvcFormatterはASP.NET Core MVC用のシリアライザ換装するやつです。

拡張を有効にする場合は、Resolverというものを使っていきます。こんな感じで。

// set extensions to default resolver.
MessagePack.Resolvers.CompositeResolver.RegisterAndSetAsDefault(
    // enable extension packages first
    ImmutableCollectionResolver.Instance,
    ReactivePropertyResolver.Instance,
    MessagePack.Unity.Extension.UnityBlitResolver.Instance,
    MessagePack.Unity.UnityResolver.Instance,
 
    // finaly use standard(default) resolver
    StandardResolver.Instance);
);

この辺のは細かい使い方といったところなので、ReadMeを見てもらえれば、なのですが、MessagePack for C#ではコンフィグ/拡張ポイントをResolverに寄せているので、これの仕組みさえ理解してもらえれば全ての拡張の方法がわかります!逆に、これがちょっと初見だとむつかしめなので、もう少し優しい何かも用意したい気もしなくはないですが、多分、このままでいいんじゃないかな、とも思ってます。

for Unity

今回はZeroFormatterと違って、コードジェネレート不要です!なんですと!!!きっちりとUnityでちゃんと動作するILGenerationによって、ふつーの.NET版と変わらない動的コード生成/パフォーマンスでUnityでも動きます。IL2CPPじゃなければ。IL2CPPじゃなければ。PCでもAndroidでもどんとこい、なんですが、IL2CPPはダメです。IL2CPPの場合は、やっぱりコードジェネレートしてください、今回もコードジェネレーター同梱してあります(そして未だにWindowsでしか動作しません、なんとかしたい……)

更に今回はunsafeじゃありません!ほとんどのコードがsafeで動いてるのでソースコードべた配布。やったね。unsafe使わなくても結構速く出来るんですよ。とはいえ、LZ4がunsafeバリバリなので、LZ4使いたい場合はunsafeを有効にしてください。詳しいことはReadMeで。

ついでにunsafe時のスペシャルフィーチャーとして、エクストリーム高速なVector3[]シリアライザをUnity用に特別に用意しました。

JsonUtilityの20倍速い。これならMeshとかの大量の頂点を扱うものでも、そこそこなんとか戦えるんじゃないでしょうか。それ以上頑張りたかったらC++で、ですけれど、C#でもここまでなら頑張れる……!

なんで速いかというと、structの配列はメモリ上に一列に並ぶというC#の特性を利用して、まるっとそのままコピーしてるからです。Oh……。まぁ、アリでしょう。アリでしょう。なお、さすがにこれは正規のMessagePackの配列じゃなくなる(純粋なバイト列)ので、拡張フォーマットとしてマークして押し込んでます。MessagePackはこれが便利……なんか特化したの突っ込んでも仕様的にValidだと言い張れる。てわけで、アリでしょう。アリ。最高にクールな機能だと思ってます。

MsgPack-Cliとの互換性

あまり考えてない&こちらからサポートする気はあんまナイデス。互換性は基本的にあるんですが、微妙にありません!多分、普通に使ってる場合は非互換になります。C#の型をMsgPackとしてどう表現するか、というところで差異が出ちゃうんで、しょーがない。

Enumのシリアライズ/デシリアライズが、MessagePack for C#ではデフォルトはIntegerになります。文字列でのシリアライズ/デシリアライズのサポートは、Enumを文字列で扱うと明らかに遅くなるのでやる気nothing、と思ってたんですがまさかの1.0.0を投げた直後に要望が来たのでしょうがなく追加で入れることになったのであった。1.0.1スタートの理由、おうふおうふ。というわけでResolverを差し替えることによってEnumを文字列で扱う対応はできます。よかったね。なお、MsgPack-Cliは文字列になるほうがデフォです。なのでデフォのままだと、ここで互換性なくなります。

DateTimeの形式が互換性ありません。MessagePack for C#ではProposalで提唱されているTimestamp拡張を実装しています(ほぼほぼファイナルなんだと思うけど一向にマージされないので、早まったかな、どうなんだろう……)。これもResolverを自前で書けば解決可能なので適当にどうぞ。

あとはdecimalとかGuid辺りの扱いもちょっと違いますがResolverを自前で(以下略)

多言語間での通信

C#独自の型になると、なんというかよしなにハンドリングしてください状態になってしまうんですが、基本型だけ使ってる分には概ね大丈夫でしょう。ただしDateTimeだけは↑に書いたように、特殊なハンドリングしてるんで他の言語のサポート状況次第です。不安なら文字列にして送ったりUnixTimestampにして送ったりすればいいんじゃないでしょーか。DateTimeが互換の問題になるのは別にMsgPackに限らず、JSONでもよくあることですねー。故に標準で型としてサポートして欲しいし、↑のTimestamp拡張がAcceptされるのを待ち望んでいます。

あとは、オブジェクトはIntがキーのArrayかStringがキーのMapのどちらかです、ってことですね。これは他の言語も概ねその二択なので、問題なく相互変換できると思っています。

Protobufとの比較

Protocol Buffersと比較すると、MsgPackはダンプ耐性があるのが好みです。自己記述的で、スキーマと照らし合わせなくても良いため、デバッグとかで何かと捗ります(MessagePack for C#についてるJSONへのダンプ機能は超嬉しいはず、ていうか私が超嬉しい)。また、nullの扱いが明確なのも嬉しいところで、Protobufはそれがかなりのハマりどころで、色々と詰むんですが、MsgPackは完全にC#をシリアライズ/デシリアライズしても自然のまま扱えます。どういうことかというとこういうことです。

[ProtoContract]
public class Parent
{
    [ProtoMember(1)]
    public int Primitive { get; set; }
    [ProtoMember(2)]
    public Child Prop { get; set; }
    [ProtoMember(3)]
    public int[] Array { get; set; }
}
 
[ProtoContract]
public class Child
{
    [ProtoMember(1)]
    public int Number { get; set; }
}
 
using (var ms = new MemoryStream())
{
    // nullをシリアライズすると
    ProtoBuf.Serializer.Serialize<Parent>(ms, null);
 
    ms.Position = 0;
    var result = ProtoBuf.Serializer.Deserialize<Parent>(ms);
 
    // なんとデシリアライズするとstructのように0埋めされたものになってデシリアライズする!これはヤバい。
    Console.WriteLine(result != null); // True
    Console.WriteLine(result.Primitive); // 0
    Console.WriteLine(result.Prop); // null
    Console.WriteLine(result.Array); // null
}
 
using (var ms = new MemoryStream())
{
    // 空配列をシリアライズする
    ProtoBuf.Serializer.Serialize<Parent>(ms, new Parent { Array = new int[0] });
 
    ms.Position = 0;
    var result = ProtoBuf.Serializer.Deserialize<Parent>(ms);
 
    // nullになって帰ってくる!なんじゃそりゃ、マジでヤバい。
    Console.WriteLine(result.Array == null); // True, null!
}

protobuf-netの問題というか、protobuf自体の型表現力的にしょーがないんですねー、protobufの表現力は実はかなり弱いのです……。なので、protobufを.protoからの生成じゃなく使う、つまり普通の汎用シリアライゼーションフォーマットとして使うのは激しくお薦めしません。実運用に入ると間違いなく問題になるはずです(というか実際グラニでは激しく問題になった!もう二度とprotobuf-netは使わん!)

かわりに、protobufはIDLやそのRPCフレームワークであるgRPCが強力で、多言語間での通信仕様として使うには、圧倒的に秀でていると思います。gRPCは最高ですよ。MsgPackはオブジェクトシリアライズの統一的仕様が存在しないので、言語間での通信仕様としては正直、かなり厳しいと思いますね。いや、別にJSONのように手で調整するなら構わないし、It’s like JSONってのはそういうことだろっていうとそういうことなんですが、話が違うのはいかんせんバイナリだということ。JSONはテキストなので目で見て調整できたり、暗黙的にObjectはStringがKeyのMapですよね、で統一されてるんですが、MsgPackはバイナリなので調整辛いし、オブジェクトがArrayなのかMapなのかも統一感なかったりで、ちょっとショッパイと言わざるをえないです。

なので、gRPCとか言語超えたRPCではProtobufが圧倒的に優勢で、これは未来永劫変わらないでしょう。MsgPack-RPCやMsgPack-IDLはコケた、といっても過言ではないし、別に蘇ることもないと思うんで。

しかしバイナリ仕様としては非常に優れてるし、Dump可能なところも嬉しすぎるので、多言語間通信「以外」での局面では、最高のフォーマットだと思います。多言語間通信においても自社内とかの閉じたところなら調整はやりやすいので、決してダメというわけでもない、でしょうが、まぁそういう場合はIDL欲しくなるのがフツーなので、訴求力は弱くなっちゃてるでしょうねえ、現状で既に(MsgPackを「選ばない」理由としては至極真っ当だと思います)。RPCを捨てて、JSON-Schema的な純粋な仕様定義を再展開すればあるいは?とは、やっぱあんま思わないんで、ここはしゃーなしで諦めたほうがいいかしらん、外野の意見では。

MessagePack-RPC/gRPC

と、言っておきながらなんですが、MessagePack for C#を使ったRPCを作っています。MagicOnion - gRPC based HTTP/2 RPC Streaming Framework for .NET, .NET Core and Unity. ということで、通常gRPCはprotobufで通信するんですが、そのシリアライゼーションレイヤーをMessagePackに置き換えてます。なんでかっていうと、それによってIDL不要でRPCできるようにしてからです。IDLを使わない局面ではMsgPackは上で言った通り最高のフォーマットなので。

MagicOnionの特徴は、IDLを使わなくても、型安全で通信のスキーマがかっちり決まった状態になることです。何故か、というと、C#そのものがスキーマとして動くので。MagicOnionは Server C# - Client C# の通信フレームワークになっていて、多言語ではなく同言語間に限定することによって、MsgPackのウィークポイントを塞ぎつつ、素のgRPCよりも、よりC#の特色を活かした強力な機能と書きやすさを付与しています。パフォーマンスも文句なく良い、むしろ素のgRPCよりも良い(シリアライザの性能差で)

まだ開発中なので、今後に乞うご期待:) 実際にUnityで開発中のゲームはこのフレームワークを使ったものになっています。HTTP/1 APIは完全消滅。中々アグレッシブです。

まとめ

ZeroFormatterよりもResolver回り(拡張/オプション)のAPIが大幅に改善されてます。ふつーの利用時は関係ないんですが、フレームワークに組み込んだり、拡張する場合に、こちらのほうが圧倒的に良いです。性能特化のDIを用意したってことなんですが、まぁ相応に良いですねぇ。ちょっとDI嫌いは返上しよう……。ZeroFormatterにも後で移植しよう……。

改めてZeroFormatterとどっちがいーんですか!というと、特性に合わせて選んでくださいとしか言いようがありません。ZeroFormatterが効果アリ!なシチュエーションでピンポイントで使っていけば勿論それは効果アリ!ですが、ぶっちけ7割がた、MsgPackのほうが良いケースのほうが多いとは思っています。MsgPackは偉大なフォーマットですぞよ(ただしTimestampのフォーマットは早く決めて欲しい)。私の中でZeroFormatterのようなフォーマットが必要な理由が、MasterMemoryを作ったことにより、そっちのほうが上位の形で解消されてしまったというのがんががが……。

MsgPack-Cliとでは、まぁお好みで。アタリマエですが実績は無視できないファクターでしょう。ライブラリのメンテナーとして信頼できるかどうかも違いですね(私よりもずっと安定感あると思います!別に私もやらないわけじゃないんですが、ムラがあるんで)。それと私はSilverlightとかWindows Phoneとかサポートする気はないんで、その辺が必要な場合は必須ですね。

世の中、もう十分枯れきったと思っているところでも全然ゆるくて、手を加えられる余地はあるんだなぁ、というのは発見でした。シリアライザがここまで性能伸ばせるなんて、やってみるまで思いもしなかった。C#の良くないところに、ピーキーにチューニングされたライブラリが少ない(Javaのほうが遥かに多いのは事実でしょう!)ことがあり、それが諸々のパフォーマンステストや、そもそもの実績に影響を与えているのですよね。

結局、今までC#がその辺を「ゆるく考えていた」ことの積み重なりが、今の体たらくを招いていることの一因だとも思っています。別にMicrosoftだけではなくコミュニティ全体がね。吐気がするような継承の瓦礫の塔を築いたり、無駄にFunctionalであろうとしたり。私は、C#は好きな言語だから使っているというだけじゃなくて、「前線で戦える言語」だから使っているのです。何かの理想を追う言語ではなく、真に実践的な言語であるから全力で投資しているのです。常に戦場であり、他の言語なりフレームワークなりと戦っているフィールドであり、そこではフェアに評価されるべきであり、戦って死ね。と。C#を前線で戦わせるためにも、こうして一つ一つ、証明し続けていかなければならないでしょう。

UniRxを支えるユニットテスト - RuntimeUnitTestToolkit for Unity

オープンなようなクローズドなような、ラウンドテーブルディスカッションのような、少人数のところでUnityのユニットテストについて話してきました。というか、UniRxのために作って、以降、私の作るUnity用の色々なので使いまわしてる自作のユニットテストフレームワークについて、ですね。

RuntimeUnitTestToolkit for Unity from Yoshifumi Kawai

このフレームワークはずっとUniRxの中に埋まったまんまだったんですが、使える形でパッケージしたのを、今日GitHubに公開しました。unitypackageとしても置いてあるので、一応インポートはしやすいはずです。

とりあえず必要な機能しか入れてないんで、汎用テストフレームワークとしては足りない機能が普通に多いので、その辺も作ってからアセットストアに公開したいなぁ、と思ってはいたんですが、まぁそうなるといつまで経っても公開できなさそうなので、とりあえず現段階のもので公開、です。

.NETのテスト事情、或いはUnityでテストを書かないことについて

私はライブラリとしてはふつーの.NETと共通で動くものを作ることが多いんで、まぁそういう場合は大部分はふつーの.NETのユニットテストを書いたほうが遥かに書きやすいでしょう!つまりUnityでテストを書くコツはUnityで書かないということです!!!みもふたもない。

image

テストのメソッドを右クリックしてデバッグ実行で直接Visual Studioのデバッガでダイレクトにアタッチできたりとか、基本的に最高ですね。

さて、スライドにも書いたのですが、最近はxUnit.netを好んで使っています。MSTestはいい加減投げ捨てていいでしょう、というか投げ捨てるべきでしょう。NUnitは知らん。いらん。補助としてChainingAssertionは変わらず使ってるんですが、.NETCore対応を内部では作って使ってるんですが公開には至ってない……。

また、モックライブラリとしてはMicrosoft Fakes Frameworkのような大仰なものは「絶対に」使うべきではない、という思いが強くなってます。テストはただでさえ負債になりやすいのに(盲目的にテストは書くべき信仰してる人は、テストの負債化に関して全く言及しないのがポジショントークなのか脳みそお花畑なのか、頭悪そうですね)、大きな自動生成を伴うものは負債の連鎖を作りやすいなー、と。シンプルに作らないと、シンプルに投げ捨てることができない、というね。そして、投げ捨てるのは簡単ではなく、投げ捨てるのもまた技術なわけです。

RuntimeUnitTestToolkit

.NETでテスト書くからそれでOK、というわけは当然なくて、Unityだけでしか動かない部分もあるし、そもそもUnityでちゃんと動くかどうかの保証はない。更にはIL2CPPに通した場合はやっぱり別物の挙動というか動かなくなるケースは「非常に多い」ので、ちゃんとIL2CPPで動くことを保証しなければならない。そこで作ったのがRuntimeUnitTestToolkitです。Unityには標準でテストツールあるじゃん、って話ですが、あれは実機動作させられないので論外です。それで用が満たせりゃあ標準の使うわ。

image

テストが並べられて、ボタン押したら実行、ボタンが緑になったら成功、赤になったら失敗というシンプルなふいんきのものです。一個のシーンになってるので、ビルドして実機転送すればそのまま実機で動きます。

実際に自分で使うには、Releaseページからunitypackageを落としてきてインポート。で、UnitTest.sceneを開いて再生すればOK。簡単簡単。

テストの書き方ですが、基本的にはMonoBehaviourを継承したりもしないシンプルなクラスを用意します。

// make unit test on plain C# class
public class SampleGroup
{
    // all public methods are automatically registered in test group
    public void SumTest()
    {
        var x = int.Parse("100");
        var y = int.Parse("200");
 
        // using RuntimeUnitTestToolkit;
        // 'Is' is Assertion method, same as Assert(actual, expected)
        (x + y).Is(300);
    }
 
    // return type 'IEnumerator' is marked as async test method
    public IEnumerator AsyncTest()
    {
        var testObject = new GameObject("Test");
 
        // wait asynchronous coroutine(UniRx coroutine runnner)
        yield return MainThreadDispatcher.StartCoroutine(MoveToRight(testObject));
 
        // assrtion
        testObject.transform.position.x.Is(60);
 
        GameObject.Destroy(testObject);
    }
 
    IEnumerator MoveToRight(GameObject o)
    {
        for (int i = 0; i < 60; i++)
        {
            var p = o.transform.position;
            p.x += 1;
            o.transform.position =  p;
            yield return null;
        }
    }
}

属性とかは特に必要なく、戻り値voidのパブリックメソッドは強制的にテストメソッドとして認識します。また、戻り値IEnumertorのクラスは非同期テストメソッドとして認識してコルーチンとして動かすので、中でyieldとか他のコルーチンを動かしての待機とかも自由にできます。

さすがに定義だけでテストクラスを認識できないので、それとは別にテストローダーを書いてあげます。

public static class UnitTestLoader
{
    [RuntimeInitializeOnLoadMethod(RuntimeInitializeLoadType.BeforeSceneLoad)]
    public static void Register()
    {
        // setup created test class to RegisterAllMethods<T>
        UnitTest.RegisterAllMethods<SampleGroup>();
 
        // and add other classes
    }
}

これで実行してやれば、書いたクラスが実行時にボタンとしてシーンに追加されます。

ある程度リフレクションでメソッドとかの認識をしているんですが、ちゃんとIL2CPPで動作するギリギリのリフレクション加減で仕上げつつ、書きやすい直感的にAPIに仕立てたというのが工夫ポイントですね!

with UniRx

UniRxは結構ユニットテスト向けだったりします。例えば何かアクションを加えてイベントが発行されることを確認したい、という場合に、IObservableとして公開されているならば

public IEnumerator WithUniRxTestA()
{
    // subscribe event callback
    var subscription = obj.SomeEventAsObservable().First().ToYieldInstruction();
 
    // raise event 
    obj.RaiseEventSomething();
 
    // check event raise complete
    yield return subscription;
 
    subscription.Result.Is();
}

と、サクッと書けたりします。あるいは、何か色々によって色々値が変わるということは

public IEnumerator UniRxTestB()
{
    // monitor value changed
    var subscription = obj.ObserveEveryValueChanged(x => x.someValue).Skip(1).First().ToYieldInstruction();
 
    // do something
    obj.DoSomething();
 
    // wait complete
    yield return subscription;
 
    subscription.Result.Is();
}

と、ObserveEveryValueChangedで外側からサクッと値の監視が可能です。また、各種のObservableTriggerを突っ込むことによって、外側から内部の状態をサクッとモニタできます。あまり実際のプログラムでは使うことはないようなことも、ユニットテストなら派手に使っても構わないし、そういう時に楽ができるツールがUniRxには揃っています。外側からサクッとどうこうする手段がないと、インスペクタにユニットテスト用の特別な何かを仕込んでアサートとかいう、しょぼいテストフレームワーク(UnityのIntegration Test Frameworkのことですよ!)になってしまいがちですので。

まとめ

現状のUnityの単体テストツールは、必要な要件を全く満たしてなくて使えなさすぎですぅ。テストツールは結構大事で、とりあえずテスト大事、とりあえずテスト書くんだ、とかいってしょうもないツールを土台にやってるとボロボロに負債になるんで、ちゃんと自分の要件を意識して選択しないとダメですね。そこも把握できてなかったり、あとシンタックスも非常に大事で、Spec系がぶっちゃけ書き方違うだけで本質的に変わらないのに非常に感触が変わるのと同じで、そういうの大事にできない人はプログラミングの感性足りてないんで、小手先のテスト信仰とかしてないで、それ以前にまともな感性磨いたほうが良さそうですね。

とはいえ、Unity 5.6から良くなる気配を見せていて、少なくともその延長線上にはちゃんとした未来がありそうなだけの土台は作れてそうなので良かった。それ以前の(現在の)は本当にセンスなさすぎて、こいつらの感性の先に未来はなさそうだなー、と思ってたんで。

RuntimeUnitTestToolkitをオススメするかっていうと、実機で動かすのに困ってればいいんじゃないでしょうか!とはいえ、素朴すぎるってところはあるんで、もう少し作り込まないと使えないというケースは多そうってところです。私も、自分の作る程度の規模では困ってないんですが、会社のプロジェクトに入れると困るところは多く出てきそうだなー、という感じですね。足らないところを自分で補っていけるならというところです。

近況

ところでなんと今年に入ってブログ書いてなかった!はうう!というのは、書きかけのプロジェクトが多くてそれにあくせくあくせくだからんですねえ。公開まであともう一歩、というところまでに持ってけているのは MessagePack for C#(.NET, .NET Core, Unity, Xamarin) です。

ZeroFormatterあるじゃん、なのに何故、って話ですが、まぁそれは公開時にでも。とりあえず、エクストリーム速いです。それと、拡張性も重視して組んでいて、Unity用の特殊な拡張をアドオンとして有効化すると、例えばVector3[]のシリアライズ/デシリアライズがJsonUtilityの50倍高速化(50倍!)とか、色々強力で強烈になってます。乞うご期待。

それと会社ブログ - Grani Engineering Blog始めましたということで、そっちに幾つか記事書いてますね。C#のswitch文のコンパイラ最適化についてとか。あとgRPC化とか。

NextGen Server/Client Architecture - gRPC + Unity + C# from Yoshifumi Kawai

こちらも、シリアライザのMessagePack for C#化とか大工事を何度かしつつも、もうすぐとりあえずStableといえるとこまで持ってけそうです。

また、Unity用のインメモリ内蔵データベースとしてMasterMemoryというのも作っていて

*neuecc/MasterMemory

これももうすぐ公開できそうかもかもといったところで、とりあえず色々あって大変大変。どれもUnityでのユニットテストには RuntimeUnitTestToolkit で動かしてるんで、私自身は超ヘビーに使いまくってますよ、です。

ZeroFormatter 1.3 - 機能強化とstructの超高速性能とFAQと。

ほとんど昨日の今日な状態で1.3って、バージョン1.0とは何だったのか、というかそれってベータだったということなのでは?という、あまりにいい加減なバージョン番号付けなのですけれど、そんなわけで1.3です。これが本当の1.0だ……。

基本的な概要は初出での記事 ZeroFormatter - C#の最速かつ無限大高速な .NET, .NET Core, Unity用シリアライザーを読んでいただければと思うのですが、では何が変わったかというと、ReadMeを全部書いた!いや地味に面倒なんですよ、分量あるし。英語だし。

というのもあるんですが、方向性を若干変えました。なんというか、反響が思ったよりも良すぎた。あまりの良さにビビッた(GitHub Starも私的最高伸び速度最大をマークした)、のと、だいぶ気を良くしたので、ユースケースを変えたベンチマークを他にとってみたりして、改めて考えた結果「汎用的に全方位に使える最強シリアライザ」にすることにした。というのが大きな方針転換。

汎用シリアライザとして

ビルトインでサポートしてる型を大幅に増やしました。具体的には

All primitives, All enums, TimeSpan, DateTime, DateTimeOffset,
Tuple<,...>, KeyValuePair<,>, KeyTuple<,...>,
Array, List<>, HashSet<>, Dictionary<,>, ReadOnlyCollection<>, ReadOnlyDictionary<,>,
IEnumerable<>, ICollection<>, IList<>, ISet<,>,
IReadOnlyCollection<>, IReadOnlyList<>, IReadOnlyDictionary<,>, ILookup<,>
and inherited ICollection<> with paramterless constructor

です。まぁようするに、普通に生活してて(?)出てくるほとんど全部の型がそのまま使えます。特にコレクション系を、普通に使ってても一切躓かないようにしました。1.0では実はIList/IDictionaryしかサポートしていなかったのです!もともとの発端がFlatBuffersのような内部にバイト配列を抱えてデシリアライズしないから無限大に速い(ツッコミどころの多いこの表現ですが、これはCap’n Protoから引用してます。Cap’n Protoは日本での知名度はゼロに近いですが、私は最初見た時かなり衝撃を受けました。ちなみに他にもタイムトラベルRPCとか、カッコイイ用語が目白押しなのもCap’n Protoは素敵です)、という点を強く意識していたので、具象型(ListとかArray)だと、それが実現できないんですよね。なので却下にしてたのですけれど、「汎用シリアライザ」として使わせたいんだったらサポートしたほうがいいかな、と。シリアライズ/デシリアライズ速度が他を圧倒して超高速だったというのも決断を後押ししてます。まぁこれだけ速いんだから全然いいだろ、みたいな。

structが超速い

というか、これに関しては他が遅すぎるといったほうが正しいぐらい。

image

intだけとかVector3とかそれの配列とか、HTMLぐらいを想定した大きめ文字列とかの結果です。文字列は結局UTF-8でエンコード/デコードするのはみんな変わらないのでそんなもんかってところですが、他が絶望的に違いすぎる。アホみたいに差が開いてるんですが、これは事実なんだなぁ。

これは、小さいデータに関しての考慮が全然ないから、というのがめっちゃ大きい。int(1)を書くってのは、つまり最速は BitConverter.GetBytes(1) なんですよ、で、もはやそこからどれだけ「遅くするか」の勝負ですらある。他のシリアライザは、やってることがあまりにも多い、だから際限なく、最速から遠くなる。ZeroFormatterは限界まで無駄がない(実際、これ以上縮めようがない)ので、もんのすごく差が開きます。どうせ小さいデータだから一個一個は差がデカいといっても小さいとも言えるんですが、頻度が高いと馬鹿にならない差になります。というかさすがにここまで違うと全然違うでしょう。

小さいデータのやり取りって、ないようで結構あるんですよ。ウェブだったら、例えばMemcachedやRedisなどKVSへのアクセスでintだけ格納したりとかって普通によくある。ゲームだったら座標データ(Vector3)のやり取りとかね。なのでまぁ、ZeroFormatterはかなり価値あるかなー、と。

Union型の追加

なにそれ、というと、一個の型の表明で複数の型を返せるようになります。どちらかというとポリモーフィズムのほうが近いですかねー、実際C#でのデシリアライズ結果はポリモーフィズムとしての表現に落としているので。ド直球に言うとFlatBuffersにあるやつです。

// こんなんで判別したいとして
public enum CharacterType
{
    Human, Monster
}
 
// こんなふーにabstract classとUnionAttributeに子クラスを並べて、UnionKeyで識別するものを指します
[Union(typeof(Human), typeof(Monster))]
public abstract class Character
{
    [UnionKey]
    public abstract CharacterType Type { get; }
}
 
// あとは延々と並べる。
[ZeroFormattable]
public class Human : Character
{
    // UnionKeyはintでもstringでもなんでもいいんですが、かならず同じ値が帰ってくるようにする必要がある
    public override CharacterType Type => CharacterType.Human;
 
    [Index(0)]
    public virtual string Name { get; set; }
 
    [Index(1)]
    public virtual DateTime Birth { get; set; }
 
    [Index(2)]
    public virtual int Age { get; set; }
 
    [Index(3)]
    public virtual int Faith { get; set; }
}
 
[ZeroFormattable]
public class Monster : Character
{
    public override CharacterType Type => CharacterType.Monster;
 
    [Index(0)]
    public virtual string Race { get; set; }
 
    [Index(1)]
    public virtual int Power { get; set; }
 
    [Index(2)]
    public virtual int Magic { get; set; }
}
// で、こう使う。
var demon = new Monster { Race = "Demon", Power = 9999, Magic = 1000 };
 
// Union型を指定してシリアライズする(そうしないと子を直接シリアライズしてしまうので)
var data = ZeroFormatterSerializer.Serialize<Character>(demon);
 
var union = ZeroFormatterSerializer.Deserialize<Character>(data);
 
// 結局みんな大好きswitchですが何か。
switch (union.Type)
{
    case CharacterType.Monster:
        var demon2 = (Monster)union;
        demon2.Race...
        demon2.Power..
        demon2.Magic...
        break;
    case CharacterType.Human:
        var human2 = (Human)union;
        human2.Name...
        human2.Birth...
        human2.Age..
        human2.Faith...
        break;
    default:
        Assert.Fail("invalid");
        break;
}

最終的にswitchなのがダサいといえばダサいんですが(C#でやる表現上の限界かな!)、まぁ悪くない落とし所なのではないかな、と。で、これ、便利ですよ。マジで。うーん、結構あるんですよね、状況に応じて複数データ返したいときって。で、愚直にやるとこうなるわけです。

public class Hoge
{
    public 何か1の時の型 Nanika1 { get; set;}
    public 何か2の時の型 Nanika2 { get; set;}
    public 何か3の時の型 Nanika3 { get; set;}
}

いやー、色々無駄だし型の表現としてもアレだしちょっと、ねー、っていう。

Unionをシリアライザで記述するという点では、ZeroFormatterのやり方はかなり上手い感じで(自分で言う)、書きやすさと安全性(完全ではないけれど、意識しやすさが高いのでそこそこはある)をいい塩梅に両立させれたんじゃないかなー、と。特に書きやすさはかなりあると思います。というかぶっちけ他のシリアライザでこの手のポリモーフィズムやるのは凄まじく大変なので、革命的に便利になったといっても過言ではない。

バイナリ仕様の整理と多言語対応

諸々の追加や事情も踏まえて、バイナリ仕様を整理しました。

まず、言語中立にしました。いやまぁ、もともと、C#依存度の高いものは外して移植しようと思えばできるように、みたいな感じに作ってはいたのですけれど、より明確に中立を意識して整理しました。元々かなり頭悪く単純に作ってあるので(ZeroFormatterの速さは賢くないバイナリ仕様をC#実装力でねじ伏せる、というところがかなりあって、逆に言えば実装Firstで作られているので、言語実装で最速になるように寄り添って仕様が固まったとも言える)

というのと、↑のように遅延実行ではないコレクションのサポートを正式に入れるということで、Sequence Formatというのを正式に用意して遅延ではないDictionaryなどのレイアウトはここに属する、という形にしました。Objectも、ObjectとStruct という分けかたで定義して、KeyTupleはStructに属してますよ、みたいに割とそこそこちゃんと汎用的感な分類になってるんじゃあなかろうか。結構あーでもないこーでもないと弄ってたんですが、うーん、なるほど、こういうのは結果はあっさりしてるけど過程はとても大変……。

と、いうわけで、言語がC#のみってのはさすがに普通に欠点なんですが、整備してみたんで多言語サポートよろしくお願いします、みたいな(?)。やりたい気持ちはあるんですが、如何せんちょっとC#以外は手が回らないのデスデス。社内ではサーバーもC#で完動するようになってるので、あんまり強い外圧が働かなくて。そして実際手が回らないので。仕様作る!実装する!社内のプロジェクトのデータの移植もする!更にこれを使った次の何かも作る!あわあわわわわあわ、本当に手が回ってないヤヴァイ。

スキーマはあるよ

スキーマはあります。見えないだけで。どういうことかというとこういうことです。

namespace /* Namespace */
{
    // Fomrat Schemna
    [ZeroFormattable]
    public class /* FormatName */
    {
        [Index(/* Index Number */)]
        public virtual /* FormatType */ Name { get; set; }
    }
 
    // UnionSchema
    [Union(typeof(/* Union Subtypes */))]
    public abstract class UnionSchema
    {
        [UnionKey]
        public abstract /* UnionKey Type */ Key { get; }
    }
}

C#自体がスキーマなのです。それの利点はかなりあって、「パーサーを作らなくて済む(C#のコンパイラは既にC#で実装されていて、それのパーサーが使える)」「入力補完/コードフォーマット/シンタックスハイライト/アナライザー拡張などIDE(Visual Studio)の恩恵をフルに使える」ってのが、まずは良い。実際、zfc.exe(ZeroFormatterCompiler)という実行ファイルによって、C#というスキーマをもとにコード生成をしています。現在はAOTのためのC#コード生成ですが、別に出力を変えれば、他の言語のコードでも全然吐けます(ランタイムがないから無理だけど!)

デメリットは「機能が制限されてないので容易に制限からはみだせるので言語中立にしづらい」「現行のC#の言語機能に制限される(例えば非nullなStringは定義できない)」ってとこですね。特に前者がビミョーなんですが紳士協定の範囲内(C#としてコンパイル可能でもZeroFormatterとして解析不能だっていうエラーを放り投げちゃえばSyntaxErrorなコードと変わらない)に収めることはなんとか可能なんじゃあないかなあ、とか。ってのは夢見てます。

そして最大の利点がスキーマが生成を介さなくてもシェアできる、ということ。「プロジェクト参照」や「DLL参照」という形で、スキーマと生成コード(実際は実行時動的生成するんですが)をコード生成なしで複数プロジェクト間で共有できます。シームレスに。これは非常に大きくて、まぁ前の記事でも書いたんですがコード生成はやればやるほど複雑化していくんで、ないに越したことはないんですよね。んで、C# as Schemaだと、ゼロにできる。これはワークフローにとってはインパクトが相当大きいことです。

私は、コード生成や自動化って「したくない」ことの筆頭候補に挙げてます。自動化はミクロでは楽になっても、その積み重ねがマクロでは害悪になるケースが往々にして多い。なので、やるべきことは「自動化をしなくてすむ」ようにすることです。そのために脳みそを動かしたい。結果、脳みそが追いついてなくてそこら中が止まることも往々にしてある。shoganai。

まとめ

redddit/r/csharp/ZeroFormatterでAsk Me Anythingやってます(とは)。Fastestとかぶち撒けたせいでシリアライザ戦争が勃発している(恐ろしい)。なるほどWire、シランカッタ。コード的には基本的にZeroFormatterのほうが速そーなので、トータルで色々なケース作れば勝つと思うんだけど、弱点を突くと負けるケースは出てくるのかなぁ。とはいえ普通に私の手元で図ったら圧勝した、ふむ。(最終的に相手のベンチマークにZeroFormatter足して計測→結果 圧倒的な圧勝ということで、まぁしょうがない、相手が悪い。確かにWireは二位なので、惜しかったで賞というところ)

というわけで、真面目に、C#でサッと今使ってるシリアライザをそのまま置き換えられるものにしました。つまり、あらゆるところで使ってください、と言ってます。実際、小さなところから大きなところまで効果あると思います。小さなところは↑でstructを例にしましたが、大きなところでは、例えばバッチ処理の連鎖とかで、延々と巨大なデータを送っているのだけれど、一つ一つはその一部しか使わないんだよねー、みたいな場合。に、ものすごく効くんじゃない?って意見貰いました。その通りで、実際そういうケースでは正しくめっちゃ効きますねー。

とかとかって感なので、是非是非試してみてくださいな。あとクドい告知ですが11/27開催の歌舞伎座.tech#12「メッセージフォーマット/RPC勉強会」でもお話します&クロスプラットフォーム(Unity, Windows, Mac, Linux)で使える通信用のフレームワークをリリースします(!)のもします(ホントに!)

UnityのMonoアップグレードによるasync/awaitを更にUniRxで対応させる

ついに!.NET 4.6アップグレードが始まりました。Unityの。Unity 5.5でC#コンパイラをアップグレードしていましたが、今回はついにフレームワークも、です。また、Unity 5.5のものはC#のバージョンは4に制限されていましたが、今回はC# 6が使えます。現在はForumでアーリアクセスバージョンが公開されていて、ついでにそこでリンクされているVisual Studio Tools for Unityも入れると、かなりふつーに.NET 4.6, C# 6対応で書ける感じです。

さて、.NET 4.6, C# 6といったら非同期。async/await。もちろん、書けました。が、しかし。

async Task ThraedingError()
{
    Debug.Log($"Start ThreadId:{Thread.CurrentThread.ManagedThreadId}");
 
    await Task.Delay(TimeSpan.FromMilliseconds(300));
 
    Debug.Log($"From another thread, can't touch transform position. ThreadId:{Thread.CurrentThread.ManagedThreadId}");
    Debug.Log(this.transform.position); // exception
}

これはtransformのとこで例外でます。なんでかっていうと、awaitによって別スレッドに行ってしまっているから。へー。この辺、async/awaitではSynchronizationContextという仕組みで制御するのですが、現在のUnity標準では特に何もされてないようです。

それだけだとアレなので、そこで出てくるのがUniRx。今日、アセットストアで最新バージョンのVer 5.5.0を公開したのですが、この5.5.0ではasync/await対応を試験的に入れています。それによって自動的にSynchronizationContextも生成/登録してくれます。

async Task UseUniRxInBackground()
{
    Debug.Log($"Start ThreadId:{ Thread.CurrentThread.ManagedThreadId}");
    await Task.Delay(TimeSpan.FromMilliseconds(300));
    Debug.Log($"From same thread, because UniRx installs UniRxSynchronizationContext.ThreadId:{ Thread.CurrentThread.ManagedThreadId}");
    Debug.Log(this.transform.position); // show transform
}

というように、UniRxをインポート後では、前の例外を吐いたコードと全く同じでも、ちゃんとメインスレッドに戻してくれるようになります。

Coroutine is awaitable

UniRxを入れることで追加される機能はそれだけではなく、更に普通のコルーチンもawait可能な仕組みを裏側で仕込んでいます。これにより

async Task CoroutineBridge()
{
    Debug.Log("start www await");
    var www = await new WWW("https://unity3d.com");
    Debug.Log(www.text);
    await CustomCoroutine();
    Debug.Log("await after 3 seconds");
}
 
IEnumerator CustomCoroutine()
{
    Debug.Log("start wait 3 seconds");
    yield return new WaitForSeconds(3);
    Debug.Log("end 3 seconds");
}

といったように、WWWとかIEnumeratorを直接awaitすることが可能になります。これはUniRx側で用意した仕組みによるものなので、普通では(現状は)できません。

勿論(?)IObservableもawait可能になっています。

async Task AwaitObservable()
{
    Debug.Log("start await observable");
    await Observable.NextFrame();  // like yield return null
    await Observable.TimerFrame(5); // await 5 frame
    try
    {
        // ObservableWWW promote exception when await(difference in await WWW)
        var result = await ObservableWWW.Get("https://404.com");
        Debug.Log(result);
    }
    catch (WWWErrorException ex)
    {
        Debug.LogError(ex.ToString());
    }
    Debug.Log("end await observable");
}

ObservableWWWを使うと例外はちゃんとtry-catchのほうに投げてくれるようになって、より自然に、簡単に扱えるようになります。

まとめ

思ったよりも、普通に使えて、普通に統合できるな、という印象があります < async/await。コルーチンで扱うよりも自然で、より強力なので、非同期を扱うのに適したシチュエーションではこっちのほうが良いのは間違いないはずです。Rxとの住み分けですが、基本的に非同期が対象ならばasync/awaitのほうが良いです。が、今回見ていただいたようにIObservableはawaitableなので、コードがRxになっているならば、現在のコードから自然にasync/awaitベースにソフトに移行することが可能でしょう。

Unityが今後、標準でSynchronizationContextを入れてくるのか、コルーチン対応クラスをawait対応にさせてくるのか、などはちょっと分かりません。分かりませんが、UniRxならば、その対応がずっと後のことになるとしても、今すぐ問題なく使うことが出来ますし、その可能性を今すぐ感じ取ることが可能なので、ぜひとも試してみてください!

余談

UniRxがAssetStore STAFFPICKに選ばれましたー。

うーん、嬉しい。

Photon Server Deep Dive - PhotonWireの実装から見つめるPhotonServerの基礎と応用

本題と関係ない連絡ですが、UniRx 5.4.1出しました。更新内容は主にUnity 5.5 Beta対応です(不幸にもそのままだとコンパイルエラーが出てしまっていたのだ!)。LINQ to GameObject 2.2もついでに出てます。こちらは最適化を更に進めたのと、Descendants(descendIntoChildren)というベンリメソッド(子孫要素への探索時に条件で打ち切る)の追加です。どちらも便利なので是非。

と、いうわけかで、昨日、GMO Photon 運営事務局さん開催のPhoton勉強会にてPhoton Server Deep Dive - PhotonWireの実装から見つめるPhoton Serverの基礎と応用というタイトルで話してきました。

Photon Server Deep Dive - PhotonWireの実装から見つめるPhotonServerの基礎と応用 from Yoshifumi Kawai

Deep Diveなのか入門なのか微妙なところに落ち着いてしまいはしたのですけれど、他の通信ライブラリ候補との比較含めPhotonの検討理由、PhotonServerの真っ白な基本的なところ、PhotonWireの優れているところ、黒騎士と白の魔王で予定している構成、などなどを一通り紹介できる内容になったのではかと思います。

PhotonWireの細かい話はPhotonWire - Photon Server + Unityによる型付き非同期RPCフレームワークと、実装の(Photonと関係ないC#的な)細かい話は実例からみるC#でのメタプログラミング用法集のほうが詳しいです。おうふ。より詳細を話すつもりが、逆に表面的になってしまった。反省。

ZeroFormatter

一番反響があったのは、Photonよりも、むしろスライド53pから少し説明しているZeroFormatter(仮称)という、私が製作中の無限大に速い新シリアライザ/フォーマットの話でした。Oh……。

まぁ実際、(Unityに限らずですが特にUnityで)かなり使えるシリアライザにするつもりなので乞うご期待。JsonUtility、いいんですけど、制約が強すぎるんですよね、特にオブジェクトをデシリアライズする際に、nullが0埋めされたクラスに変換されちゃうのがかなりヤバかったりなので、汎用フォーマットとしては使いにくいのではないかな、というところはあります。速いんですけどねえ。また、FlatBuffersはAPIがヤバいので検討する価値もないと思ってます。あれはアプリケーションの層で実用に使うのは無理。

というわけで、絶妙にイイトコドリを目指してますので、乞うご期待。出来上がったらGitHubやUnityのAssetStoreに投下しますので人柱募集です。

LINQ to GameObject 2.1 - 手書き列挙子による性能向上と追加系をより使いやすく

(前回の1.3から)1年ぶりの更新です!2.0は諸事情でスキップされました。アセットストアには出したんですが内容的にもう少しやりたかったのでなかったこと扱いで。LINQ to GameObject自体の説明はVer 1.0リリース時のブログLINQ to GameObjectによるUnityでのLINQの活用を参照ください。

今回はパフォーマンスチューニングを徹底的にやりました。というのも以前の素朴な実装は、素朴な通りの性能で、いいとか悪いとかじゃなく素朴なので、やるのならいっそギチギチにやってみたらどうかな、と。性能面でここまでやってるものは絶対にないはず。

もう一つは追加系をより使いやすく。のためにガッツリと破壊的変更を入れています。破壊的変更が入った理由は、使いにくかったからです。うぇぇ……。使いにくいポイントは概ね分かっていたし、プルリク等も貰っていたのですが、API的にイマイチなもので乗り気になれず、かといってAPIを維持しているとオーバーロードの解決などの問題でうまく処理できなくて、モニョモニョしている間に一年が経ってしまった。互換性は残したくはあったんですが、使いにくいままであったり、微妙なオーバーロードの追加とかで解決するよりは良いかな、と。いう決断です。

Traverse系

APIはほとんど変わってないです(但しnameでフィルターかけるオーバーロードは消しました、HTMLやXMLと違って名前でのフィルタの重要性がかなり低いので、むしろないほうがいいかな、と)。

ヒエラルキーをツリーに見立てて、「軸」の概念を元にして、必要となる全方向での列挙を満たしています。今回、コードを劇的に書き換えたパフォーマンスチューニングを施しました。一点目は、yield returnによるコードを、全部手書きの構造体の列挙子に書き換えてます。これにより列挙に伴うゴミ発生が理想的にはなくなっています、理想的には:)

残念ながら、そのままforeachに流すと C#のGCゴミとUnity(5.5)のコンパイラアップデートによるListのforeach問題解決について によりboxingが発生しますが(ゴミ化)、それでも構造体のサイズや再帰的に処理される場合での内部処理は気を配っているので(特にDescendantsはエクストリームにチューニングしたコードに変えた(再帰を特化Stackで置き換えたり……))、以前よりも良くなっているのは間違いないです。

ちなみに、基本的にはmutableなstructは避けたほうがいいです。Enumeratorはまさにそれで、実装にも注意が必要なら、利用にも注意を要するため(これはList<T>.Enumeratorも同様で、直接触ろうとすると罠にはまるケースが出てくる)なんでもかんでもstructで、というのは止めたほうがいいでしょう、どうしてもということでなければ原則やらないほうがいい事案です。struct enumeratorを返すテクニック自体は今は亡きXNAでも使われていたので(EffectPassCollectionやModelMeshCollectionなど各種コレクションがstruct Enumeratorを返す)、まぁ最終テクニックとしては有効(但し現状Unityではどうせforeachではボックス化されるのでそこまで有効ではないので、基本やらなくていいでしょう)

LINQで繋げたら、当然普通にLINQの消費フローに入るので、そんな意味ないんですけどね!というだけなのもアレなので、改善二点目、頻出パターンについて特化した最適化を入れてます。(+ OfComponent) + First, FirstOrDefault, ToArray に関しては通常のLINQではなく、この構造体Enumeratorに特化した呼び出しをするため、所謂LINQで想像する性能劣化を受けません。社内調べによると、割と FirstOrDefault や ToArray が直接接続されてる場合が多いので、それだけでも6~7割はカバーできているのではないかな、と。

更に三点目、ToArrayNonAllocというメモリ節約/GC防止メソッドが追加されています(IEnumerable<T>にも生やしてあるのでLINQ to GameObject関係ないシーンでも使えないこともない)

GameObject[] array = new GameObject[0];
 
// 毎フレーム走査していても余計なメモリ確保はしない!
void Update()
{
    var size = origin.Children().ToArrayNonAlloc(ref array);
    for (int i = 0; i < size; i++)
    {
        var element = array[i];
    }
}

Physics.RaycastNonAllocやGetComponentsInChildren[T](List[T] results) のようなものですね。どうしても走査頻度が高くて、という場合には使えるんじゃないかと思います。まぁ、Find系は極力使わないように、というのと同じ話で、走査系を頻繁にやること自体が全然よくはないのですけれど。

また、ToArray/ToArrayNonAlloc/Destroyには(Func<GameObject, T> selector), (Func<GameObject, bool> filter), (Func<GameObject, bool> filter, Func<GameObject, T> selector), (Func<GameObject, TState> let, Func<TState, bool> filter, Func<TState, T> selector) といった、Where().Select().ToArray() のような割とよくある状況に対する最適化オーバーロードを入れてます。

この辺を活用してもらえば、単純にインラインで自前実装するよりも、むしろ速い/効率的なことのほうが多いでしょう。

特化したものを速くなるのはある種当たり前で、しかしそうするとメソッドが雪だるま式に増えるのが良くなくて、そしてLINQのいいところは合成可能なことにより特化させずとも無限の組み合わせで機能を実現できるところにある。しかし、まぁ勿論、柔軟性とパフォーマンスが幾ばくかトレードオフなのは当然の話なわけで、LINQの雰囲気を保ったまま、裏側だけ特化実装にこっそり差し替わってる。というあたりが落とし所としては良いのかな、と思ってますし、なのでそういう風に実装しました。

再帰的なイテレータの罠

Children(子要素列挙)なんかは数が大したことないので問題はそんなないんですが、Descendants(子孫要素列挙)は性能差が大きく出てきます。そして、利用頻度で言ってもDescendants系が基本多い。これのパフォーマンスを改善することは、非常に意味のあることです。さて、これはシンプルなDescendantsの実装です。

static IEnumerable<GameObject> Descendants(GameObject root)
{
    yield return root;
    foreach (Transform item in root.transform)
    {
        foreach (var child in Descendants(item.gameObject))
        {
            yield return child.gameObject;
        }
    }
}

このコードには大きな問題があります!再帰的なイテレータ、つまり foreach (var child in Descendants(item.gameObject)) は危険です。Baaaaad Practice、デス。要警戒です。これ、子孫にあるGameObjectの数だけ、イテレータ作ってます。GetEnumerator祭り!これは、LINQがどうのとかそういう次元を超えています。LINQのコストというのはメソッドチェーン分のGetEnumeratorの加算とMoveNextの連鎖による一回の呼び出しコストの増加が基本的な話で、ようするに2~3増えるという話で大したことあるといえば大したことあるし、大したことないといえば大したことない。が、さすがに要素数分だけ無駄にEnumerator作るとなったら話は別だ。ちょっとね、かなり気になるよね。

解決策は2つあります。一つはstruct enumeratorで、struct生成コストはあるもののゴミにはなりません。↑で書いたように実装済みです。

もう一点は、内部イテレーター化。イテレーターには概ね二種類、内部イテレーターと外部イテレーターがあります。外部イテレーターはforeachで使える、つまりGetEnumerator経由のもので、内部イテレーターはListのForEachなどクラスに直接生えてるもの。それぞれ利点と欠点があります。外部イテレーターの利点は柔軟性(LINQ)と言語サポート(foreach/generator)、よって基本的にはこちらを選べばOKです。欠点はパフォーマンスが内部イテレーターほど稼げない。どうしても一つシーケンスを進めるのにMoveNextとCurrentの2つのメソッド呼び出しが必要になるので。内部イテレーターの利点はパフォーマンスで、内部構造に最適化したループを回せるので、基本最速です。欠点は柔軟性がないのと、それぞれのコレクションで独自実装になること。

LINQ to GameObjectでは両方実装しています。外部イテレーターは手書きで最適化したstruct enumerator(とStackPoolと、その他諸々の仕掛け)によって、遅延実行やLINQサポートなどの柔軟性を維持したまま、パフォーマンスとGC行きのゴミを全く出さないようにしています。内部イテレーターに関してはForEachとToArray(NonAlloc)に関しては、外部イテレーター版と全く異なる実行パスを通ることにより、最速を維持します。

ところで、Unityネイティブに用意されているものがある場合は、それを使ったほうが速くなります。例えば DescendantsAndSelf().OfComponent().ToArray() は GetComponentsInChildren(includeInactive:true) に概ね等しく(一つのオブジェクトに複数コンポーネントが貼り付けてある場合、LINQ to GameObjectではそれぞれのGameObjectに一つのみ、GetComponentsInChildrenは複数と、正確には挙動が異なります)、後者を使ったほうが断然速い。一応ですが、ネイティブだから常に速いとか、そういうことはなくて、ネイティブ-マネージド間の変換コストのほうが勝る場合もあります(たとえばUnityにおけるコルーチンの省メモリと高速化について、或いはUniRx 5.3.0でのその反映のような話)。けれど、この場合は、C#だけで走査すると、GameObject毎でのGetComponentが避けられません(GetComponentのコストはタダではないのだ)。なので、一発でネイティブ内でかき集めてきたほうが絶対的に速くなります。子孫を辿るだけならほとんど遜色ない、むしろ速いといっていいぐらいなので、本当にこれはGetComponentに対する処理効率の差だけですね。これだけはどうにもできませんでした。

追加系

変わってます。使い勝手的にはこっちの対応がメインです。

以前のAPIの何が不便かって、引数にGameObjectしか受け付けなかった!そして戻り値がGameObject!大抵の場合はComponentを入れてComponentを受け取りたいのに!これは酷い!いやほんと酷すぎでした……。なんでそうなってたかというと言い訳はそれなりにあって、まずGameObjectとComponentって継承階層が別のとこにいるんですよねー、のが困る。それをオーバーロードとして分けると、IEnumerableを受け取るオーバーロードが存在していたため、どうやってもうまく型が解決できなかったのだ……。

もうどうにもならなかったので、API変えてます。IEnumerableを受け取るオーバーロードはXxxRangeという名前に分離。また、基本的には<T>を返すように、そして T:UnityEngine.Object を受け取れるようにしたので、引数としてやっとMonoBehaviourなComponentを素直に流し込めるようになりましたー。万歳。継承階層が別のとこにいて困ります問題は、UnityEngine.Objectを受け取った上で、動的にGameObjectとComponentに仕分けすることで解決。

というわけで、やっと自信持って普通に使えるようになりました。単純な話なんですが、まず破壊的変更にする、ということに腰が重かったことと、それを踏まえても、うまいAPIを構築するのに手間取った。のせいでこんなに遅れてしまって、いやはや……。

その他、あとDestroyでデフォルトでヒエラルキから外さなくしました。このヒエラルキから外すというのは最低のアイディアで、配列ではなく列挙しながら(LINQ to GameObjectでやるような!)Destroyする場合に、ヒエラルキから外すせいで位置がずれて死ぬ。というのを防ぐためにToArrayでキャッシュしなければならない(無駄なオーバーヘッド!)。というしょうもない自体に陥りがちなので、やめました。わざわざ外すコストだってゼロじゃないので、二重に悪い。

まとめ

GameObjectBuilderというものがあったのですが、イラナイ子なので消しました。LINQ to XMLのFunctional Constructionを模した――ものなのですが、そういう、コピーに一生懸命なだけなのって悪趣味なんですよね。大事なのは、概念(LINQ to Tree)を対象環境(Unity)に最適化することであって、コピーすることではない。そういうの、分かっているつもりではいたのですが、やり始めるとついついやってしまうところがある。随時見切って、バッサリ切り落とせるようにならないとですね。

LINQ to GameObjectのオリジナルのデザインは2014/10/28だったんですが、その頃は今よりは全然遥かにUnityへの習熟度、知識が欠けていたなぁ、というのを改めて痛感しました。思い上がる、ということはないですが、環境への理解力が足らないとどこかイマイチなものになってしまうわけで、C#云々抜きに、常にUnityに真摯に向き合ってかないとダメですね。実際問題、愛情を持って突き詰めて考えられないと、本当の理想のところまでは行けない。小手先の知識だけで処理したようなライブラリは、まぁ使いたくないですねえ、そういうの実際どうしてもどこか独りよがりのしょうもないものになってしまうので。

LINQは遅い/GCキツくなるというのは絶対的な事実ではあるのですけれど、極力書き味を失わないようにしつつ、6, 7割ぐらいのシチュエーションには特化した最適化を施し、何も考えずともむしろ普通に書くよりも速くなる。それ以外のシチュエーションでも、速さを意識した使い方をすれば、やはり普通に書くよりも速くなる。という、私的には理想的かな、というところで表現できたので、是非是非、機能を気にする人も、性能を気にする人も使ってみてください。どちらも満たせるものになっているはずです。

ところでしつこいですが、9/13にPhoton勉強会で「Photon Server Deep Dive - PhotonWireの実装から見つめるPhoton Serverの基礎と応用」というタイトルで話しますので、Photon興味ある人も、そうでなくてもUniRx興味ある人もどうぞ。LINQ to GameObject、或いはUnityとLINQについての話は、さすがにあんま関係ないのでセッション内容には含まれませんが懇親会ででも掴まえてもらえば何でも答えます。

C#のGCゴミとUnity(5.5)のコンパイラアップデートによるListのforeach問題解決について

UnityにおいてList<T>のforeachは厳禁という定説から幾数年。しかしなんと現在Unityが取組中のコンパイラアップデートによって解決されるのだ!ついに!というわけで、実際どういう問題があって、どのように解決されるのかを詳しく見ていきます。

現状でのArrayのforeachとListのforeach

まずは現状確認。を、Unityのプロファイラで見てみます。以下の様なコードを書いて計測すると……。

var array = new int[] { 1, 2, 3, 4, 5 };
var list = new List<int> { 1, 2, 3, 4, 5 };
 
// ボタンを叩いて計測開始
button.OnClickAsObservable().Subscribe(_ =>
{
    Profiler.BeginSample("GCAllocCheck:Array");
    foreach (var item in array) { }
    Profiler.EndSample();
 
    Profiler.BeginSample("GCAllocCheck:List");
    foreach (var item in list) { }
    Profiler.EndSample();
 
    // プロファイラでそこ見たいのでサッと止める。
    Observable.NextFrame(FrameCountType.EndOfFrame).Subscribe(__ =>
    {
        EditorApplication.isPaused = true;
    });
});

image

Unityのプロファイラは使いやすくて便利。というのはともかく、なるほどListは40B消費している(注:Unity上でコンパイラした時のみの話で、普通のC#アプリなどでは0Bになります。詳しくは後述)。おうふ……。ともあれ、なぜListのforeachでは40Bの消費があるのか。ってところですよね。foreach、つまりGetEnumeratorのせいに違いない!というのは、半分合ってて半分間違ってます。つまり100%間違ってます。

GetEnumeratorとforeach

foreachはコンパイラによってGetEnumerator経由のコードに展開されます。

// このコードは
foreach(var item in list)
{
}
 
// こう展開される
using (var e = list.GetEnumerator())
{
    while (e.MoveNext())
    {
        var item = e.Current;
    }
}

GetEnumerator、つまり IEnumerator<T> はクラスなので、ヒープに突っ込まれてるに違いない。はい。いえ。だったらArrayだって突っ込まれてるはずじゃないですかー?

// こんなコードを動かしてみると
 
Profiler.BeginSample("GCAllocCheck:Array.GetEnumerator");
array.GetEnumerator();
Profiler.EndSample();
 
Profiler.BeginSample("GCAllocCheck:List.GetEnumerator");
list.GetEnumerator();
Profiler.EndSample();

image

そう、むしろArrayは32B確保していてListはむしろ0なのだ。どっちも直感的には変てこ。

配列とforeachの最適化

配列をforeachで回すとコンパイラが、forループに展開します。

// このコードは
foreach (var item in array)
{
 
}
 
// こうなる
for (int i = 0; i < array.Length; i++)
{
    var item = array[i];
}

ちなみに配列のループを回すときは明確にLengthを使うと良いです。というのも、配列の境界チェック(自動で入る)が実行時に消せます。

// こっちよりも
var len = array.Length;
for (int i = 0; i < len; i++)
{
    var item = array[i];
}
 
//  こっちのほうが速い
for (int i = 0; i < array.Length; i++)
{
    var item = array[i];
}

詳しくはArray Bounds Check Elimination in the CLRをどうぞ。ようするに基本的には配列はforeachで回しておけばおk、indexを別途使う場合があるなら、Lengthで回すことを心がけるとベター。というところでしょうか。(もっというと配列の要素は構造体であると、更にベターなパフォーマンスになります。また、配列は色々特別なので、配列 vs Listで回す速度を比較すれば配列のほうがベタベターです)

List<T>のGetEnumeratorへの最適化

list.GetEnumeratorが0Bの理由は、ここにクラスライブラリ側で最適化が入っているからです。と、いうのも、List<T>.GetEnumeratorの戻り値はIEnumerator<T>ではなくて、List<T>.Enumeratorという構造体になっています。そう、特化して用意された素敵構造体なのでGCゴミ行きしないのだ。なので、これをわざとらしくtry-finallyを使ったコードで回してみると

Profiler.BeginSample("GCAllocCheck:HandConsumeEnumerator");
 
var e = list.GetEnumerator();
try
{
    while (e.MoveNext())
    {
        var item = e.Current;
    }
}
finally
{
    e.Dispose();
}
 
Profiler.EndSample();

image

0Bです。そう、理屈的にはforeachでも問題ないはずなんですが……。ここでちゃんと正しくforeachで「展開された」後のコードを書いてみると

using (var e = list.GetEnumerator())
{
    while (e.MoveNext())
    {
        var item = e.Current;
    }
}

image

40B。なんとなくわかってきました!?

using展開のコンパイラバグ

「List<T>をforeachで回すとGCゴミが出るのはUnityのコンパイラが古いせいでバグッてるから」というのが良く知られている話ですが、より正しい理解に変えると、「構造体のIDisposableに対するusingの展開結果が最適化されていない(仕様に基づいていない)」ということになります。この辺の話はECMA-334 C# Language Specificationにも乗っているので、C#コンパイラの仕様に対するバグと言ってしまうのは全然良いのかな?

どういうことかというと、現状のUnityのコンパイラはこういうコードになります。

var e = list.GetEnumerator();
try
{
    while (e.MoveNext())
    {
        var item = e.Current;
    }
}
finally
{
    var d = (IDisposable)e; // ここでBoxing
    d.Dispose(); // 本来は直接 e.Dispose() というコードでなければならない
}

そう、全体的に良い感じなのに、最後の最後、Disposeする時にIDisposableにボックス化してしまうので、そこでGCゴミが発生するというのが結論です。そして、これは最新のmonoコンパイラなどでは直っています、というか2010年の時点で直ってます。どんだけ古いねん、Unityのコンパイラ……。

40Bの出処

ゴミ発生箇所は分かったけれど、せっかくなのでもう少し。サイズが40Bの根拠はなんなの?というところについて。まずは色々なもののサイズを見ていきましょうー。

// こんなのも用意した上で
struct EmptyStruct
{
}
 
struct RefStruct
{
    public object o;
}
 
class BigClass
{
    public long X;
    public long Y;
    public long Z;
}
---
 
// 色々チェックしてみる
Profiler.BeginSample("GCAllocCheck:object");
var _0 = new object();
Profiler.EndSample();
 
Profiler.BeginSample("GCAllocCheck:class");
var _1 = new BigClass();
Profiler.EndSample();
 
Profiler.BeginSample("GCAllocCheck:int");
var _2 = 99;
Profiler.EndSample();
 
Profiler.BeginSample("GCAllocCheck:int.boxing");
object _3 = 99;
Profiler.EndSample();
 
Profiler.BeginSample("GCAllocCheck:emptyStruct");
var _4 = new EmptyStruct();
Profiler.EndSample();
 
Profiler.BeginSample("GCAllocCheck:emptyStruct.boxing");
object _5 = new EmptyStruct();
Profiler.EndSample();
 
Profiler.BeginSample("GCAllocCheck:bool.boxing");
object _6 = true;
Profiler.EndSample();
 
Profiler.BeginSample("GCAllocCheck:float.boxing");
object _7 = 0.1f;
Profiler.EndSample();
 
Profiler.BeginSample("GCAllocCheck:double.boxing");
object _8 = 0.1;
Profiler.EndSample();
 
Profiler.BeginSample("GCAllocCheck:refStruct.boxing");
object _9 = new RefStruct();
Profiler.EndSample();

image

なるほどなるほど。当たり前ですがstructのままのは0B。EmptyStructやboolなど最小1バイトのboxingは17B(ほえ?)、int(4バイト)が20Bでdouble(8バイト)や参照を一個持たせた(IntPtr - 64bit環境において8バイト)構造体が24B。classにlongを3つめたのが40B。そしてobjectが16B。つまり。つまり、最小が16Bで、そこからフィールドのそれぞれの要素のサイズが加算されるということです。

この16 bytesがどこから来ているかというと、オブジェクトのヘッダです。ああ、なるほどそういう……。

さて、これを踏まえてListのEnumeratorのフィールドを見てみると

public struct Enumerator : IEnumerator, IDisposable, IEnumerator<T>
{
	private List<T> l;
	private int next;
	private int ver;
	private T current;

ヘッダ16B + IntPtrの8B + intの4B + intの4B + Tがintの場合は4B = 36B。40じゃないじゃん、ってところは、32以降は8Bずつ埋まってくっぽ、実質33Bだと40B, 41Bだと48Bという感じ。といったところから40Bの消費になっていたということですね!

Experimental Scripting Previews

ついにコンパイラアップデートのPreviewがやってきた!Experimental Scripting Previewsにて、コンパイラのアップデートプロジェクトも始まっています。そして今のところ5.3.5p8-csharp-compiler-upgradeが配られています。

というわけで早速、冒頭の配列とListのforeachをかけてみると……

Profiler.BeginSample("GCAllocCheck:Array");
foreach (var item in array) { }
Profiler.EndSample();
 
Profiler.BeginSample("GCAllocCheck:List");
foreach (var item in list) { }
Profiler.EndSample();

image

やった!これで問題nothingですね!(実際は計測時は初回にListのほうに32B取られててあれれ?となったんですが、コンパイル後のIL見ても正常だし、まぁ二回以降叩いたのは↑画像の通りになったので、よしとしておこ……)

まとめ

で、現状はList<T>の列挙はどうすればいいのか、というと、まぁforでindexerでアクセスが安心の鉄板ではある。ForEachが内部配列に直接アクセスされるので速い説はなくはないですが、ForEachだとラムダ式のキャプチャに気を使わないと逆効果なので(詳しくはUnityでのボクシングの殺し方、或いはラムダ式における見えないnewの見極め方)、基本的には普通にforがいいと思います(なお、キャプチャのないように気を使えば、ForEachのほうが速度を稼げる余地はあります。理論上、正常になったforeachよりも良い場合があるため)

理想的にはforeachであるべきだし、改革の時はまもなく!(5.5に↑のコンパイラアップグレードは入るっぽいですよ)。ちなみに、あくまでコンパイラのアップグレードなだけで、フレームワークのアップデートや言語バージョンのアップデートは今は含まれてはいない。段階的にやっていく話だと思うので、とりあえずはコンパイラがより良くなる、というだけでも良いと思ってます。というか全然良いです。素晴らしい。

UniRx 5.4.0 - Unity 5.4対応とまだまだ最適化

UniRx 5.4.0をリリースしました!ちょうどUnity 5.4もリリースされたので、5.4向けの修正(Warning取り除いただけですが)を出せて良かった。というわけで5.4正式対応です。リリースは前回が5月だったので3ヶ月ぶりです。5.2 -> 5.3も3ヶ月だったので、今のとこ3ヶ月スパンになってますが偶然です。

何が変わったのかというと

Add: Observable.FrameInterval
Add: Observable.FrameTimeInterval
Add: Observable.BatchFrame
Add: Observable.Debug(under UniRx.Diagnostics namespace)
Add: ObservableParticleTrigger and OnParticleCollisionAsObservable, OnParticleTriggerAsObservabl(after Unity 5.4) extension methods
Add: UniRx.AsyncReactiveCommand
Add: ReactiveCommand.BindToOnClick, `IObservable<bool>.BindToButtonOnClick`
Add: UniRx.Toolkit.ObjectPool, AsyncObjectPool
Add: UniRx.AsyncMessageBroker, asynchronous variation of MessageBroker
Add: ObserveEveryValueChanged(IEqualityComparer) overload
Add: `Observable.FromCoroutine(Func<CancellationToken, IEnumerator>)` overload
Add: ObservableYieldInstruction.IsDone property
Add: IPresenter.ForceInitialize(object argument)
Improvement: Where().Select(), Select().Where() peformance was optimized that combine funcs at internal
Improvement: MicroCoroutine performance was optimized that prevent refresh spike
Improvement: Observable.Return performance was optimized that reduced memory cost
Improvement: Observable.Return(bool) was optimzied perofmrance that allocate zero memory
Improvement: Observable.ReturnUnit was optimzied perofmrance that allocate zero memory
Improvement: Observable.Empty was optimzied perofmrance that allocate zero memory
Improvement: Observable.Never was optimzied perofmrance that allocate zero memory
Improvement: Observable.DelayFrame performance was optimized
Improvement: UnityEqualityComparer.GetDefault peformance was optimized
Improvement: AddTo(gameObject) dispose when ObservableTrigger is not activated
Improvement: AddTo(gameObject/component) performance was optimized by use inner CompositeDisposable of ObservableDestroyTrigger
Improvement: `FromCoroutine<T>(Func<IObserver<T>, IEnumerator>)` stops coroutine when subscription was disposed
Improvement: ReactiveCollection, ReactiveDictionary implements dispose pattern
Fix: ToYieldInstruction throws exception on MoveNext when reThrowOnError and has error 
Fix: ObserveEveryValueChanged publish value immediately(this is degraded from UniRx 5.3)
Fix: Prevent warning on Unity 5.4 at ObservableMonoBehaviour/TypedMonoBehaviour.OnLevelWasLoaded
Fix: Remove indexer.set of IReadOnlyReactiveDictionary
Breaking Changes: Does not guaranty MicroCoroutine action on same frame
Breaking Changes: UniRx.Diagnostics.LogEntry was changed from class to struct for performance improvement

相変わらずへっぽこな英語はおいといてもらえるとして、基本的にはパフォーマンス改善、です。

前回紹介したMicroCoroutineを改良して、配列をお掃除しながら走査する(かつ配列走査速度は極力最高速を維持する)ようになったので、より安定感もましたかな、と。その他メモリ確保しないで済みそうなものは徹底的に確保しないようになど、しつっこく性能改善に努めました。あと新規実装オペレータに関しては性能に対する執拗度がかなり上がっていて、今回でいうとBatchFrameはギチギチに最適化した実装です。既存オペレータも実装甘いものも残ってはいるので、見直せるものは見なおしてみたいですねえ。

また、9/13日にPhoton勉強会【Photon Server Deep Dive - PhotonWireの実装から見つめるPhoton Serverの基礎と応用、ほか】で登壇するので、PhotonWireではUniRxもクライアント側でかなり使っているので、その辺もちょっと話したいなと思っていますので、Photonに興味ある方もない方も是非是非。Photon固有の話も勿論しますが、普通にUnityとリアルタイム通信エンジンについての考えや、UniRx固有の話なども含めていきますので。

Debug

Debugという直球な名前のオペレータが追加されました。標準では有効化されていなくて、UniRx.Diagnosticsというマイナーな名前空間をusingするようで使えるようになります。実際どんな効果が得られるのかというと

using UniRx.Diagnostics;
 
---
 
// [DebugDump, Normal]OnSubscribe
// [DebugDump, Normal]OnNext(1)
// [DebugDump, Normal]OnNext(10)
// [DebugDump, Normal]OnCompleted()
{
    var subject = new Subject<int>();
 
    subject.Debug("DebugDump, Normal").Subscribe();
 
    subject.OnNext(1);
    subject.OnNext(10);
    subject.OnCompleted();
}
 
// [DebugDump, Cancel]OnSubscribe
// [DebugDump, Cancel]OnNext(1)
// [DebugDump, Cancel]OnCancel
{
    var subject = new Subject<int>();
 
    var d = subject.Debug("DebugDump, Cancel").Subscribe();
 
    subject.OnNext(1);
    d.Dispose();
}
 
// [DebugDump, Error]OnSubscribe
// [DebugDump, Error]OnNext(1)
// [DebugDump, Error]OnError(System.Exception)
{
    var subject = new Subject<int>();
 
    subject.Debug("DebugDump, Error").Subscribe();
 
    subject.OnNext(1);
    subject.OnError(new Exception());
}

シーケンス内で検出可能なアクション(OnNext, OnError, OnCompleted, OnSubscribe, OnCancel)が全てコンソールに出力されます。よくあるのが、何か値が流れてこなくなったんだけど→どこかで誰かがDispose済み(OnCompleted)とか、OnCompletedが実は呼ばれてたとかが見えるようになります。

超絶ベンリな可視化!ってほどではないんですが、こんなものがあるだけでも、Rxで困ったときのデバッグの足しにはなるかなー、と。

BatchFrame

BatchFrameは特定タイミング後(例えばEndOfFrameまでコマンドまとめるとか)にまとめて発火するという、Buffer(Frame)のバリエーションみたいなものです。都度処理ではなくてまとめてから発火というのは、パフォーマンス的に有利になるケースが多いので、そのための仕組みです。Bufferでも代用できなくもなかったのですが、Bufferとは、タイマーの回るタイミングがBufferが空の時にスタートして、出力したら止まるというのが大きな違いですね。その挙動に合わせて最適化されています。

// BatchFrame特定タイミング後にまとめられて発火
// デフォルトは0フレーム, EndOfFrameのタイミング
var s1 = new Subject<Unit>();
var s2 = new Subject<Unit>();
 
Observable.Merge(s1, s2)
    .BatchFrame()
    .Subscribe(_ => Debug.Log(Time.frameCount));
 
Debug.Log("Before BatchFrame:" + Time.frameCount);
 
s1.OnNext(Unit.Default);
s2.OnNext(Unit.Default);

実装的には、まとめる&発火のTimerはコルーチンで待つようにしているのですが、今回はそのIEnumeratorを手実装して、適宜Resetかけて再利用することで、パイプライン構築後は一切の追加メモリ消費がない状態にしてます。

Optimize Combination

オペレータの組み合わせには、幾つかメジャーなものがあります。特に代表的なのはWhere().Select()でしょう。これはリスト内包表記などでも固有記法として存在するように、フィルタして射影。よくありすぎるパターンです。また、Where().Where()などのフィルタの連打やSelect().Select()などの射影の連打、そして射影してフィルタSelect().Where()などもよくみかけます(特にWhere(x => x != null)みたいなのは頻出すぎる!)。これらは、内部的に一つのオペレータとして最適化した合成が可能です。

// Select().Select()
onNext(selector1(selector2(x)));
 
// Where().Where()
if(predicate1(x) && predicate2(x))
{
    onNext(x);
}
 
// Where().Select()
if(predicate(x))
{
    onNext(selector(x));
}
 
// Select().Where()
var v = selector(x);
if(predicate(v))
{
    onNext(v);
}

と、いうわけで、今回からそれらの結合を検出した場合に、内部的には自動的にデリゲートをまとめた一つのオペレータに変換して返すようになっています。

MessageBroker, AsyncMessageBroker

MessageBrokerはRxベースのインメモリPubSubです。AndroidでOttoからRxJavaへの移行ガイドのような記事があるように、PubSubをRxベースで作るのは珍しいことではなく、それのUniRx版となってます。

UniRxのMessageBrokerは「型」でグルーピングされて分配される仕組みにしています。

// こんな型があるとして
public class TestArgs
{
    public int Value { get; set; }
}
 
---
 
// Subscribe message on global-scope.
MessageBroker.Default.Receive<TestArgs>().Subscribe(x => UnityEngine.Debug.Log(x));
 
// Publish message
MessageBroker.Default.Publish(new TestArgs { Value = 1000 });
 
// AsyncMessageBroker is variation of MessageBroker, can await Publish call.
 
AsyncMessageBroker.Default.Subscribe<TestArgs>(x =>
{
    // show after 3 seconds.
    return Observable.Timer(TimeSpan.FromSeconds(3))
        .ForEachAsync(_ =>
        {
            UnityEngine.Debug.Log(x);
        });
});
 
AsyncMessageBroker.Default.PublishAsync(new TestArgs { Value = 3000 })
    .Subscribe(_ =>
    {
        UnityEngine.Debug.Log("called all subscriber completed");
    });

AsyncMessageBrokerはMessageBrokerの非同期のバリエーションで、Publish時に全てのSubscriberに届いて完了したことを待つことができます。例えばアニメーション発行をPublishで投げて、Subscribe側ではそれの完了を単一のObservableで返す、Publish側はObservableになっているので、全ての完了を待ってSubscribe可能。みたいな。文字だけだとちょっと分かりにくいですが、使ってみれば結構簡単です。

UniRx.Toolkit.ObjectPool/AsyncObjectPool

UniRx.Toolkit名前空間は、本体とはあんま関係ないけれど、Rx的にベンリな小物置き場という感じのイメージでたまに増やすかもしれません。こういうのはあまり本体に置くべき「ではない」とも思っているのですが、Rxの内部を考慮した最適化を施したコードを書くのはそこそこ難易度が高いので、実用的なサンプル、のような意味合いも込めて、名前空間を隔離したうえで用意していってもいいのかな、と思いました。

というわけで、最初の追加はObjectPoolです。ObjectPoolはどこまで機能を持たせ、どこまで汎用的で、どこまで特化させるべきかという範囲がかなり広くて、実装難易度が高いわけではないですが、好みのものに仕上げるのは難しいところです。なのでまぁプロジェクト毎に作りゃあいいじゃん、と思いつつもそれはそれで面倒だしねー、の微妙なラインなのでちょっと考えつくも入れてみました。

// こんなクラスがあるとして
public class Foobar : MonoBehaviour
{
    public IObservable<Unit> ActionAsync()
    {
        // heavy, heavy, action...
        return Observable.Timer(TimeSpan.FromSeconds(3)).AsUnitObservable();
    }
}
 
// それ専用のPoolを<T>で作る
public class FoobarPool : ObjectPool<Foobar>
{
    readonly Foobar prefab;
    readonly Transform hierarchyParent;
 
    public FoobarPool(Foobar prefab, Transform hierarchyParent)
    {
        this.prefab = prefab;
        this.hierarchyParent = hierarchyParent;
    }
 
    // 基本的にはこれだけオーバーロード。
    // 初回のインスタンス化の際の処理を書く(特定のtransformに下げたりとかその他色々あるでしょふ)
    protected override Foobar CreateInstance()
    {
        var foobar = GameObject.Instantiate<Foobar>(prefab);
        foobar.transform.SetParent(hierarchyParent);
 
        return foobar;
    }
 
    // 他カスタマイズする際はOnBeforeRent, OnBeforeReturn, OnClearをオーバーロードすればおk
    // デフォルトでは OnBeforeRent = SetActive(true), OnBeforeReturn = SetActive(false) が実行されます
 
    // protected override void OnBeforeRent(Foobar instance)
    // protected override void OnBeforeReturn(Foobar instance)
    // protected override void OnClear(Foobar instance)
}
 
public class Presenter : MonoBehaviour
{
    FoobarPool pool = null;
 
    public Foobar prefab;
    public Button rentButton;
 
    void Start()
    {
        pool = new FoobarPool(prefab, this.transform);
 
        rentButton.OnClickAsObservable().Subscribe(_ =>
        {
            // プールから借りて
            var foobar = pool.Rent();
            foobar.ActionAsync().Subscribe(__ =>
            {
                // 終わったらマニュアルで返す
                pool.Return(foobar);
            });
        });
    }
}

基本的に手動で返しますし、貸し借りの型には何の手も入ってません!Rent後のトラッキングは一切されてなくて、手でReturnしろ、と。まあ、9割のシチュエーションでそんなんでいいと思うんですよね。賢くやろうとすると基底クラスがばら撒かれることになって、あまり良い兆候とは言えません。パフォーマンス的にも複雑性が増す分、どんどん下がっていきますし。

どこがRxなのかというと、PreloadAsyncというメソッドが用意されていて、事前にプールを広げておくことができます。フリーズを避けるために毎フレームx個ずつ、みたいな指定が可能になっているので、その完了がRxで待機可能ってとこがRxなとこです。

それと同期版の他に非同期版も用意されていて、それは CreateInstance/Rent が非同期になってます。

MessageBrokerと同じくAsyncとそうでないのが分かれているのは、Asyncに統一すべき「ではない」から。統一自体は可能で、というのも同期はObservable.Returnでラップすることで非同期と同じインターフェイスで扱えるから。そのこと自体はいいんですが、パフォーマンス上のペナルティと、そもそもの扱いづらさ(さすがにTのほうがIObservable[T]より遙かに扱いやすい!)を抱えます。

sync over asyncは、UniRx的にはバッドプラクティスになるかなあ。なので、同期版と非同期版とは、あえて分けて用意する。使い分ける。使う場合は極力同期に寄せる。ほうがいいんじゃないかな、というのが最近の見解です。

なお、Rent, Returnというメソッド名はdotnet/corefxのSystem.Buffersから取っています。

AsyncReactiveCommand

というわけでこちらもsync/asyncの別分けパターンで非同期版のReactiveCommandです。ReactiveCommandは何がベンリなのか分からないって話なのですが、実はこっちのAsyncReactiveCommandはかなりベンリです!

public class Presenter : MonoBehaviour
{
    public UnityEngine.UI.Button button;
 
    void Start()
    {
        var command = new AsyncReactiveCommand();
 
        command.Subscribe(_ =>
        {
            // heavy, heavy, heavy method....
            return Observable.Timer(TimeSpan.FromSeconds(3)).AsUnitObservable();
        });
 
        // after clicked, button shows disable for 3 seconds
        command.BindTo(button);
 
        // Note:shortcut extension, bind aync onclick directly
        button.BindToOnClick(_ =>
        {
            return Observable.Timer(TimeSpan.FromSeconds(3)).AsUnitObservable();
        });
    }
}

interactableの状態をコード実行中、というかつまりIO<T>が返されるまでfalseにします。連打防止でThrottleFirstがよく使われますが、それをより正確にコントロールしたり、また、引数にIReactiveProperty[bool]を渡せて、それを複数のAsyncReactiveCommandで共有することで、特定のボタンを実行中は他のボタンも実行できない、のような実行可否のグルーピングが可能になります(例えばグローバルでUI用に一個持っておけば、ゲーム中でUIは単一の実行しか許可されない、的なことが可能になる)

PresenterBase再考

PresenterBase、Obsoleteはつけてないのですけれど、GitHub上のReadMeで非推奨の明言を入れました。賢い基底クラスは悪。なのです。POCO。それはUnityにおいても何事においても例外ではない。その原則からするとPresenterBaseは賢すぎたのでナシ of the Year。動きはする、動きはするんですが……。

Model-View-Presenterパターン自体の否定ではなくて(それ自体は機能するとは思っています、ただし関心がModelにばかり向きがちですが、Viewは何か、Presenterは何か、についてもきちんと向き合わないとPresenterが奇形化するかなー、というのは実感としてある。ViewであるものをPresenterとして表現してアレゲになる、とか)、PresenterBaseというフレームワークのミスかな、とは。です。

とりあえずいったん初期化順序が気になるシーンは手でInitializeメソッド立てて、それをAwake, Startの代わりにして、呼ばせる。いじょ。みたいな素朴な奴で十二分かなー、とオモッテマス。結局。メリットよりもデメリットのほうが大きすぎたかな。反省。

この辺りに関してはアイディアはあるので、形にするまで、むー、ちょっと味噌汁で顔洗って出直してきます。

まとめ

あんまり大きな機能追加はなく細々とした変化なんですが、着々と良くはなっているかな、と!

Rxに関してもバッドプラクティスを色々考えられるというか反省できる(おうふ……)ようになっては来たので、どっかでまとめておきたいですね。油断するとすぐリアクティブスパゲティ化するのはいくないところではある。強力なツールではあるんですが、やりすぎて自爆するというのは、どんなツールを使っても避けられないことではあるけれど、Rxがその傾向はかなり強くはある。

まぁ、sync over asyncはいくないです。ほんと(思うところいっぱいある)。

というわけかで繰り返しますが、9/13日にPhoton勉強会【Photon Server Deep Dive - PhotonWireの実装から見つめるPhoton Serverの基礎と応用、ほか】で登壇するので、よければそちらも是非是非です。

Japan VR Hackathonに参加し、AMD賞受賞しました

Japan VR Hackathonに参加してきました!の結果が昨日発表されまして、AMD賞(Best Graphics)を受賞しました。やったー。事前に決めた5人チームでの参加で、大賞取る!という気概でやってたので、入選できて良かったです。

今回作ったのは「Clash of Oni Online」というゲームで、HTC Vive用のVRゲームです。テーマである”日本らしさ”を(一応)イメージした(一応)和風の装い。

飛んで来る岩を

吹っ飛ばす

という、VRバッティングセンター。ViveはVRというだけじゃなくてコントローラーがあるのがいいですねー。

今回、2日間 31時間で242コミット(最初のコミットが2016/06/18 09:13で 最後のコミットが 2016/06/19 16:34)。時間制限のなかでは、ちゃんとゲームしてる(ゲーム性的にもとりあえず爽快に全部打ち倒すパターンと一球一球を狙い撃ちしないパターンを用意)し、グラフィックもまぁ見栄えがするレベルで、オンライン協力プレイも実装(ただしデモ時はオフラインモード)したのは結構頑張った。ハッカソン系参加が全員初めての割には綺麗に収められた感あります。

チーム編成と最終的な役割は

  • 私(プログラマ):プロジェクトセットアップ、敵ボス挙動、サーバーセットアップ、エフェクト発注、雑進行管理、プレゼンスライド作成、動画撮影
  • プログラマ:マルチプレイプログラム、シーン管理プログラム
  • プログラマ:Viveプログラム、エフェクト組み込み
  • プログラマ:企画、地形エディット、サウンド、アセット検索、デモ
  • テクニカルアーティスト:アセット検索、敵モーション作成、ライティング、エフェクト

という感じでした。全員ほぼViveのプログラミング経験はなし(SDK入れて雰囲気掴んだことはあります程度)

最初に入れたアセットは

  • UniRx - ないと無理
  • LINQ to GameObject - ベンリ、だけど今回は別にあまり使わず
  • PhotonWire - マルチプレイ前提なので。合わせてサーバー側プロジェクトもセットアップ
  • SteamVR Plugin - Viveがターゲットなので
  • The Lab Renderer - 使ったことなかったので結局あんま余裕なく使えなかった…

という編成から随時アセット追加追加。

タイムライン

1週間ぐらい前に参加を決める。3日前ぐらいに「2人マルチ協力プレイ」「背中合わせにして立ちまわる(時代劇にある格好いい感じのアレ)雑魚戦 + デカい鬼を撃退するボス戦というアクションゲーム」「グラフィックで魅せる」を軸にする。というのを決定。和風です、和風。コミュニケーション手段としてSlack(チャット)を前日に立てる。Unityのバージョンを5.4.0b21に決めて全員にインストールしておいてもらうように。持ち込み物としてHTC Viveを2台用意。当日、会場ついてからGitHub(リポジトリ管理)のPrivateリポジトリを立てて全員招待。

雑魚戦+ボス戦といっても、作りきれるか怪しいので(実際ボス戦で手一杯だった、そりゃそうだ)、ボス戦から先に作っていくように。最初に決めていた役割分担は

  • 私(プログラマ):マルチプレイ
  • プログラマ:ボスプログラム、プレイヤー行動プログラム
  • プログラマ:ボスプログラム、プレイヤー行動プログラム
  • プログラマ:地形アセット購入/組み込み・サウンド購入/組み込み、パーセプションニューロン触る(使えそうなら使う)
  • テクニカルアーティスト:モデルやらモーションやらエフェクトやら

でした。まぁマルチプレイといってもUnity側のプログラムがある程度できないとやることもないんで、まずは自分で持ってきたViveを自前PCのSurface Bookに繋ごうとしたらSurface Bookの出力をViveがうまく認識してくれなくて最終的に諦め(Forumとか見た限りだとノートPCの出力端子とのトラブル事案は結構多い模様なのでshoganaiね)。ということでViveのプログラミングは他の人に完全に任せることにして、ボスのプログラムを作っていこうかなー、ということに。ボス戦は、崖のような場所にボスが立っている(下半身は見せない)というイメージが共有されたので

迫り来るシリンダー撃退ゲーとして作成。雑に作ったこのシーンは、初日ずっとフル活用されることになったのだった。動画系は常時GifCamで撮ってSlackにあげてました。イメージが瞬時に共有されますし、良い内容だとテンションも上がりますし。

この時点でLeanTweenを追加。マルチプレイできるようにするので、非確定要素をいれないように、というのとそんな凝ることもないしなので弾は全部トゥイーンで制御しようかと。トゥイーンライブラリは色々ありますが、今のとこ私が選ぶならDOtweenかLeanTweenかなぁ。普通だったらDOtween選びます。ただ、今回はLeanTweenにしました、ちょっと慣れておこうかな、と思って。LeanTweenは複数のTweeenの制御とかの補助が入ってないんですが、その辺はUniRxで制御させたので全く問題なし。基本的に完了などのイベントをObservable化すれば既存トゥイーンライブラリとUniRxの統合は容易ですし、かなりいい具合にコントロールできます。実際今回は色々な挙動をそれで組みました。ところで、今ひっそりとUniRx前提のハイパフォーマンスでリアクティブなトゥイーンライブラリを作っているので、それが完成したら基本的にそれしか使いません:) まぁ、というのもあって色々なトゥイーンライブラリを試しているというのもあります。

このシーンをプレイヤー行動プログラム側に渡して、VIVEで弾き返したり防いだりを作ってもらう感じに。パーセプションニューロン触るマンはパーセプションニューロンを触りつつサウンド探しを、テクニカルアーティストはボスのモデル(これ自体は買ったもの)のUnityへのインポートとテクスチャ調整とモーション付けを、私は弾のバリエーションを作ってました。

豪速球を投げ込むバッティングセンター的イメージ。手前で弾が伸びてくるので振りにくい。二者択一(手前のキューブはプレイヤーAとプレイヤーBです、マルチ協力プレイだから!)でどっちに来るか分からないので、なんとなく緊迫感あってゲーム的でもあるよね、ということで最終的なボス行動にも採用。

ぶわーっと全方位に出すのが欲しい(VIVEのデモゲームのThe LabにあるXortexという360度シューティングのボス弾のような)というオーダーを受けて。中々いい感じに派手なので、これも採用。

あとはボツ案的な弾を作ったり、その他、この時点ではまだ夢膨らみんぐで、ボスの行動も腕をばちーんと振り下ろしてきてそれを斬撃の連打で防いで弾き返す(協力プレイなので、片方が防いでたらそれに加勢しないとダメ、とか)、などを想定したコードを準備したりAIは少しリッチにしようかな、とBehaviorTreeのライブラリ書いてたり(ノードエディタなしの基本的なランタイムだけ)、プレイヤープログラム作るチームは盾で防御する処理(最終的に削られたけれど最初は刀と盾の装備のつもりだった)をやってたら、夜0時。うーむ、時間が過ぎるのは早い。

この辺でさすがに未だにシリンダーとキューブが相手で完成形が全く見えてないのはヤヴァいでしょうということで、シーン統合しましょう祭り。特にテストで大量のアセットを抱えていたマップ作るマンがGitHubに中々Pushできないなどなどトラぶりつつも、2時ぐらいにようやく一段落。マップにモーション付きボスモデル配置して、とりあえず弾を出るようにして、でこんな具合に。

ezgif com-resize

色々アレですが、しかし中々格好よくてテンションあがりますね!その他プレイヤーのほうも入れこんだりなんなりで床に転がって仮眠とって、朝。キューブにも岩を当てはめてついに出来上がったのは……!

image

んー、悪くない。悪くないんだけど、和ではない。雲南省(適当なイメージ)とかそういう中国の高山っぽい気すらする。ここで実際完成させる仕様を概ね確定。

  • マップは明確に和テイストが出るものにリテイク
  • ボスは殴り攻撃などなし、弾のみ
  • 盾はなし、弾を打ち返してボスにダメージ与えて、一定回数食らわせたらクリア
  • マルチプレイは諦めないので作業は並走、ただし最終的にはシングルプレイが完全にプレイできるの優先

私は、ボスのモーションが二種あって、殴りつけてくるつもりでつけてもらったモーションはボスが弾を投げ飛ばしてくる(つもり)な雰囲気に適当に調整(タイミングは適当にdelayかけて目視で合わせただけでジャストとは程遠いんですが、まぁなんとなくそれっぽく見えなくもないのでヨシとした)したり、もう一個の大技っぽくやってくるモーションは、なんか岩を抱え込んでる感じにできそうな気がしたので、適当にそれっぽく位置合わせして破裂させてみることに。

resize

うん、それっぽい。地面にめり込んでってるのとかも、まぁ全然気にならないし。このボス行動は今回のハッカソンで私的な私が作った中では一番よくやりましたしょうでした。全部、偶然素材が揃っただけなんですがうまく噛み合ったということで。

この後は、マップを和テイストに差し替えて常時マップブラッシュアップ、ゲームの要素が確定したので、各種のヒットエフェクトを作ってもらって当て込みや効果音、プレイ感向上のための弾の動きなどの調整、そして諦めてないマルチプレイなどなどを時間ギリギリまで使ってなんとか完成……!(実際、最後の30分でボスのダメージエフェクトがつき、最後の15分前でボスが死ぬようになった程度にギリギリだった)

マルチプレイに関しては、Viveのセンサーが干渉してうまく二台プレイの調整ができなかったのと、もう一台のデスクトップPCを会場の無線LANに繋げなくて、というネットワーク的な問題で断念。いちおう、プログラム側はマルチ想定で動作するように最後まで組んでました、いやほんと。サーバー側、AzureのVMも一時的なものなのでということで、かなりマシンパワーの強いものに変更したりしたんですけどね、というわけでここをお見せできなかったのは残念。なので、最後の5分でマルチプレイ用のログイン待機処理を消して、リリースビルド完成。お疲れ様ー。

完成形

出来上がったものは、マップリテイクによって城が追加されたことにより「城下まで迫ってきた赤鬼を、手に持つサムライブレードにより撃退し、城を守る」という設定に。なっていた。完全後付けで。ゲーム名は特に何も考えもなく直前で「Clash of Oni Online」に大決定。

ハッカソンでの評価は特に会場でのプレゼンはなく、体験してもらって審査員が表を付けてく形式とのことで、あとはデモマンがいい感じに来場者に説明してるのを横目に私は会場を見学する:)最後の一秒までドタバタと調整を続けていた割には、目立ったバグもなくスムースにプレイできてて良かった良かった。

最終的には当日の審査ではなく後日の審査ということなので、プレゼン資料を作ったり動画を撮ったりして

Clash of Oni Online - VR Multiplay Sword Action from Yoshifumi Kawai

結果待ち……!そして発表……!受賞……!やったね!

反省点とか

当初の想定よりもViveのルームスケールを活かしてない、直立不動のスタイルになったのは、ちょっと想定外。弾を避けたりとか、近寄ったりとかもうちょっとだけアクティブなのをイメージしていたので、しょうがないといえばしょうがないのだけれど、次に何か作るのだったら動くタイプのを作りたいですねぇ。

効果音が足りなかったり、割れてたり。効果音足りないのは、岩の音を、ボス撃破音とか足すべき箇所はいっぱいですよねー。マップリテイクで時間が取れなかったのがその辺の敗因か。Viveコントローラーと刀の位置が微妙にあってなかったり、足が地面に設置してなかったりといった、プレイヤーに対する調整も甘め。shoganai。この辺はViveプログラミングにて慣れてれば、スッと合わせられる話だと思うので、経験値を積もう。ボスの全方位弾が実は全方位じゃなくて左に寄ってるのは普通にロジックのバグ……。リテイク後のマップのクオリティが急ぎで用意しただけあってリテイク前に比べると低い(雑に光源足すためだけの灯籠を並べるとかしたかった)、ボスを遠方に置く形になってしまったのでスケール感が出なかった、など。

とか、まぁアラはいくらでも見つかりますが、基本的にはよくやったと思ってる……!よ!チームメンバーが全員、より良くするために自分の仕事を探して作りきっていったというのは純粋なハッカソンの楽しさという感じで、疲労困憊だけど達成感はありますね。

それとViveでのプログラミングは、結構ゲーム作成入門(Unity入門)にいいかもですね。3Dのプレイヤーの操作ってモーションつけたり色々ハードル高いですが、Viveならすぐに手の動きがキャプチャされて自由に動かせるアクションが作れるので、よくあるシューティングとかブロック崩しとか作っていくよりも楽しいんじゃないかな。(今のVR経験値が少ない現状なら)VRで空間を見て、Viveコントローラーで自由に操作できるというのは、それだけで楽しい体験を作れちゃいますしね。

そんなわけで、家でもViveを設置したしGeforce GTX 1080搭載PCも買ったので、ちょいちょいとVive用に何か作っていきたいという気持ちを強くしました。ので、ちょいちょいと出していければいいですねー。

Prev |

Search/Archive

Category

Profile


Yoshifumi Kawai
Microsoft MVP for Developer Technologies(C#)

April 2011
|
July 2020

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