約1年ほどU#でコードを書いてワールドを4つ作りました。その時に一番手こずったのが同期処理です。
毎回同期周りの仕様を忘れるせいで書くのもテストするのも大変です。
なので、今回はそんな同期周りをスッキリ整理して書き残しました。
間違っている箇所があれば指摘お願いします!
事前知識
※2021/12/17時点の情報なため、それ以降に記事が更新されていなければ情報が古い可能性があります。記事を読んだ上で鵜呑みにせずドキュメントを読んだり、実際に実装して確認ください
GameObjectの同期
ワールドにあるGameObjectは何も設定しなければどれも同期されません。
じゃあどうやって同期するの?について書きます。
同期ルール
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が成功後に行う処理 }
- Ownerは RequestPlaylist 呼び出し
- GameObjectのOwner権限を得てから変数同期
- OnDeserializationがコールされたMasterが別途存在するU#の曲管理スクリプトへ情報を渡す
- Masterは成功結果をOwnerへ返す
- Ownerは OnSuccessedPlaylist が呼ばれて成功した事を知る
※RequestPlaylist/OnSuccessedPlaylistの名前はなんでもいいです。メソッド名で重要なのはOnDeserializationだけ
ここでは失敗ケースは端折っていますが、呼び出し元のUpdateやSendCustomEventDelayedSeconds等で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
まとめ
冒頭に書いた同期ルールさえ念頭に実装しておけば何とかなります。
怖がらずに同期対応してみてください。