ボタンで開く扉を作りました。 ところが友達と入ってみると、自分の画面では開いているのに、相手の画面では閉じたままです。
扉のように 全員が同じ状態を見るべきもの は、開閉を共有する必要があります。しかも、あとから入ってきた人にも今の状態が伝わらないと、片方だけ壁があるように見えてしまいます。
この記事では、全員の画面で同じ開閉状態になる扉 を作ります。
この記事でわかること
- Animatorの状態ではなく、boolを同期する理由
- 途中参加の人にも伝える仕組み
- 押した人が所有権を取る形
- 同時に押されたときの挙動
ネットワーク同期入門 と AnimatorとUdonで扉を動かす を試した状態から始めます。
送るのは「開いているか」だけ
扉を同期しようとすると、「アニメーションの状態を全員に送る」という発想が浮かびます。その必要はありません。
送るのは、isOpen という真偽の値1つだけです。

理由は3つあります。
- 同期できるのは単純な値だけ: Animatorの状態そのものは送れません
- 通信が軽い: 真偽1つなら、送るデータはごくわずかです
- 途中参加に強い: 同期変数には最新の値が届くので、あとから入った人も「いま開いている」ことが分かります
流れはこうです。
押した人: isOpen を反転 → 自分のAnimatorに反映 → 送信を頼む
受け取った人: isOpen が届く → 自分のAnimatorに反映
各自が、受け取った値を見て自分の扉を動かします。 これは ネットワーク同期入門 のライトと、まったく同じ形です。対象がライトから扉に変わっただけです。
実践:全員で開く扉を作る
ボタンを押すと、全員の画面で扉が開きます。あとから入った人にも、その時点の状態が伝わります。
1. 扉とボタンを用意する
AnimatorとUdonで扉を動かす と同じ構成を作ります。まだなら、そちらの手順で用意してください。
DoorPivot(空のオブジェクト。扉の端。Animatorが付いている)
└─ DoorPanel(扉の板)
Animator Controllerには、IsOpen というBoolのパラメータと、DoorClosed / DoorOpen の2つの状態があります。遷移のHas Exit Timeは外してあります。
ボタンは DoorButton として、扉のそばに置きます。
2. コードを書く
Assets/Scripts で「Create → U# Script」を選び、SyncedDoor を作ります。
using UdonSharp;
using UnityEngine;
using VRC.SDKBase;
[UdonBehaviourSyncMode(BehaviourSyncMode.Manual)] // 変わったときだけ送る
public class SyncedDoor : UdonSharpBehaviour
{
[SerializeField] private Animator doorAnimator;
[UdonSynced] private bool isOpen; // 全員で共有する値
private void Start()
{
ApplyState(); // 手元の値を、まず反映しておく
}
public override void Interact()
{
if (!Utilities.IsValid(Networking.LocalPlayer)) return;
// 1. 所有権を取る(押した人が書ける人になる)
if (!Networking.IsOwner(gameObject))
{
Networking.SetOwner(Networking.LocalPlayer, gameObject);
}
if (!Networking.IsOwner(gameObject)) return;
// 2. 値を変えて、自分の画面に反映する
isOpen = !isOpen;
ApplyState();
// 3. 送信を頼む
RequestSerialization();
Debug.Log("[SyncedDoor] open=" + isOpen);
}
// 値が届いたときに呼ばれる(押した本人では呼ばれない)
public override void OnDeserialization()
{
ApplyState();
}
// 値を見て、扉の状態を作る。何度呼んでも同じ結果になる
private void ApplyState()
{
if (doorAnimator != null)
{
doorAnimator.SetBool("IsOpen", isOpen);
}
}
}
構造は、同期入門のライトと同じです。変わるのは ApplyState() の中身だけ で、targetLight.enabled が SetBool に置き換わっています。
同じ形が使い回せることに気づくと、同期は一気に扱いやすくなります。「状態を1つ持って、それを見て見た目を作る」 という設計にしておけば、対象が何であれ同じ書き方で同期できます。
ApplyState() を Start() からも呼んでいるのが、途中参加への対応です。あとから入った人は、届いた値を持った状態で Start() を迎えるか、OnDeserialization() で受け取ります。どちらでも扉が正しい状態になります。
3. 割り当てる
DoorButton に Udon Behaviour を追加し、SyncedDoor を指定します。
| 欄 | 指定するもの |
|---|---|
| Door Animator | Hierarchyの DoorPivot |
| Interaction Text | 扉を開ける |

Sync Modeの欄は表示されないか、Manualになっています。コードの属性で指定しているためです。
2人で確かめる
ClientSimで自分の画面が動くことを確認したら、実際のVRChatで確かめます。

SDKのBuilderで Number of Clients を 2、Force Non-VR を有効にして Build & Test を実行します。
| 順序 | 操作 | 期待する結果 |
|---|---|---|
| 1 | AとBが入る | 両方とも扉が閉じている |
| 2 | Aがボタンを押す | Aの扉が開き、少し遅れてBの扉も開く |
| 3 | Bがボタンを押す | Bが所有権を取り、両方とも閉じる |
| 4 | Aが開けたあと、Bが入り直す | Bは押していないのに、開いた状態で入る |
4番目が、この記事でいちばん大事な確認です。 同期変数を使っているので、あとから来た人にも最新の状態が届きます。
もしここで扉が閉じて見えるなら、Start() か OnDeserialization() で ApplyState() を呼んでいるか確認してください。
うまくいかないときは
- 自分の画面も動かない → 同期以前の問題です。Animatorのパラメータ名、Door Animatorの割り当てを確認します
- 自分だけ動いて、相手に届かない →
Manualの指定とRequestSerialization()を確認します - 押した人だけ動かない →
Interact()の中でもApplyState()を呼んでいるか確認します。OnDeserialization()は押した本人では呼ばれません - 途中参加すると閉じている →
Start()でApplyState()を呼んでいないか、Network Eventだけで伝えています - 相手が押すと戻る → 別のスクリプトが同じAnimatorを触っていないか確認します
おまけ:先に知っておくと良いこと
- 同時に押されたときは、最後の操作が残る: 2人がほぼ同時に押すと、どちらかが先に所有権を取ります。扉なら最終的に全員そろうので実害はありません。取りこぼしが困る用途(得点など)では、別の設計が要ります
- 自動ドアはもう一段むずかしい: 「範囲に入ったら開く」を同期させるには、「いま範囲に誰かいるか」を全員で共有する必要があります。人数を数える処理と、退出したときの引き継ぎが絡むので、この記事の範囲を超えます

- 鍵のかかる扉も同じ形: 「開いているか」に加えて「鍵が開いているか」を同期変数に足すだけです。状態が増えても書き方は変 わりません
- 音は各自で鳴らす: 開くときの音は、
ApplyState()の中で鳴らせば全員の画面で鳴ります。音そのものを同期する必要はありません - 使いどころはいろいろ: 脱出ゲームの扉、イベント会場の入場ゲート、隠し部屋の仕掛け、店舗ワールドのシャッター。どれも同じ形です
まとめ
同期する扉は、同期入門のライトと同じ形で作れます。
- 送るのは「開いているか」の真偽1つだけ
- 押した人が所有権を取ってから、値を変えて送信を頼む
- 見た目への反映は
ApplyState()にまとめ、起動時・押したとき・受信時から呼ぶ - 同期変数なら、途中参加の人にも最新の状態が届く
作るときの問いかけは、「この扉の状態は、あとから来た人にも見えていてほしいか」 です。イエスなら同期変数です。
持てる小物の位置を共有するなら VRC Object Sync、一瞬の演出を伝えるなら Network Events へ進んでください。