Unity

決定論的な物理演算を実現するdeterministic-physicsを導入する上でつまづいたこと【Unity】

Unity
記事内に商品プロモーションを含む場合があります

Unityで決定論的(同じ入力から同じ結果を得られる)な物理演算をしたい場合、標準の物理エンジンであるPhysXでは実現できず、現状Unity公式からサポートされているものとしてはDOTSのUnityPhysicsがあります(以降はDOTS-Physicsと表記します)。

しかしDOTS-Physicでは内部でfloatを扱うため、同じハードウェア上では決定論的になってもクロスプラットフォームではfloat誤差により実現できません。

これに対してDOTS-Physicの中身をfloatではなく内部的にuintを使うように置き換えてくれたものがdeterministic-physicsです。

決定論的な物理演算ができると何が嬉しいのかは、こちらの記事などが参考になると思います。

開発中のアプリではdeterministic-physicsを実際に導入して、無事にAndroid・iOS上で動作させることができました。

しかしDOTS上で動作する都合上、導入する上でハードルの高い部分も多く問題も多発しました。この記事ではそれらについてまとめていきます。

具体的な使い方やコードなどはこの記事では解説しません。作者が上記のGitHub上にサンプルコードを用意してくれているので、そちらを参考にして下さい。

宣伝

deterministic-physicsを導入したアプリ『リモートダイス3D』はAppStore・GooglePlayにて配信中です!

AppStoreからダウンロード Google Playで手に入れよう

iOS / Appleシリコン搭載Mac / Android で物理演算の完全一致を実現しています。

導入は慎重に検討を

派生元となっているDOTS-Physicsは、この記事を書いている時点での最新バージョンが2021年01月18日にリリースされた「0.6.0-preview.3」です。

そもそもPreview版である上に、まるっと1年放置されています。

それを引き継いだdeterministic-physicsも、DOTS-Physicsの「0.6.0-preview.3」に対応する形でアップデートされたところで更新がストップしています。

今後更新されるかどうか不明なものをプロダクトに導入することは慎重に検討しなければいけないでしょう。後述しますが、早くもUnityのバージョンを2020.3.26f1にアップデートしたところで動作しなくなってしまいました。

今回は個人開発かつ限定的な用途、そもそもゲームではないということもあり即座に選択肢から外したのですが、コストをかけられるならPhotonQuantumを検討するのが良いかと思います。

unity-deterministic-physicsの機能はおおむねDOTS-Physicsと同じなので、使い方についてはDOTS-Physicsのドキュメント等を参照することになります。

ハードル1:変換コンポーネントが用意されていない

DOTS-Physicsには従来のTransform、RigidbodyやColliderからDOTSへ変換する機能(GameObjectベースとのハイブリッド)と、最初からDOTS-Physics用にエディタ上で設定ができる各種コンポーネントが用意されています。

これらは「Unity.Physics.Editor」「Unity.Physics.Hybrid」という2つのアセンブリに組み込まれているのですが、deterministic-physicsには移植されていません。

要するにdeterministic-physicsにはエディタ上で設定できるものが何一つありません。すべてスクリプト上に記述することになります。

これはfloat誤差を防ぐ上では安全といえば安全(例えば座標や回転などの値がズレてしまうのを防ぐために、Transformから変換するよりもuintのままシリアライズしておいた方がいい)なのですが、とはいえこの状態で作るのは大変なケースが多いと思います。

この問題についてはissueにも上げれていますが、作者から「いつか私に自由な時間ができたら導入されるかもね」とコメントされています。

DOTS-Physicsからの移植も試みましたが、すべてを移植するのはあまりにも書き換える部分が多くて断念しました。

Rigidbodyにあたる処理についてはdeterministic-physicsのサンプルコードを参考にしつつ実行時に生成、Colliderに関しては今回はBoxColliderとMeshColliderしか使用しなかったので、この2つを変換するための処理だけDOTS-Physicsを参考に作成しました。

具体的には、実行時に変換するのではなくエディタ上で「パラメータへ変換」するためのボタンを用意しておき、これを押すことでuintへ変換した値を保存。実行時にこの値を使ってColliderを生成するという具合です。

Convexの実装を断念した

この過程で諦めたことが従来のMeshColliderに用意されている「Convex」のチェックです。

MeshColliderのConvexMeshColliderのConvex

チェックを付けないとMeshColliderに設定したメッシュの頂点をすべて使用したコライダが生成されてしまうため、パフォーマンス的に厳しくなるやつですね。

従来のPhyicsではチェックを付けていないコライダは動作をしないオブジェクトにしか利用できず、Rigidbodyを付ける場合はチェックが必須でした。

DOTS-Physicsでは動くオブジェクトでもConvex無しが許容されています。

deterministic-physicsにもConvexにあたるコライダを生成する機能があるのですが、いろいろと試してみてもうまく生成できなくて地面にめり込んでしまうなどの現象が起きました。

DOTS-Physicsで変換コンポーネントを使った場合は普通に動いていたのでうまく移植さえできれば動作すると思うのですが、コードを見ても意味不明な部分が多く…。

今回は使用したアセットの中にコライダ用に最低限の頂点だけ残したメッシュが含まれていたので、Convexは諦めることにしました。誰か作ってくれないかな(チラッ)。

普通のMeshColliderならメッシュから情報を取り出して流し込むだけで生成できるので、比較的簡単でした。

Convexじゃないことが原因なのか不自然な動きをすることがたまにあるのですが、Convexにすることで確実に治る保証もないので今回は妥協することにしました。

ハードル2:sfloat対応がとても大変

deterministic-physicsではuintをfloatのように扱えるようにしてくれている「sfloat」という型を利用します。

