hyoromoのブログ

最近はVRSNS向けに作ったものについて書いています

VRChat知りうる限りの同期処理まとめ


約1年ほどU#でコードを書いてワールドを4つ作りました。その時に一番手こずったのが同期処理です。
毎回同期周りの仕様を忘れるせいで書くのもテストするのも大変です。
なので、今回はそんな同期周りをスッキリ整理して書き残しました。
間違っている箇所があれば指摘お願いします!

事前知識

※2021/12/17時点の情報なため、それ以降に記事が更新されていなければ情報が古い可能性があります。記事を読んだ上で鵜呑みにせずドキュメントを読んだり、実際に実装して確認ください

MasterとOwner


ワールドにはMasterとOwnerの概念があります。
OwnerはGameObject一つずつに更新権限を持つPlayerが割り当てられています。
Masterはインスタンスへ最初に入ったPlayerで、生成直後は全GameObjectはMasterがOwnerに割り当てられます。更に、Masterがインスタンスから抜けるとMaster変更が発生し、元MasterがOwnerだったものが新Masterに委譲されます。

GameObjectの同期

ワールドにあるGameObjectは何も設定しなければどれも同期されません。
じゃあどうやって同期するの?について書きます。

同期ルール
  • 同期したい内容に合わせて必要なコンポーネントや方法がある
  • 同期可能なのはOwnerのみ
  • GameObjectが非Activeな場合は同期されない*1
    なので同期要素をRoot GameObjectとし、モデル/Collider/Udon等は子に配置して子の方を非Activeにするテクニックがあります。特にUdonコンポーネントの付いているGameObjectが非Activeになると変数同期やSendCustomNetworkEventが届かなくてハマります
Transformの同期


GameObjectのTransformを同期したい場合、VRCObjectSync コンポーネントを同期したいGameObject付けるだけで同期されます。
注意点はGameObjectのOwnerのクライアント上で更新されたTransformが同期される点です。
なので、もしテニスゲームを作りたい時に何も考えず作ると別Ownerの弾をラケットで打てない・・・という事になります。
そんな時はAllow Collision Ownership Transferのチェックを入れます。別OwnerのGameObjectが衝突した時にOwner権限が委譲されます*2

変数の同期

UdonもしくはUdonSharp内で扱う変数も同期出来ます。
UdonSharpでの変数宣言は以下のように変数に [UdonSynced] を付けるだけで済みます。

public class Hoge : UdonSharpBehaviour {
    [UdonSynced]
    int _huga;
}

扱える型は以下を参照ください。種類は多いのですが現時点で配列は扱えないためカンマ区切り等の工夫が必要になります。
docs.vrchat.com
UdonSyncModeも設定可能ですが使ってないのでどの程度の物か把握してません。
github.com

同期の方法

GameObject単位の同期種類

種類 説明
Continuous 自動同期
変数等が自動的に同期されます。Transform等の頻繁に同期する必要がある場合に使用し、前述したVRCObjectSyncを追加したGameObjectはContinuous利用を強要されます
Manual 手動同期
RequestSerialization()を呼び出す事で同期変数が同期されます
Node 同期なし

同期変数が更新された時の受信メソッド
同期されると呼ばれるメソッドがあります。注意点は呼び出し元はこのメソッドが呼ばれません。

public override void OnDeserialization() {
    // 同期変数の受信プレイヤーが行う処理
}

OnDeserializationメソッドを使わずに以下リンク先のような書き方も出来ます。
github.com

同期するためのメソッドコール

処理同期するためにメソッドを呼び出すパターンもあります。
使い方は簡単で SendCustomNetworkEvent をU#コード内で呼べば対象メソッドが対象者へ届きます。

SendCustomNetworkEvent(VRC.Udon.Common.Interfaces.NetworkEventTarget.All, nameof(HogeFuga));

VRC.Udon.Common.Interfaces.NetworkEventTarget は色々設定出来ると思いきや All or Owner しかありません。Allだと自分もコール対象に含まれるため、都合が悪い場合は呼び出し先の冒頭に判定処理を入れます。

実例紹介

同期処理を作ってきたワールドでどのように実装したかの実例紹介。

曲のリクエストシステム

ジュークボックスのようなシステムは今までの仕様を把握していれば簡単に作れます。

[UdonSynced]
private string _playlistIds;

/// <summary>
/// 同期変数が更新された時に呼ばれる
/// </summary>
public override void OnDeserialization()
{
    if (Networking.IsMaster) {
        // TODO:Master管理下の曲管理システムを更新

        // 成功した事をオーナーへ返す
        SendCustomNetworkEvent(VRC.Udon.Common.Interfaces.NetworkEventTarget.Owner, nameof(OnSuccessedPlaylist));
    }
}

/// <summary>
/// プレイリストをリクエストする
/// </summary>
public void RequestPlaylist(string playlistIds)
{
    // 自身をオーナーに設定し、リクエストを送信する
    Networking.SetOwner(Networking.LocalPlayer, gameObject);
    if (Networking.IsOwner(gameObject)) {
        _playlistIds = playlistIds;

        RequestSerialization(); // 同期
        OnDeserialization();
    }
}

/// <summary>
/// プレイリストの登録が成功した時に呼ばれる
/// </summary>
public void OnSuccessedPlaylist()
{
    // TODO:Ownerが成功後に行う処理
}
  1. Ownerは RequestPlaylist 呼び出し
  2. GameObjectのOwner権限を得てから変数同期
  3. OnDeserializationがコールされたMasterが別途存在するU#の曲管理スクリプトへ情報を渡す
  4. Masterは成功結果をOwnerへ返す
  5. Ownerは OnSuccessedPlaylist が呼ばれて成功した事を知る

※RequestPlaylist/OnSuccessedPlaylistの名前はなんでもいいです。メソッド名で重要なのはOnDeserializationだけ

ここでは失敗ケースは端折っていますが、呼び出し元のUpdateSendCustomEventDelayedSeconds等でtimeoutを設けて失敗処理を書くとプレイヤーに親切です。実際に使ったのは [Unofficial] 765PRO VR Live Theater となります。プレイリストに曲を追加していき、Requestボタンを押下した時に実施しています。

乗り物(Interact対象)

乗り物である VRCStation はOwnerで無くても乗れます。ただし動かないのでちゃんとOwner付与してあげましょう。

public override void Interact()
{
    Networking.SetOwner(Networking.LocalPlayer, gameObject);
    
    // TODO:乗り物に乗るタイミングで行う処理
    // Stationだと直後に OnStationEntered が呼ばれるのでココに書く事は少ない
}

特に多く書く事もありませんが、Station/PickupといったInteract可能なUdonだとInteractした直後に Interact が呼ばれます。
実際に使ったのは Maze In Cube です。うるおぼえですが、このワールドのようにPlayerが座る位置を変更するためにPlayerEnterLocationを設定したら設定先もVRCObjectSyncコンポーネントを追加&Owner付与が必須だった気がします。

テスト方法

LocalBuildでもサブ垢でログイン出来るので別々にウィンドウ起動してテストします。
もしくは以下のような事をします。
hyoromo.hatenablog.com

まとめ

冒頭に書いた同期ルールさえ念頭に実装しておけば何とかなります。
怖がらずに同期対応してみてください。

*1:子も当然

*2:当然ですが両方のGameObjectにColliderを追加する必要があります