[VRChat] Calling Logic After a Delay: A Door That Opens in Three Seconds

Created: 2026-09-09

Building 'run three seconds after the press' with SendCustomEventDelayedSeconds. Covers why it's lighter and more readable than counting seconds in Update, and what happens when people spam the button.

You press a button and want the door to open three seconds later. Or a countdown starts on press and something happens at zero.

"After a short wait" comes up often in world effects. The straightforward approach is adding seconds every frame in Update(), but that means calculating the entire time you're waiting.

This article covers booking "call this once at that time." It avoids Update and makes the code shorter.

Pressing a switch, an hourglass, then a door beginning to open

What You'll Learn

  • How to book delayed logic
  • Why it's lighter and more readable than counting in Update
  • How to show a countdown on screen
  • What happens when people spam the button

Start from having tried methods and custom events.

Sponsored


Don't wait — book it

There are two ways to "do something in three seconds."

The first counts every frame. You add elapsed time inside Update() and run when it passes three seconds. It works, and calculation runs the entire time you wait. At 90fps that's 90 times a second, 270 over three seconds.

The second books it. You ask VRChat to "call this method in three seconds," and then do nothing.

The booked routine getting called three seconds after the press

This is the line you use.

SendCustomEventDelayedSeconds(nameof(OpenDoor), 3f);

It means "please call the method OpenDoor in three seconds." After booking, the lines below it continue immediately. It doesn't stop and wait.

Compared with counting in Update:

Counting in UpdateDelayed event
While waitingCalculates every frameDoes nothing
Code lengthNeeds a variable and a checkOne line
ReadabilityYou have to follow elapsed timeReads as "call it in three seconds"

For logic that runs only on press, delayed events fit better. Update fits things that keep moving continuously, like a rotating ornament (covered in events and execution order).

The method being called has conditions. It must be public and take no arguments — the same conditions as SendCustomEvent.

Sponsored

Hands-On: Open a door three seconds after the press

Press a button, wait three seconds, and a panel drops so you can walk through.

1. Place the button and the door

Place two things in a scene with a floor.

NameHow to make it, and settings
DelayButtonCube. (0, 1, -1), Scale (0.4, 0.4, 0.4)
SlideDoorCube. (0, 1, 2), Scale (1.6, 2, 0.15)
Arrangement of the button and the delayed door

SlideDoor is a panel standing up from the floor. It drops, opening the way through.

2. Write the code

In Assets/Scripts, choose "Create → U# Script" and make DelayedDoor.

using UdonSharp;
using UnityEngine;

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

    [Header("Door positions")]
    [SerializeField] private float closedY = 1f;
    [SerializeField] private float openY = -1.2f;

    private bool isMoving;   // Whether something is booked right now

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

        // Already in motion, so do nothing
        if (isMoving) return;

        isMoving = true;
        Debug.Log("[DelayedDoor] opening in " + delaySeconds + " seconds");

        // Ask for OpenDoor to be called in 3 seconds
        SendCustomEventDelayedSeconds(nameof(OpenDoor), delaySeconds);
    }

    // Called via the booking. Public, no arguments
    public void OpenDoor()
    {
        Vector3 p = doorTransform.localPosition;
        doorTransform.localPosition = new Vector3(p.x, openY, p.z);
        Debug.Log("[DelayedDoor] opened");

        // Book the close, five seconds out, right here
        SendCustomEventDelayedSeconds(nameof(CloseDoor), 5f);
    }

    public void CloseDoor()
    {
        Vector3 p = doorTransform.localPosition;
        doorTransform.localPosition = new Vector3(p.x, closedY, p.z);
        isMoving = false;   // Pressable again from here
        Debug.Log("[DelayedDoor] closed");
    }
}

Three things to note.

isMoving prevents double booking. Without it, the door opens and closes once per press in a spam.

OpenDoor() books another call from inside itself. "Open in three seconds, close five seconds after that" is written as a chain of bookings. The order shows up directly in the code, unlike counting seconds in Update.

CloseDoor() is what resets isMoving. It returns to a pressable state once the whole sequence finishes.

3. Assign it and confirm

Add a Udon Behaviour to DelayButton and set DelayedDoor.

FieldWhat to set
Door TransformSlideDoor from the Hierarchy
Delay Seconds3
Closed Y1
Open Y-1.2
Interaction TextOpen the door
Dragging in the door, and entering seconds and heights as numbers

Press Play and try it.

TimingExpected result
The moment you pressConsole shows opening in 3 seconds. The door hasn't moved
Three seconds laterThe door drops. opened
Five seconds after thatThe door returns. closed
Press while waitingNothing happens (blocked by isMoving)

Confirm that there's a gap where pressing doesn't move anything. The log appears the instant you press, and the door moves three seconds later. That's booking in action.

Showing a countdown

Nothing happening while you wait is unsettling. Let's show the remaining time.

Put UI text above SlideDoor. Create "UI → Text - TextMeshPro," put it in a World Space Canvas, and position it in front of the door.

Add a one-second booking to the code.

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);
}

// Re-book itself once a second
public void Tick()
{
    remaining--;
    UpdateCountdown();

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

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

The point is that Tick() books itself again. Repeating "call myself in one second" gives you once-per-second logic. It stops booking when the remainder reaches zero, so it stops on its own.

This too is lighter and more readable than watching seconds every frame in Update(). It runs once a second.

What happens with rapid presses

An important property: a booked call can't be cancelled.

Rapid presses stacking up bookings

Remove the isMoving check and press the button five times quickly. Five bookings go in, and the door opens five times and closes five times. It executes faithfully, once per press.

The response is to block it, either on the receiving side or the calling side.

  • Block on the calling side: check isMoving as in this example and skip booking while in motion
  • Block on the receiving side: check "if it's already open, do nothing" at the top of OpenDoor()

Either works, though blocking on the calling side is usually clearer. The button stops responding, which tells the presser "not accepting right now."

To be kinder about it, change the Interaction Text or the button's color while waiting.

Sponsored

Common Pitfalls

  • The booked call never fires → Check that the method is public and takes no arguments. private won't be called
  • You mistyped the method name → With nameof() it becomes a compile error. Written as a raw string, you don't find out until runtime
  • Rapid presses run it repeatedly → Bookings can't be cancelled. Block with a check like isMoving
  • Leaving while waiting → That booking never runs. Splitting into short intervals is safer than booking a long one

Bonus: Good to Know Up Front

  • There's a frame-based wait too: SendCustomEventDelayedFrames waits a specified number of frames. Use it for fine control like "run on the next frame"
  • You can book on other Udons: other.SendCustomEventDelayedSeconds(...) books a method on another script
  • This is local: Both the booking and the execution happen on the presser's PC alone. To open the door for everyone, you need synchronization. Covered in networking basics
  • Smooth motion needs a different mechanism: The door here jumps instantly to its position. For a slow opening motion, use driving a door with Animator and Udon
  • Don't rely on long bookings: A booking minutes out may be preceded by people leaving or the instance ending. Stacking short bookings is more dependable

Summary

Delayed logic books rather than waits.

  • One line: SendCustomEventDelayedSeconds(nameof(MethodName), seconds)
  • The called method must be public with no arguments
  • Lines after the booking continue immediately
  • Bookings can't be cancelled. Add your own spam protection
  • Re-booking itself turns it into repeating logic

The question to ask before writing is: "Does this run continuously, or once, later?" For the latter, use a booking rather than Update.

To build a door that moves smoothly, go to driving a door with Animator and Udon. To open the same door for everyone, go to networking basics.

VRChat Notes in this section63