普通のUnity開発でよく使われる「Math」や「Vector3」は当然ながら利用できないのですが、幸いなことにUnity公式から出ているシェーダ言語ライクに数学系の処理を記述できるライブラリ「Mathematics」はsfloat用に移植されています(Mathematicsのどのバージョンを移植したのかは不明です)。

deterministic-physicsを導入したらusingを「Unity.Mathematics」を「UnityS.Mathematics」に書き換えるだけでsfloat版が利用できるようになります。

Mathematicsに用意されていない数学系の処理をsfloatに対応するには、自分でメソッドを用意していくことになります。

Convexの実装を諦めたのもコレが理由です。従来のメソッドと同じ処理ができるMathematicsのメソッドはどれ?そもそも用意されてる?といったことが分からないものも多く、大変すぎました。

ハードル3:決定論を実現するための制約がある

これは他の方法を使った場合でも同じですが、クロスプラットフォームで完全に一致する動作を実現するには守らなければいけない制約がいくつかあります。

deterministic-physicsのサンプルを見ればわかるのですが、処理の実行順がズレないようにマルチスレッドを封印しています。

そしてfloatではなくsfloatを使うため、基本的にdeterministic-physicsでは実行速度が大幅に低下します。といっても大量のオブジェクトを処理させなければ大丈夫な範囲かと思います。

また、物理に影響する処理はすべてECS上の「FixedStepSimulationSystemGroup」の中で実行しなければいけません。

処理の実行はFixedStepSimulationSystemGroupによって指定した時間間隔で実行されるので、FPSが安定していなくても動作は一致します。ちなみにTime.TimeScaleをいじった場合でも大丈夫です。0.5倍速でも3倍速でも同じ動作になります。これは大きなメリットですね。

普通に使う上で難しいのはオブジェクトの生成順や生成タイミングを一致させることかと思います。マルチプレイゲームで使うには工夫が必要になりそうです。

今回は物理演算をシミュレーションするために必要なパラメータを1度だけ送信して相手側のデバイスで再現するような用途だったので、ここに関しては大丈夫でした。

アーティストが事前に決めたパラメータを使って、物理挙動を入れたデモシーンなどを作るのには向いているかもしれません。

問題と対応1:Unity2020.3.25f1までしか使えない

Unity2020.3.26f1にアップデートしたら実行中にNativeArrayを使っている特定の箇所でエラーが出るようになりました。

deterministic-physicsに起因するものではなく、DOTS-Physicsでも発生します。

試しにエラーが発生した箇所だけコメントアウトしてみても、他の場所でも同様のエラーが出ていました。

リリースノートを確認したところNativeArrayに関するアップデートが入っているようなので、これが原因ということで間違いなさそうです。

LTSバージョンに入れる前にUnity2021.2あたりに入れて検証してほしかったところですが、最終更新が1年前のパッケージでは致し方ない気もしますね。

原因を特定してエラーが出ないように修正することもできると思いますが、大変そうなので今回はUnity2020.3.25f1でアプリをリリースすることにしました。

問題と対応2:IL2CPPで起動時にクラッシュ

IL2CPPかつDevelopmentBuildを外した状態でアプリをビルドすると、起動時にクラッシュしました。

しばらくDevelopmentBuildを外してビルドをしていなかったので原因の特定が難航しました。

原因特定までの余談

まずiOSでクラッシュしてAndroidではクラッシュしなかったので「iOSかつDevelopmentBuildを外した時」だと想定していました。

起動直後にクラッシュしてしまうためログも確認できず、Xcodeに表示されるスタックトレースも意味不明。

deterministic-physicsのサンプルプロジェクトでは発生していなかったので、deterministic-physicsとは関係ないところに原因があると思い色々と探っていました。

そのあとAndroidではMonoでビルドしていることに気づき、IL2CPPに変更したらクラッシュしました。

この段階ではAndroidでもログが取れていなかったのですが、最初に実行するシーンの中身を空にしてカメラだけ置いたらクラッシュを防げるようになり、エラーログを確認できました。

UNITY_DISABLE_AUTOMATIC_SYSTEM_BOOTSTRAPで解消

エラーの発生箇所はEntitiesパッケージのAutomaticWorldBootstrap.Initialize()内で実行されているGameObjectSceneUtility.AddGameObjectSceneReferences()でした。

具体的な原因はあんまりよくわかっていませんが、このメソッド中でBuildSettingsに設定されているビルド時に含めるシーンの一覧にアクセスしています。

そのとき、IL2CPPかつDevelopmentBuildを外した時だけ参照をうまく取得できないようです。

サンプルプロジェクトではクラッシュしていなかったのもBuildSettingsにシーンが設定されていなかったことが理由でした(プロジェクトにシーンが1つしか存在しなかったのでそれでも動く)。

上記のコードを見ればわかる通り、この処理は「UNITY_DISABLE_AUTOMATIC_SYSTEM_BOOTSTRAP」をScriptingDefineSymbolsに追加することで無効化できます。

その場合、ECSのWorldが自動で生成されなくなるので上記コード中のDefaultWorldInitialization.Initialize()だけ別の場所で実行する必要があります。

iOSはIL2CPPビルドが必須なので、こちらの対応が必要になるかと思います。

ECS x IL2CPP は何かと問題を発生させてしまうようですね。

まとめ

何かと導入する上で大変なdeterministic-physicsですが、これのおかげで物理エンジンを自作せずに済んだので作者の方には大変感謝しています。

プロダクトに実践投入する方はあまりいないと思いますが、もし導入に挑戦することがあればこの記事が何かの参考になれば幸いです。

追加の情報があればまた記事に追記していきます。もっと具体的な使い方も需要がありそうであれば書くかもしれませんが、DOTS-Physicsが更新されて正式リリースに向けて動いていることが判明してからにしたいですね。