92
67

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 3 years have passed since last update.

【VRChat】 Udon開発する上での注意点【Unity】

Last updated at Posted at 2020-05-25

はじめに

この記事はVRChatにおけるUdonおよびUdonSharp(U#)を使った際の備忘録です。
これからUdonを使い始める人のために書き連ねておきます。

この記事は2020/5/26現在のVRChatを前提に書いています。

Udonとパフォーマンスチューニング

Udonは実質、UnityのAPIをラップして呼び出しているだけにすぎません。
そのためUnityでプログラミングをするときと同じ様にパフォーマンスにもこだわる必要があります。

プロファイラを見よう

Unityには標準でProfilerという機能が備わっています。
1フレーム単位でどのような処理が実行され、それにどれくらいの時間がかかっているかをチェックすることができます。

OpenProfiler.png

Profiler.png

詳しい使い方はこちらを参考にしてください。

なお、プロファイラで動作を監視すること自体がかなりの負荷となります。
プロファイラを使っている間はfpsがガタ落ちしますが、しょうがないと割り切ってください。
(普通のUnity開発なら回避策があるのですが、VRChatだと仕様上どうしようもできないです)

Raw Hierarchyで調査

プロファイラの表示をRaw Hierarchyに切り替えて時間がかかっている順にソートすると何がボトルネックか調査することができます。
とくにこのモードだとUdon VMの中身の実行順も見ることが出来ます。

Profiler2.png

Udonの仕様上、どのスクリプトであるか名前はわからないのですが、メソッド呼び出しの様子からどのスクリプトかあたりをつけることはできます。

GCアロケートを避けよう

とくに負荷の原因となりやすいものはGC Allocと表示されているものです。
これはUnity上で、プログラムを実行するために必要なメモリを確保する動作を表しています。
GCアロケートと呼ぶ)

そしてこの確保したメモリですが、解放される瞬間にVRChatが一瞬フリーズしてしまいます。
GC(ガベージコレクタ)が実行される、と呼びます)

GCが実行される頻度は少ないほどfpsに与える影響は小さくなります。
逆に高頻度でGCが実行されると、体感できるレベル(ひどいと数十fps)で影響がでてきます。
そのためGCの実行をさける、つまりGC Allocの頻度を下げる工夫が必要となります。

プリミティブ型以外はボックス化される

C#にはプリミティブ型(組み込み型)というものがあります。たとばintfloatなど。
これらはC#に最初から定義されている型であり、基本的にGCを気にせずノーコストで利用することができます。

そしてUdonですが、プリミティブ型以外の型を触るとボックス化されてGCアロケートが起きます
たとえVector3Quaternionといった構造体であってもGCアロケートします。

このあたりは回避不可能なので、仕様と割り切って付き合っていくしか無いとは思います。

stringは避けよう

C#の仕様上、string(文字列)は定義するだけでかなりのGC Allocを引き起こします。
そのためUpdate()で毎フレーム文字列を生成するなどしていると、パフォーマンスにかなりの悪影響を及ぼします。

極力stringは使わない、使うにしても必要なタイミングで必要なだけ生成する工夫が必要です。

U#は別コンポーネントのメソッド呼び出しがコスト

非常に便利なUdonSharpですが、見えないところでコストがかかります。
それはUdonSharpBehaviourから別のUdonSharpBehaviourなオブジェクトのメソッドを呼びだす時です。

たとえば、次のようなU#スクリプトがあったとして。

Runner
using UdonSharp;
using UnityEngine;

namespace DebugTest
{
    public class Runner : UdonSharpBehaviour
    {
        private Rigidbody _rigidbody;

        void Start()
        {
            _rigidbody = GetComponent<Rigidbody>();
        }

        public Vector3 GetCurrentVelocity()
        {
            return _rigidbody.velocity;
        }
    }
}
Observer
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へのメソッド呼び出しがSendCustomEventGetProgramVariableに変換されているところです。
(メソッドに引数を渡すとSetProgramVariableも追加される)

そしてこのSendCustomEventGetProgramVariableですが、なぜかGC Allocします

GCAlloc.png
(Udon内部実装の問題なのでおそらく回避不可)

ということで、U#を用いた場合、気軽にメソッド呼び出しを実行するとそれだけでGC Allocが発生します。
普通のUnity開発ではノーコストな操作が、U#ではコストがかかる点はかなり罠な気がします。

OnTriggerStay大暴走

UdonBehaviourにはOnTriggerStayが定義されています。
そのため**「VRC_Pickup + UdonBehaviourなオブジェクト」を一箇所に大量にまとめて配置するとOnTriggerStayが暴走します。**

OnTriggerStay.png
(50個ほど重ねて配置した例)

数個程度なら問題ないですが、数十個レベルで一箇所にまとめるとfpsがガタ落ちするレベルで影響がでてきます。
アイテムを一箇所にまとめておいて擬似的な「無限湧き」を作るようなことはやめておきましょう。

Udon Synced Variables役に立たない問題

結論からいうとUdon Synced Variablesはパフォーマンスのために 「使わない」 が正解です。

UdonにはUdon Synced Variablesという機能があります。
こちらは指定したプリミティブな変数をネットワークをまたいで同期する機能です。
U#でいうところの[UdonSynced]

ですがこのUdon Synced Variables、挙動が結構ヤバイです。

  • Owner常時パラメータを相手に送信し続ける
  • 転送量が増えるとパケットロスして不着となる
  • スループットがかなり低い

同期するオブジェクト、変数の数が増えるとDeath Run Detected: dropped N eventsというエラーが大量に出てきます。
これが発生してしまうと、変数同期の成功率が極端に下がってしまいます。

image.png
(大量にパケロスしている様子)

そのため、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 BehaviourSynchronize Positionを使う

Aのパターンは前述の問題があるのでオススメできません。
ということで実質的にBの「Synchronize Position」一択になります。

このSynchronize Positionはちゃんと差分同期してくれるため、大量にオブジェクトがあってもネットワークへの負荷は小さいです。

Synced.png

Synchronize Positionの同期ズレ問題

Synchronize Positionは差分同期してくれるためネットワーク負荷は小さいのですが、大量にオブジェクトがある場合、後からワールドに参加した人には正しく位置と姿勢が同期されない場合があります。
こちらはワールドにいるプレイヤー数とオブジェクト数によりますが、「5人以上かつ20個くらいオブジェクトを動かした」あたりから発生してきます。

原因はハッキリとはしていないのですが、どうも次の複数の問題が絡んでいるっぽいです。

前者についてはバグ報告済みですが、後者についてはいまいち挙動がつかめていないため報告していません。

Synchronize Positionの同期ズレ対策

対処療法として、次の対策をいれましょう。
実際にモノレールワールドで実施している対策がこれです。

  • 触れていないPickupオブジェクトはすべてMasterが所有権をもつ

  • オブジェクトを持っている間は持っている人にOwnerを渡す

  • 手を離したらMasterに所有権を返すようにする

  • 若干安定するが、それでもまだ同期ズレは起きる

  • 強制的に位置を同期する仕組みをいれる

  1. Master側で同期対象のオブジェクトをすべて少しだけ位置と姿勢をズラす
  2. 数秒後に元の位置姿勢に戻す

image.png
(同期ズレの発生をゼロにはできないので、同期ズレが起きる前提で対策した方が早い)

かなりツラミがある仕組みですが、現状これくらいしか大量のオブジェクトを安定して同期する方法がありません。

まとめ

Udonつらいし、UdonSharpも結構ツライです。
それなりのUnity開発経験と、Unityでパフォーマンスチューニングをできるスキルが求められますね。

92
67
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
92
67

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?