Udonのコードは、書いた順に上から下へ走るわけではありません。待ち受け です。
何かが起きたときに、その担当の場所だけが動きます。この「何か」を イベント と呼びます。ここが分からないまま書くと、「処理は正しいのに、いつまでも動かない」にぶつかります。
この記事では、回転する飾りと停止ボタンを作って、Start・Update・Interactがそれぞれいつ動くか を目で確かめます。
この記事でわかること
- Start・Update・Interactが動くタイミングの違い
Time.deltaTimeを掛ける理由- Updateを使うべき場面と、避けたほうがいい場面
- 実行順を当てにしてはいけない場所
最初のUdonSharp を試した状態から始めます。
3つのイベントは、動く回数が違う
まず使うのはこの3つです。動く「回数」で分けると覚えやすくなります。

- Start は、ワールドに入ったときに 1回だけ 動きます。初期状態をそろえる場所です
- Update は、毎フレーム 動きます。90fpsなら1秒に90回です
- Interact は、そのオブジェクトが 使われたときだけ 動きます

この3つで、たいていのギミックは書けます。「毎フレーム動かし続けたいもの」はUpdate、「押されたときだけ変えたいもの」はInteract、「最初にそろえておきたいもの」はStartです。
書く前に決めるのは、文法ではなく 「どのタイミングで動かしたいか」 です。ここが決まれば、書く場所も決まります。
実践:回る飾りを、ボタンで止める
天井から吊るした飾りがゆっくり回り、ボタンを押すと止まります。もう一度押すと回り出します。
完成すると、Consoleに Start が1回だけ出て、押すたびに Interact のログが増え、飾りは回り続けます。同じシーンの中で、1回だけの処理と毎フレームの処理が同居している のが目で見えます。
1. 飾りとボタンを置く
床のあるシーンに2つ置きます。
| 名前 | 作り方と設定 |
|---|---|
SpinDisplay | Cube。Position (0, 1.5, 2)、Scale (1, 0.1, 0.3) |
SpinButton | Cube。Position (0, 1, 1)、Scale (0.4, 0.4, 0.4) |

