【VRChat】Udonを軽くする:毎フレーム聞くのをやめる

作成: 2025-12-19最終更新: 2026-09-09

毎フレームの処理をイベントに置き換えて、Udonの負荷を下げます。取得結果を覚えておく効果、同期の送信回数を減らす考え方も、測って確かめながら解説します。

仕掛けを10個ほど作ったあたりから、ワールドが重くなってきました。飾りは増やしていないのに、フレームレートだけが落ちています。

Udonのコードは、見た目はふつうのC#です。ただ、実行のされ方が違います。 翻訳された命令を1つずつ解釈しながら動くので、1行のコストが想像より高くつきます。

だからこそ効くのは、書き方を凝ることではありません。何回呼ぶかを減らすこと です。この記事では、毎フレームの処理をイベントに置き換えます。

毎フレーム確かめる形から、変わったときだけ動く形へ

この記事でわかること

  • 毎フレーム確かめる処理をイベントに置き換える
  • 取得した結果を覚えておく効果
  • 同期の送信回数を減らす考え方
  • 直したあと、測って確かめる方法

メソッドとカスタムイベント重さを測る を読んだ状態から始めます。

Sponsored


毎秒60回、同じ質問をしない

重いUdonのコードには、共通の形があります。

private void Update()
{
    // 近づいたか? を毎フレーム確かめる
    float d = Vector3.Distance(player.GetPosition(), transform.position);
    doorAnimator.SetBool("IsOpen", d < 3f);
}

書いてあることは正しいのですが、1秒に60回、同じ質問をしています。 実際に答えが変わるのは、人が近づいた瞬間と離れた瞬間の2回だけです。

毎フレーム確かめる形と、変化した瞬間だけ動く形

置き換え先は、たいていすでに用意されています。

毎フレーム確かめていること置き換えるイベント
範囲に人がいるかOnPlayerTriggerEnter / OnPlayerTriggerExit
掴まれているかOnPickup / OnDrop
ボタンが押されたかInteract
同期の値が変わったかOnDeserialization
誰かが入ってきたかOnPlayerJoined / OnPlayerLeft

「誰かが触った」「誰かが入った」「値が届いた」。 この3種類で、ワールドのUpdateの大半は消せます。

もう1つ、「いまは待って、あとで1回だけ」という処理もあります。これはUpdateで数えるのではなく、予約で書きます。

// 3秒後に1回だけ閉める
SendCustomEventDelayedSeconds(nameof(CloseDoor), 3f);

Updateで経過時間を足していくより短く、負荷もかかりません。詳しくは 時間差で処理を呼ぶ を参照してください。

Updateを使ってよい場面もあります。 回り続ける看板、揺れ続ける水面のように、毎フレーム本当に変わり続けるものです。避けたいのは「変わったかどうかを確かめる」使い方だけです。

Sponsored

同じものを何度も探さない

もう1つ、Udonで特に高くつくものがあります。探す処理 です。

private void Update()
{
    // 毎フレーム探している
    GetComponent<AudioSource>().volume = 0.5f;
}

GetComponent は、そのオブジェクトの部品を1つずつ見て回る処理です。ネイティブのC#でも避けたいところですが、Udonでは特に高くつきます。

毎回探すのと、1回だけ探して覚えておくのの違い

答えは単純で、Start() で1回取って、フィールドに覚えておきます。

private AudioSource cachedAudio;

private void Start()
{
    cachedAudio = GetComponent<AudioSource>();   // 1回だけ
}

覚えておきたいのは、この3つです。

高くつくものどうするか
GetComponentStart() で取って覚える
GameObject.Find使わない。インスペクタで割り当てる
文字列をつなげる処理表示が変わるときだけ作る

インスペクタで割り当てられるものは、コードで探さない。 これが基本の姿勢です。

実践:距離を測るドアを、イベントに置き換える

毎フレーム距離を測る自動ドアを作って、測って、イベントに置き換えて、また測ります。

人が近づいた1枚だけが開き、残りは何もしていない

1. 重い形で作る

AnimatorとUdonで扉を動かす と同じ扉を用意します。DoorPivot にAnimatorが付いていて、IsOpen というBoolのパラメータがある状態です。

Assets/ScriptsPollingDoor を作ります。

using UdonSharp;
using UnityEngine;
using VRC.SDKBase;

// 重い書き方。あとで置き換える
[UdonBehaviourSyncMode(BehaviourSyncMode.None)]
public class PollingDoor : UdonSharpBehaviour
{
    [SerializeField] private Animator doorAnimator;
    [SerializeField] private float openDistance = 3f;

    private void Update()
    {
        VRCPlayerApi player = Networking.LocalPlayer;
        if (!Utilities.IsValid(player)) return;

        float d = Vector3.Distance(player.GetPosition(), transform.position);
        doorAnimator.SetBool("IsOpen", d < openDistance);
    }
}

DoorPivot に付けて、Door Animatorに自分自身を割り当てます。

Playすると、ちゃんと動きます。 近づけば開き、離れれば閉じます。動くからこそ、そのままにしてしまいがちな形です。

