【VRChat】時間差で処理を呼ぶ:3秒後に開く扉とカウントダウン

作成: 2026-09-09

SendCustomEventDelayedSecondsで「押して3秒後に実行」を作ります。Updateで秒数を数えるより軽くて読みやすい理由と、連打されたときの扱いも解説します。

ボタンを押したら、3秒待ってから扉が開いてほしい。押した瞬間にカウントダウンが始まって、0になったら何かが起きる。

「少し待ってから」という処理は、ワールドの演出でよく出てきます。素直にやるなら Update() で毎フレーム秒数を足していく方法が思いつきますが、待っているあいだずっと計算し続けることになります。

この記事では、「その時刻に1回呼んで」と予約する 書き方を解説します。Updateを使わずに済み、コードも短くなります。

スイッチを押すと、砂時計を挟んで扉が開き始める

この記事でわかること

  • 時間差の処理を予約する書き方
  • Updateで数えるより軽く、読みやすい理由
  • カウントダウンを画面に出す方法
  • 連打されたときにどうなるか

メソッドとカスタムイベント を試した状態から始めます。

Sponsored


待つのではなく、予約する

「3秒後に何かをする」には、2つのやり方があります。

1つ目は、毎フレーム数える方法 です。Update() の中で経過時間を足していき、3秒を超えたら実行します。動きますが、待っているあいだずっと計算が走ります。90fpsなら1秒に90回、3秒で270回です。

2つ目は、予約する方法 です。「3秒後にこのメソッドを呼んで」とVRChatに頼んで、あとは何もしません。

押した時刻から3秒後に、予約した処理が呼ばれる

使うのはこの1行です。

SendCustomEventDelayedSeconds(nameof(OpenDoor), 3f);

OpenDoor というメソッドを、3秒後に呼んでください」という意味です。予約したあと、この行の下の処理はすぐに続きます。待って止まるわけではありません。

Updateで数える方法と比べると、こうなります。

Updateで数える遅延イベント
待っているあいだ毎フレーム計算する何もしない
コードの長さ変数と判定が必要1行
読みやすさ経過時間を追う必要がある「3秒後に呼ぶ」と読める

押されたときだけ動く処理なら、遅延イベントのほうが向いています。 Updateが向くのは、回転し続ける飾りのように「常に動かし続けるもの」です(イベントと実行順 で扱いました)。

呼ばれる側のメソッドには条件があります。public で、引数を取らない ことです。SendCustomEvent と同じ条件です。

Sponsored

実践:押して3秒後に扉を開く

ボタンを押すと3秒待ってから板が下がり、通れるようになります。

1. ボタンと扉を置く

床のあるシーンに2つ置きます。

名前作り方と設定
DelayButtonCube。(0, 1, -1)、Scale (0.4, 0.4, 0.4)
SlideDoorCube。(0, 1, 2)、Scale (1.6, 2, 0.15)
ボタンと、時間差で動く扉の配置

SlideDoor は、床から立った板です。これが下がって、通れるようになります。

2. コードを書く

Assets/Scripts で「Create → U# Script」を選び、DelayedDoor を作ります。

using UdonSharp;
using UnityEngine;

[UdonBehaviourSyncMode(BehaviourSyncMode.None)]
public class DelayedDoor : UdonSharpBehaviour
{
    [SerializeField] private Transform doorTransform;
    [SerializeField] private float delaySeconds = 3f;

    [Header("扉の位置")]
    [SerializeField] private float closedY = 1f;
    [SerializeField] private float openY = -1.2f;

    private bool isMoving;   // いま予約中か

    public override void Interact()
    {
        if (doorTransform == null) return;

        // すでに動いているなら、何もしない
        if (isMoving) return;

        isMoving = true;
        Debug.Log("[DelayedDoor] " + delaySeconds + "秒後に開きます");

        // 3秒後に OpenDoor を呼んでもらう
        SendCustomEventDelayedSeconds(nameof(OpenDoor), delaySeconds);
    }

    // 予約されて呼ばれる。public で、引数なし
    public void OpenDoor()
    {
        Vector3 p = doorTransform.localPosition;
        doorTransform.localPosition = new Vector3(p.x, openY, p.z);
        Debug.Log("[DelayedDoor] 開きました");

        // 5秒後に閉じる予約を、続けて入れる
        SendCustomEventDelayedSeconds(nameof(CloseDoor), 5f);
    }

    public void CloseDoor()
    {
        Vector3 p = doorTransform.localPosition;
        doorTransform.localPosition = new Vector3(p.x, closedY, p.z);
        isMoving = false;   // ここでまた押せるようになる
        Debug.Log("[DelayedDoor] 閉じました");
    }
}

見どころが3つあります。

isMoving で二重に予約されるのを防いでいます。 これがないと、連打した回数だけ扉が開閉します。あとで詳しく説明します。

OpenDoor() の中から、さらに予約を入れています。 「3秒後に開いて、その5秒後に閉じる」という流れが、予約の連鎖で書けます。Updateで秒数を数える方法より、順番がそのままコードに現れます。

isMoving を戻すのは CloseDoor() です。 一連の動きが終わったところで、また押せる状態に戻します。