SpinDisplay は板状にしておくと、回転しているのが分かりやすくなります。ボタンは手前に置いて、押しやすい位置にします。
2. コードを書く
Assets/Scripts で「Create → U# Script」を選び、RotationSwitch という名前で作ります。
using UdonSharp;
using UnityEngine;
[UdonBehaviourSyncMode(BehaviourSyncMode.None)]
public class RotationSwitch : UdonSharpBehaviour
{
[SerializeField] private Transform display; // 回す相手
[SerializeField] private float degreesPerSecond = 45f; // 1秒あたり何度回すか
private bool isRotating = true;
private bool ready;
// 入ったときに1回だけ
private void Start()
{
ready = display != null;
if (!ready)
{
Debug.LogWarning("[RotationSwitch] Displayが未設定です。");
return;
}
Debug.Log("[RotationSwitch] Start");
}
// 押されたときだけ
public override void Interact()
{
if (!ready) return;
isRotating = !isRotating;
Debug.Log("[RotationSwitch] rotating=" + isRotating);
}
// 毎フレーム
private void Update()
{
if (!ready || !isRotating) return;
display.Rotate(0f, degreesPerSecond * Time.deltaTime, 0f, Space.Self);
}
}
Update() の中身に注目してください。Time.deltaTime を掛けています。
これは「前のフレームからの経過秒数」です。掛けないと、1フレームごとに45度ずつ回ってしまうので、120fpsのPCでは1秒に5400度、30fpsのPCでは1350度になります。同じワールドなのに、人によって回転速度が違う という状態です。
Time.deltaTime を掛けると、「1秒あたり45度」という意味になり、どのPCでも同じ速さになります。Updateの中で何かを動かすときは、必ず掛ける と覚えてください。
もう1つ、Update() の最初に if (!isRotating) return; を置いています。止まっているときに回転の計算をしないための書き方です。
3. 割り当てて動かす
SpinButton に Udon Behaviour を追加し、Program Source に RotationSwitch を指定します。
| 欄 | 指定するもの |
|---|---|
| Display | Hierarchyの SpinDisplay |
| Degrees Per Second | 45 |
| Interaction Text | 回転を切り替える |
Playして確認します。
| タイミング | 見えるはずの結果 |
|---|---|
| Playした瞬間 | Consoleに Start が 1回だけ 出る。飾りが回り始める |
| ボタンを押す | Consoleに rotating=False。飾りが止まる |
| もう一度押す | rotating=True。また回り出す |
| そのまま眺める | Consoleは増えないのに、飾りは回り続けている |
最後の1行が、この記事のポイントです。ログが増えなくてもUpdateは動いています。 毎フレームの処理は、目に見えないところで走り続けます。
4. deltaTimeを外して比べる
Time.deltaTime を一時的に外して、display.Rotate(0f, degreesPerSecond, 0f, Space.Self); にしてみてください。
猛烈な速さで回ります。1フレームごとに45度なので当然です。確認したら戻してください。
この違いを一度見ておくと、他の記事で deltaTime が出てきたときに、何のためのものか迷わなくなります。
Updateは、使うと決めてから使う
Updateは便利ですが、VRChatでは慎重に使います。理由は、同じ処理が全員のPCで走る からです。
VRなら1秒に90回。それが20個のオブジェクトに書いてあれば、1秒に1800回です。さらにワールドに10人いれば、10人分のPCでそれぞれ1800回動いています。
だから、判断はこうなります。
- Updateが向くもの: 回転し続ける飾り、常に上下する浮遊物、毎フレーム位置を追いかける処理
- Updateが向かないもの: 押されたら1回だけ変わるもの、数秒に1回でいいもの、条件を監視するだけのもの
「1秒に1回、何かを確認したい」ような処理は、Updateで毎フレーム確認する必要はありません。押されたときに動かすか、時間差で呼ぶ仕組みを使います(遅延イベント で扱いま す)。
判断の目安は、「これは本当に毎フレーム動く必要があるか?」 です。答えがノーなら、Updateは使いません。
実行順は当てにしない
もう1つ、はまりやすい落とし穴があります。
複数のスクリプトのStartが、どの順で動くかは決まっていません。 AのStartでBの値を読もうとしても、そのときBのStartがまだ走っていないかもしれません。

対処はシンプルです。
- Startには、自分の中で完結する初期化 だけを書く
- 他のオブジェクトの値が必要なら、Startで読むのではなく、必要になったときに読む
同じ理由で、OnEnable と OnDisable も気をつけます。GameObjectを無効にすると OnDisable が呼ばれ、そのオブジェクトのUpdateも止まります。有効に戻すと OnEnable が呼ばれますが、Start は2回目は呼ばれません。
「表示を消して軽くしよう」と思ってボタン自体を無効にすると、押せなくなって元に戻せません。無効にするのは、操作の対象であって、操作するボタンではありません。
おまけ:先に知っておくと良いこと
Space.SelfとSpace.World: 回転の基準が変わります。Selfはそのオブジェクト自身の向き、Worldはワールドの座標です。傾いた物を回すときに結果が変わるので、思った向きに回らないときはここを疑います- FixedUpdateは物理向け: 一定の間隔で呼ばれるイベントで、物理演算に合わせた処理に使います。見た目を動かすだけなら不要です
- Awakeは使わない: 通常のUnityで使う
Awakeは、UdonSharpでは使いません。初期化はStartに書きます - 時間差で呼ぶ方法がある: 「3秒後に何かをする」という処理は、Updateで秒数を数えるより、遅延イベントを使うほうが軽くて読みやすくなります
- 重さを測る手段がある: 実際にどのくらい負荷がかかっているかは、後で計測できます。Udonと通信の最適化 で扱います
まとめ
イベントは、処理が始まるきっかけです。
- Startは入ったとき1回、Updateは毎フレーム、Interactは押されたときだけ
- Updateの中で動かすときは
Time.deltaTimeを掛ける - Updateは全員のPCで走る。本当に毎フレーム必要かを考えてから使う
- 複数のスクリプトのStartの順番は決まっていない。他人の値をStartで読まない
書く場所に迷ったときの問いかけは、「これは、いつ動いてほしいのか?」 です。1回か、毎フレームか、押されたときか。それが決まれば、書く場所も決まります。