2. 測る

この扉を 20個 複製します。DoorPivot を選んで Ctrl + D を19回押し、位置を少しずつずらします。

Profilerを開いてPlayし、Udon関連の帯の太さを控えます。20個分のUpdateが毎フレーム回っています。

3. イベントに置き換える

DoorPivot に「3D Object → Cube」で子オブジェクト DoorSensor を作り、次のようにします。

項目設定
Position(0.8, 0, 0)(扉の前あたり)
Scale(3, 2, 3)
Box Collider の Is Triggerオン
Mesh Rendererオフ(見えなくする)

Assets/ScriptsSensorDoor を作ります。

using UdonSharp;
using UnityEngine;
using VRC.SDKBase;

[UdonBehaviourSyncMode(BehaviourSyncMode.None)]
public class SensorDoor : UdonSharpBehaviour
{
    [SerializeField] private Animator doorAnimator;

    // 入った瞬間だけ呼ばれる
    public override void OnPlayerTriggerEnter(VRCPlayerApi player)
    {
        if (!player.isLocal) return;
        doorAnimator.SetBool("IsOpen", true);
    }

    // 出た瞬間だけ呼ばれる
    public override void OnPlayerTriggerExit(VRCPlayerApi player)
    {
        if (!player.isLocal) return;
        doorAnimator.SetBool("IsOpen", false);
    }
}

Update() が消えました。 呼ばれるのは、人が入った瞬間と出た瞬間だけです。

DoorSensor にこのスクリプトを付けて、Door Animatorに親の DoorPivot を割り当てます。PollingDoor は外します。20個すべてで同じ入れ替えをします。

4. 測り直す

もう一度Profilerを見ます。

項目置き換え前置き換え後
Udonの帯の太さ
毎フレーム動く扉の数200

動きはまったく同じです。 近づけば開き、離れれば閉じます。それでいて、誰も近づいていないあいだの処理はゼロになりました。

これが、この記事でいちばん伝えたい形です。機能を削ったのではなく、聞く回数を減らしただけです。

同期は、送る回数を減らす

Udonの処理と並んで効いてくるのが、同期の送信です。人数が増えるほど響きます。

送る回数・送る量・送るまとまりを減らす

考え方は3つです。

方針具体的にすること
変わったときだけ送る同期モードを Manual にして、RequestSerialization() を必要なときだけ呼ぶ
送る値を減らす見た目そのものではなく、元になる値を1つ送って、各自が組み立てる
まとめて送る変数を何度も書き換えたあと、最後に1回だけ送信を頼む

2つ目は、これまでの記事でずっとやってきた形です。扉のときも「開いているか」の真偽1つだけを送って、各自がAnimatorに反映しました。軽さのためだけでなく、途中参加にも強い書き方でした。

Continuous は、値が変わっていなくても送り続けます。位置を送り続ける必要があるものだけに使い、それ以外は Manual にしてください。

Sponsored

うまくいかないときは

  • イベントが呼ばれない → コライダーの Is Trigger がオフか、範囲が小さすぎます
  • 他の人が近づくと自分の扉も開くplayer.isLocal の判定が抜けています
  • 置き換えたのに軽くならない → 他にUpdateを使っている処理が残っています。すべてのスクリプトを確認します
  • 人が増えると重い → 処理ではなく同期です。同期モードと送信の回数を見ます
  • どこが重いか分からない重さを測る の手順で先に測ってください

おまけ:先に知っておくと良いこと

  • 数字は覚えなくてよい: 「Udonは何倍遅い」という話をよく見かけますが、環境で変わります。覚えるのは「Updateで回すほど損」という感覚だけで足ります
  • どうしてもUpdateが要るなら間引く: 数フレームに1回だけ処理する書き方もあります。ただ、その前に「本当に毎フレーム確かめる必要があるか」をもう一度考えてください
  • 早すぎる最適化はしない: 仕掛けが3つのワールドで悩む話ではありません。測って重かったところだけ直します
  • 見えていない物のUpdateも動く: 部屋の隅に置いた仕掛けも、画面に映っていなければ描画は止まりますが、Udonは動き続けます。使わないときはオブジェクトごと無効にするのが確実です
  • 使いどころはいろいろ: 自動ドア、近づくと点く照明、範囲に入ると流れるBGM、視線を追う看板。どれも「変わる瞬間」がはっきりしているので、イベントに置き換えられます

まとめ

Udonを軽くするのは、回数の引き算です。

  • 毎フレーム確かめる処理を、変化の瞬間のイベントに置き換える
  • GetComponentStart() で1回。探す処理はインスペクタの割り当てに寄せる
  • 同期は Manual にして、変わったときだけ送る
  • 直したあと、測って確かめる

書く前の問いかけは、「この質問の答えは、いつ変わるか」 です。変わる瞬間が言えるなら、そこにイベントがあります。

物理の側を軽くするなら 物理の負荷を下げる、見た目の側なら 描画を軽くする へ進んでください。

VRChat このセクションのノート63