はじめに
この記事はVRChatにおけるUdonおよびUdonSharp(U#)を使った際の備忘録です。
これからUdonを使い始める人のために書き連ねておきます。
この記事は2020/5/26現在のVRChatを前提に書いています。
Udonとパフォーマンスチューニング
Udonは実質、UnityのAPIをラップして呼び出しているだけにすぎません。
そのためUnityでプログラミングをするときと同じ様にパフォーマンスにもこだわる必要があります。
プロファイラを見よう
Unityには標準でProfilerという機能が備わっています。
1フレーム単位でどのような処理が実行され、それにどれくらいの時間がかかっているかをチェックすることができます。
詳しい使い方はこちらを参考にしてください。
- Profiler ウィンドウ
- 【Unity】CPUプロファイラでパフォーマンスを改善する 前編
- 【Unity】CPUプロファイラでパフォーマンスを改善する 後編
- 【Unite 2017 Tokyo】最適化をする前に覚えておきたい技術
なお、プロファイラで動作を監視すること自体がかなりの負荷となります。
プロファイラを使っている間はfpsがガタ落ちしますが、しょうがないと割り切ってください。
(普通のUnity開発なら回避策があるのですが、VRChatだと仕様上どうしようもできないです)
Raw Hierarchyで調査
プロファイラの表示をRaw Hierarchyに切り替えて時間がかかっている順にソートすると何がボトルネックか調査することができます。
とくにこのモードだとUdon VMの中身の実行順も見ることが出来ます。
Udonの仕様上、どのスクリプトであるか名前はわからないのですが、メソッド呼び出しの様子からどのスクリプトかあたりをつけることはできます。
GCアロケートを避けよう
とくに負荷の原因となりやすいものはGC Allocと表示されているものです。
これはUnity上で、プログラムを実行するために必要なメモリを確保する動作を表しています。
(GCアロケートと呼ぶ)
そしてこの確保したメモリですが、解放される瞬間にVRChatが一瞬フリーズしてしまいます。
(GC(ガベージコレクタ)が実行される、と呼びます)
GCが実行される頻度は少ないほどfpsに与える影響は小さくなります。
逆に高頻度でGCが実行されると、体感できるレベル(ひどいと数十fps)で影響がでてきます。
そのためGCの実行をさける、つまりGC Allocの頻度を下げる工夫が必要となります。
stringは避けよう
C#の仕様上、string(文字列)は定義するだけでかなりのGC Allocを引き起こします。
そのためUpdate()で毎フレーム文字列を生成するなどしていると、パフォーマンスにかなりの悪影響を及ぼします。
極力stringは使わない、使うにしても必要なタイミングで必要なだけ生成する工夫が必要です。
U#は別コンポーネントのメソッド呼び出しがコスト
非常に便利なUdonSharpですが、見えないところでコストがかかります。
それはUdonSharpBehaviourから別のUdonSharpBehaviourなオブジェクトのメソッドを呼びだす時です。
たとえば、次のようなU#スクリプトがあったとして。
usingUdonSharp;usingUnityEngine;namespaceDebugTest{publicclassRunner:UdonSharpBehaviour{privateRigidbody_rigidbody;voidStart(){_rigidbody=GetComponent<Rigidbody>();}publicVector3GetCurrentVelocity(){return_rigidbody.velocity;}}}usingUdonSharp;usingUnityEngine;namespaceDebugTest{publicclassObserver:UdonSharpBehaviour{[SerializeField]privateRunner_runner;privatevoidUpdate(){// 取得するだけで何も使わないvarvelocity=_runner.GetCurrentVelocity();}}}これのObserver.cs側をUdon Assemblyにトランスパイルした結果をみるとこうなっています。
_update:
PUSH, __0_const_intnl_SystemUInt32
# {
# var velocity = _runner.GetCurrentVelocity();
PUSH, _runner
PUSH, __0_const_intnl_SystemString
EXTERN, "VRCUdonCommonInterfacesIUdonEventReceiver.__SendCustomEvent__SystemString__SystemVoid"
PUSH, _runner
PUSH, __1_const_intnl_SystemString
PUSH, __0_intnl_SystemObject
EXTERN, "VRCUdonCommonInterfacesIUdonEventReceiver.__GetProgramVariable__SystemString__SystemObject"
PUSH, __0_intnl_SystemObject
PUSH, __0_intnl_UnityEngineVector3
COPY
PUSH, __0_intnl_UnityEngineVector3
PUSH, __0_velocity_Vector3
COPY
PUSH, __0_intnl_returnTarget_UInt32 #Function epilogue
COPY
JUMP_INDIRECT, __0_intnl_returnTarget_UInt32
注目して欲しいところは、他のUdonSharpBehaviourへのメソッド呼び出しがSendCustomEventとGetProgramVariableに変換されているところです。
(メソッドに引数を渡すとSetProgramVariableも追加される)
そしてこのSendCustomEventとGetProgramVariableですが、なぜかGC Allocします。
ということで、U#を用いた場合、気軽にメソッド呼び出しを実行するとそれだけでGC Allocが発生します。
普通のUnity開発ではノーコストな操作が、U#ではコストがかかる点はかなり罠な気がします。
OnTriggerStay大暴走
UdonBehaviourにはOnTriggerStayが定義されています。
そのため「VRC_Pickup + UdonBehaviourなオブジェクト」を一箇所に大量にまとめて配置するとOnTriggerStayが暴走します。
数個程度なら問題ないですが、数十個レベルで一箇所にまとめるとfpsがガタ落ちするレベルで影響がでてきます。
アイテムを一箇所にまとめておいて擬似的な「無限湧き」を作るようなことはやめておきましょう。
Udon Synced Variables役に立たない問題
結論からいうとUdon Synced Variablesはパフォーマンスのために 「使わない」が正解です。
UdonにはUdon Synced Variablesという機能があります。
こちらは指定したプリミティブな変数をネットワークをまたいで同期する機能です。
(U#でいうところの[UdonSynced])
ですがこのUdon Synced Variables、挙動が結構ヤバイです。
Ownerは常時パラメータを相手に送信し続ける- 転送量が増えるとパケットロスして不着となる
- スループットがかなり低い
同期するオブジェクト、変数の数が増えるとDeath Run Detected: dropped N eventsというエラーが大量に出てきます。
これが発生してしまうと、変数同期の成功率が極端に下がってしまいます。
そのため、Udon Synced Variablesで大量のデータを同期することはまったくオススメできません。
たとえば、オブジェクトの位置と姿勢(Vector3 + Quaternion)をUdon Synced Variablesで同期するのは止めたほうがいいでしょう。
私が試した場合ではオブジェクト数が20個を超えたあたりからパケロスが発生しました。
さらにVRChatの通信にかなりの負荷をかけるためか、Playerの挙動までもが不安定になりました。
ちなみに、この仕様ではほぼ使い物にならないのでフィードバック報告済みではあります。
補足: Udon Synced Variablesについての公式フォーラムでの報告
VRChatのフォーラムのこちらの投稿では次のように報告されています。
- 2つの
Udon Synced Variablesな文字列をもつUdon Behaviourをシーンにいくつか配置 - 8個置いた程度ではパケロスはほぼゼロ
- 16個置くとパケロスが発生する
とのことなので、Udon Synced Variablesを使う場合はオブジェクト数が少ない場合のみにした方が無難でしょう。
文字列にエンコードして同期する、は高コスト
また、とある場所で「オブジェクトの状態をstringにエンコードしてUdon Synced Variablesで同期する」という手法が提案されていました。
Udon Synced Variablesで配列が同期できないのを回避するために編み出された手法ですが、こちらかなりコストが高いです。
- 大量の
stringを生成することによるGC Alloc - 長い文字列を常時伝送するネットワークへの負荷
そのため本当にどうしようもないときの最終手段としとっておいて、常用はしないほうが無難でしょう。
(とはいえどこれしか方法が無いならば使わざるを得ないのがUdonのツライところなのですが…。)
位置同期
オブジェクトの位置を同期する方法ですが、次の2とおり(実質1とおり)があります。
- A:
Udon Synced Variablesで位置姿勢を送る - B:
Udon BehaviourのSynchronize Positionを使う
Aのパターンは前述の問題があるのでオススメできません。
ということで実質的にBの「Synchronize Position」一択になります。
このSynchronize Positionはちゃんと差分同期してくれるため、大量にオブジェクトがあってもネットワークへの負荷は小さいです。
Synchronize Positionの同期ズレ問題
Synchronize Positionは差分同期してくれるためネットワーク負荷は小さいのですが、大量にオブジェクトがある場合、後からワールドに参加した人には正しく位置と姿勢が同期されない場合があります。
こちらはワールドにいるプレイヤー数とオブジェクト数によりますが、「5人以上かつ20個くらいオブジェクトを動かした」あたりから発生してきます。
原因はハッキリとはしていないのですが、どうも次の複数の問題が絡んでいるっぽいです。
- オブジェクトの
Ownerが新規参加した人に正しく同期されず、MasterがOwnerにみえる問題 Ownerがオブジェクトの位置同期が完了しない問題
前者についてはバグ報告済みですが、後者についてはいまいち挙動がつかめていないため報告していません。
Synchronize Positionの同期ズレ対策
対処療法として、次の対策をいれましょう。
実際にモノレールワールドで実施している対策がこれです。
触れていないPickupオブジェクトはすべてMasterが所有権をもつ
- オブジェクトを持っている間は持っている人に
Ownerを渡す - 手を離したら
Masterに所有権を返すようにする - 若干安定するが、それでもまだ同期ズレは起きる
- オブジェクトを持っている間は持っている人に
強制的に位置を同期する仕組みをいれる
Master側で同期対象のオブジェクトをすべて少しだけ位置と姿勢をズラす- 数秒後に元の位置姿勢に戻す

(同期ズレの発生をゼロにはできないので、同期ズレが起きる前提で対策した方が早い)
かなりツラミがある仕組みですが、現状これくらいしか大量のオブジェクトを安定して同期する方法がありません。
まとめ
Udonつらいし、UdonSharpも結構ツライです。
それなりのUnity開発経験と、Unityでパフォーマンスチューニングをできるスキルが求められますね。