3. 割り当てて確かめる

DelayButton に Udon Behaviour を追加し、DelayedDoor を指定します。

指定するもの
Door TransformHierarchyの SlideDoor
Delay Seconds3
Closed Y1
Open Y-1.2
Interaction Text扉を開ける
扉をドラッグし、秒数と高さを数値で入れる

Playして押してみてください。

タイミング期待する結果
押した瞬間Consoleに 3秒後に開きます。扉はまだ動かない
3秒後扉が下がる。開きました
さらに5秒後扉が戻る。閉じました
待っているあいだに押す何も起きない(isMoving で弾いている)

押してすぐ動かない、という間があること を確かめてください。押した瞬間にログが出るのに、扉は3秒後に動きます。これが予約の動きです。

カウントダウンを表示する

待っているあいだ、何も起きないのは不安です。残り時間を表示してみましょう。

SlideDoor の上あたりに、UIのテキストを置きます。「UI → Text - TextMeshPro」を作り、World Space の Canvas に入れて、扉の前に配置します。

コードに、1秒ごとの予約を足します。

using TMPro;
// ...

[SerializeField] private TextMeshProUGUI countdownText;
private int remaining;

public override void Interact()
{
    if (doorTransform == null || isMoving) return;

    isMoving = true;
    remaining = (int)delaySeconds;
    UpdateCountdown();

    SendCustomEventDelayedSeconds(nameof(Tick), 1f);
    SendCustomEventDelayedSeconds(nameof(OpenDoor), delaySeconds);
}

// 1秒ごとに自分を呼び直す
public void Tick()
{
    remaining--;
    UpdateCountdown();

    if (remaining > 0)
    {
        SendCustomEventDelayedSeconds(nameof(Tick), 1f);
    }
}

private void UpdateCountdown()
{
    if (countdownText != null)
    {
        countdownText.text = remaining > 0 ? remaining.ToString() : "";
    }
}

Tick()自分自身をもう一度予約している ところがポイントです。「1秒後にまた自分を呼ぶ」を繰り返すと、1秒ごとの処理になります。残りが0になったら予約をやめるので、勝手に止まります。

これも Update() で毎フレーム秒数を見るより軽く、そして読みやすくなります。1秒に1回しか動きません。

連打されるとどうなるか

大事な性質があります。予約した処理は、取り消せません。

連打すると予約が積み上がる

isMoving の判定を外して、ボタンを5回連打してみてください。5回ぶんの予約が入り、扉は5回開いて5回閉じます。押した回数だけ律儀に実行されます。

対処は、実行される側か、呼ぶ側で弾く ことです。

  • 呼ぶ側で弾く: いまの例のように isMoving を見て、動いていたら予約しない
  • 実行される側で弾く: OpenDoor() の先頭で「すでに開いているなら何もしない」と判定する

どちらでも構いませんが、呼ぶ側で弾くほうが分かりやすい ことが多いです。ボタンの反応がなくなるので、押した人にも「いま受け付けていない」ことが伝わります。

親切にするなら、待っているあいだ Interaction Text を変えたり、ボタンの色を変えたりします。

Sponsored

うまくいかないときは

  • 予約した処理が呼ばれない → メソッドが public か、引数を取っていないかを確認します。private では呼ばれません
  • メソッド名を間違えたnameof() を使っていれば、コンパイルの時点でエラーになります。文字列で直接書いていると、実行するまで気づけません
  • 連打で何度も動く → 予約は取り消せません。isMoving のような判定で弾きます
  • 待っているあいだに退出すると → その予約は実行されません。長い時間を1回で予約するより、短く区切るほうが安全です

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

  • フレーム単位で待つ方法もある: SendCustomEventDelayedFrames は、指定したフレーム数だけ待ちます。「次のフレームで実行したい」といった細かい制御に使います
  • 他のUdonにも予約できる: other.SendCustomEventDelayedSeconds(...) のように、別のスクリプトのメソッドを予約することもできます
  • これはローカルです: 予約も実行も、押した人のPCだけで起きます。全員の扉を開けたいなら、同期が必要です。ネットワーク同期入門 で扱います
  • 滑らかに動かすには別の仕組みが要る: この記事の扉は、瞬間的に位置が変わります。ゆっくり開く動きにしたいなら AnimatorとUdonで扉を動かす を使います
  • 長い予約は当てにしない: 数分先の予約は、その前に人が退出したりインスタンスが終わったりします。短い予約を積み重ねるほうが確実です

まとめ

時間差の処理は、待つのではなく予約します。

  • SendCustomEventDelayedSeconds(nameof(メソッド名), 秒数) の1行
  • 呼ばれるメソッドは public で引数なし
  • 予約のあとの行は、すぐに続けて実行される
  • 予約は取り消せない。連打対策は自分で入れる
  • 自分自身を予約し直すと、繰り返し処理になる

書く前の問いかけは、「これは常に動き続けるのか、それとも1回だけ後で動くのか」 です。後者なら、Updateではなく予約を使います。

滑らかに動く扉を作るなら AnimatorとUdonで扉を動かす、全員で同じ扉を開けるなら ネットワーク同期入門 へ進んでください。

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