はじめに
この記事は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
の頻度を下げる工夫が必要となります。
プリミティブ型以外はボックス化される
C#にはプリミティブ型(組み込み型)というものがあります。たとばint
やfloat
など。
これらはC#に最初から定義されている型であり、基本的にGCを気にせずノーコストで利用することができます。
そしてUdonですが、プリミティブ型以外の型を触るとボックス化されてGCアロケートが起きます。
たとえVector3
やQuaternion
といった構造体であってもGCアロケートします。
このあたりは回避不可能なので、仕様と割り切って付き合っていくしか無いとは思います。
string
は避けよう
C#
の仕様上、string
(文字列)は定義するだけでかなりのGC Alloc
を引き起こします。
そのためUpdate()
で毎フレーム文字列を生成するなどしていると、パフォーマンスにかなりの悪影響を及ぼします。
極力string
は使わない、使うにしても必要なタイミングで必要なだけ生成する工夫が必要です。
U#
は別コンポーネントのメソッド呼び出しがコスト
非常に便利なUdonSharp
ですが、見えないところでコストがかかります。
それはUdonSharpBehaviour
から別のUdonSharpBehaviour
なオブジェクトのメソッドを呼びだす時です。
たとえば、次のようなU#
スクリプトがあったとして。
using UdonSharp;
using UnityEngine;
namespace DebugTest
{
public class Runner : UdonSharpBehaviour
{
private Rigidbody _rigidbody;
void Start()
{
_rigidbody = GetComponent<Rigidbody>();
}
public Vector3 GetCurrentVelocity()
{
return _rigidbody.velocity;
}
}
}
using UdonSharp;
using UnityEngine;
namespace DebugTest
{
public class Observer : UdonSharpBehaviour
{
[SerializeField] private Runner _runner;
private void Update()
{
// 取得するだけで何も使わない
var velocity = _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でパフォーマンスチューニングをできるスキルが求められますね。