メッセージ

誰でも送れる型付きイベント。誰が処理するか、オブジェクトごとのチャンネル、通信量まで。

更新日 2026-09-10

メッセージを宣言する

メッセージは、フィールドと、その運び方を決める 2 つのメソッドを持つクラスです。

public class OpenDoor : NetworkEvent
{
    public int doorId;

    public override void Serialize(ByteWriter writer)
    {
        writer.WriteInt32(doorId);
    }

    public override void Deserialize(ByteReader reader)
    {
        doorId = reader.ReadInt32();
    }
}

詰め方を自分で書くのは、推測させるほうが高くつくからです。Udon にはリフレクションがないので、自動でシリアライズする仕組みにも結局は全フィールドの説明を渡すことになります。フィールドのない合図なら、どちらのメソッドも override しません。

「お願い」と「事実」を分けてください。 OpenDoorRequestDoorOpened の 2 つの型は、1 つの型を 2 回送るよりほぼ常に優れています。ハンドラーは型を見ればどちらか分かり、誰かのお願いを決定事項として扱ってしまうことがありません。

メッセージの行き先は 2 つ

メッセージの行き先は、ルーム全体か、あるオブジェクト自身のチャンネルか。選択肢はこれだけで、書き方がどちらかを示します。

Room.Send(new OpenDoor { doorId = 3 });   // ルーム。全クライアントへ
this.Send(new Touched());                 // このオブジェクト自身のチャンネル
channel.Send(new Touched());              // 参照した別のオブジェクトのチャンネル

this.SendRoom.Send ではありません。 ビヘイビアが乗っているオブジェクト自身のチャンネルに送るので、どのオブジェクトの話かを id で運ぶ必要がありません。Room.Send はルーム全体へのほうで、static なので this を持たないただのクラスや抽象クラスからも呼べます。

購読もそのまま対応します。

Room.Subscribe<OpenDoor>(OnOpenDoor);     // ルームからその型を受け取る
this.Subscribe<Touched>(OnTouched);       // このオブジェクトのチャンネルで受け取る
channel.Subscribe<Touched>(OnTouched);    // 別のオブジェクトのチャンネルで受け取る
書き方 行き先 受け取るのは
Room.Send(evt) ルーム 全クライアントで、その型を購読しているものすべて
this.Send(evt) このオブジェクト このオブジェクトでその型を購読しているものすべて
channel.Send(evt) そのオブジェクト そのチャンネルでその型を購読しているものすべて

誰でも送れるようにしたいならルームです。オブジェクト自身のチャンネルはオーナーが 1 人なので、this.Send にはそのオブジェクトの所有が要ります

誰が処理するか

宛先が決めるのは誰が処理するかであって、誰が送ってよいかではありません。

Room.Send(message, NetworkTarget.All);         // 全員。既定
Room.Send(message, NetworkTarget.Master);      // 調停役だけ
Room.Send(message, NetworkTarget.Others);      // 送信者以外
Room.Send(message, player);                    // 1 人だけ

どのクライアントも、どんな宛先を書いたメッセージでも送れますし、バイト列はどのみち全員に届きます。NetworkTarget.Master は絞り込みであって許可ではないので、本当に権限が必要なワールドは送信者を確認します。

private void OnOpenDoorRequest(VRCPlayerApi sender, OpenDoorRequest message)
{
    if (!Room.IsMaster || !Allowed(from))
        return;

    Room.Send(new DoorOpened { doorId = message.doorId });
}

これがマスターをサーバーとみなす形です。クライアントが尋ね、調停役が決め、その調停役から全員へのメッセージだけが実際に何かを変えます。

保証されること

順番どおり、一度ずつ、送信者も含めて。 送信したクライアントは即座に自分で処理します。自分の書き込みは自分には届かないので、そうしないと「知っている唯一のクライアントだけが受け取れない」ことになります。

同じ束が 2 回届いても 1 回だけ処理します。 各メッセージに連番があり、読む側はどこまで処理したかを覚えています。VRChat に一度断られた再送も何も変えません。

あとから入った人は、見ていない分を受け取りません。 各バッチはいつ作られたかを持っていて、後から参加したクライアントは自分が来る前のものを、見ていない争いの再生ではなく破棄として扱います。同期値に求めるものとは逆で、イベントに求めるものそのものです。あとから来た人に現在の状態を知ってほしいときは同期値を使ってください。

断られた送信でも失われません。 VRChat は 1 つのビヘイビアが一度に送れる量を制限し、超えた束は切り詰めではなく拒否されます。拒否された束は保持して再送します。Room.Bus.Pending がずっと 0 より大きいワールドは、回線が受け取れる以上に送っています。

オブジェクト自身のチャンネル

あるオブジェクトについてのメッセージは、ルームではなくそのオブジェクトを通せます。どのオブジェクトの話かを id で運ぶ必要がなくなります。付けるコンポーネントも参照も要らず、同じ 2 つの動詞をビヘイビア自身の上に書きます。

public override void Start()
{
    this.Subscribe<Touched>(OnTouched);
}

public override void Interact()
{
    this.Send(new Touched());
}

ハンドラはプレイヤーも受け取れば、誰が送ったかを知らされます。

this.Subscribe<Touched>((sender, message) => Log.Info(sender.displayName + " が触った"));

送信にはそのオブジェクトの所有が要ります。 replicate されるのは所有者の書き込みだけなので、非所有者はメッセージが出ていかなかったと後から気づくのではなく、その場で知らされます。どのクライアントからも送れるようにしたいなら、先に Ownership.Claim(gameObject) で所有を取ってください。ピックアップや interact なら、たいていすでに取れています。

別のオブジェクトのチャンネルを外から読むには、そのオブジェクトに NetworkBus が要ります。参照に対して同じ動詞が使えます。

public NetworkBus channel;

channel.Subscribe<Touched>(OnTouched);
channel.Send(new Touched());

どちらにせよオブジェクトのチャンネルはオーナーが 1 人なので、2 人が同じチャンネルに送ると奪い合いになります。オブジェクトのオーナーが送るならオブジェクトのチャンネル、誰でも送るならルームを使ってください。

通信量

1 メッセージあたりの追加は 3 バイトです。誰が処理するかで 1、長さで 1、どの型かで 1。特定のプレイヤー宛てなら、そのプレイヤーの id も乗ります。

バッチには、いつ作られたかを示す 4 バイトが 1 回だけ乗ります。1 メッセージごとではなくバッチごとなので、まとめて送るほど薄まります。

実際に効いてくるのは、1 つのビヘイビアの同期状態に対する VRChat の上限です。これは 1 メッセージではなく束全体にかかります。this.Pending() はそれが近いことを見る手段です。ほぼ常に 0 で、0 より上のままなら 1 回の同期が運べる量よりバッチが大きいということです。

このパッケージなしのカスタムイベント

VRChat 自身のイベント呼び出しは、上のどれを使うかに関係なく使えます。コンパイラがそのまま通すからです。

this.SendCustomNetworkEvent(NetworkEventTarget.All, nameof(Ring));

public void Ring()
{
    bell.Play();
}

public なメソッドを名前で全クライアントで呼びます。NetworkEventTarget.Owner ならオーナーだけです。データは運べません。型付きメッセージがあるのはそのためです。SendCustomEventDelayedSecondsSendCustomEventDelayedFrames はローカルの遅延呼び出しで、送信はしません。

OnOwnershipRequest を override して false を返せば移譲を拒否でき、OnOwnershipTransferred はオーナーが変わったとき全クライアントで動きます。ClientSim はオーナーシップ要求を発生させないので、この 2 つはアップロードしたワールドで確かめてください。

何も届かないとき

推測させずに、ビヘイビアごとに一度だけ知らせます。

  • シーンにバスがない状態で送ったので、メッセージがどこにも行かなかった。
  • 購読したのに、10 秒たっても読めるバスがどこにもない。

どちらも実際には同じことです。ルームのメッセージを運ぶオブジェクトが無いか、無効になっています。最初に送るときに自動で追加されるので、たいていは何かと一緒に無効化されたのが原因です。

問題が発生しました 再読み込み