VRChatワールド開発　同期のコツみたいなもの

#概要
VRChatのワールドギミックを作るうえで避けては通れない同期問題。
同期処理を組む際に私が重要だと思うポイントを解説していきたいと思います。

#1.基礎知識
##*BroadCastType*と*VRC_ObjectSync*

まずは同期を行う際に最も重要なこの二つの要素を理解しましょう。
VRChatではプレイヤーの位置以外は、特別な処理を行わない限り同期されることはありません。
ほとんどの同期処理は、この二要素によって行われます。

VRC_Triggerの*BroadCastType*を**Local以外**に設定する事で、条件を満たした際Actionsをワールド内の他のプレイヤーに反映することができます。
ここで重要なのは、**"条件を満たしたプレイヤーからActionsのみが伝播する"**ということです。
他のプレイヤーが条件を満たしているかどうかはActionsの伝播に関係が無いことを覚えておいてください。

*BroadCastType*による同期とは、
　***全てのプレイヤーが同じActionsを行う ≒ 全てのプレイヤーで同じ状態になる***
という理屈な訳ですが、Actionsの伝播には通信ラグがあり、重力などの外部的な影響があるとずれが生じてしまいます。
そこで使われるのが*VRC_ObjectSync*であり、名前の通りオブジェクトの位置をリアルタイムで同期するコンポーネントです。

一見非常に便利な*VRC_ObjectSync*ですが、ここで一つ質問です。
　『*BroadCastType*は条件を満たしたプレイヤーを基準にActionsの同期を行う物でしたが、
　　*VRC_ObjectSync*は誰を基準に位置を同期させているのでしょうか？』

答えは**オーナーシップを持っているプレイヤー**です。

オーナーシップは**Pickupした時**や、**TakeOwnerShip関数を実行した時**等に移動します。
SpawnObjectアクションで発生したオブジェクトは、それを実行したプレイヤーがオーナーシップを持ちます。

スクリプトによる移動が絡んだオブジェクトを同期させる際は、**誰がオーナーシップを所有しているか**を意識して処理を組みます。

#2.同期のTips
##処理させるプレイヤーは一人に絞る
前章で解説した通り、同期処理は必ず一人のプレイヤーを基準に行われます。
複数のプレイヤーが処理に関わると、通信ラグも相まって同期が非常に難しくなるため、基本的に**一人のプレイヤーに処理させるようにします。**
##*Local*を活用する
*BroadCastType*の*Local*は**Actionsを他のプレイヤーに伝播させません。**
*Local*でコンポーネントのEnableや、Animatorのパラメータを変更する事で特定のプレイヤーだけで処理を行う事ができます。

また、一部のVRC_Triggerの条件は複数のプレイヤーが同時に満たす場合があるため、その際も*Local*を使いActionsが余計に伝播されるのを防ぎます。
>必ず全てのプレイヤーで満たされる条件
>>OnPlayerJoined OnPlayerLeft OnSpawn OnDestroy OnStationEntered OnStationExited

>状況によって複数のプレイヤーで満たされる条件
>>Custom OnEnable OnTimer OnEnterTrigger OnExitTrigger OnEnterCollider OnExitCollider OnAvatarHit 

##Masterで処理する
特定のプレイヤーに処理させる必要が無い場合、**ワールドのMaster上で処理する**のが効果的です。
*BroadCastType*の*Master*はその条件を満たしたのがワールドのMasterの場合のみActionsの実行、伝播を行います。
*Always*のActivateCustomTriggerから*Master*のCustomを発火させる、等の方法で全ての処理をMasterに集約します。


##見た目だけを同期する
**VRC_ObjectSync**が入ったオブジェクトは、親子構造が複雑になるとオーナーシップの取得に問題が発生しやすいです。

そういう場合は実際に動かすオブジェクトは同期させず、親子関係外にある、外見とコライダだけのオブジェクトを*AutoCam*や、*FollowTarget+LookatTarget*で追従させて*VRC_ObjectSync*を追加する事で同期処理を単純化します。

*VRC_Station*でプレイヤーを移動させる際、ピックアップが荒ぶる現象の対策にもなったりします。

#3.実用例
GoogleDriveにUnityPackageをアップロードしているので、そちらをインポートしてからご覧ください。
https://drive.google.com/open?id=1927gFc3JDWZjh-U2soV9BOB-dkz-D4SG
##同期ドア
Prefab名**"SyncDoor"**
アニメーションで開閉するドアです。

ドアの開閉を切り替える程度のアニメーションなら、*Animator*のパラメータ変更を*Always*で行う事で十分同期させる事ができます。
Toggleは使わず、オブジェクトを分けてTrueとFalseで処理します。
![2018-06-27 (1).png](https://qiita-image-store.s3.amazonaws.com/0/266653/6aaa084b-6d5e-98c9-a75a-546608562dba.png)

最近では*VRC_SyncAnimation*を使った同期も確立されているので、アニメーションの種類が多い場合等はそちらの方が安定するでしょう。
##タイマー
Prefab名**"TimerSystem"**
簡易的なカウントダウンタイマーです。

タイマーがスタートすると、1秒毎に*Animator*からアニメーションイベントによって"Timer"という*Custom*トリガーが起動します。
これをそのまま*Local*で処理してしまうと、各自のPC処理速度によってタイマーの経過に差が出来てしまいます。

最初のタイマー起動を*Always*で行い、時間の進行処理を*Master*にする事で、全てのプレイヤーの時間経過処理をワールドMasterに一本化し同期させています。
![2018-06-27 (4).png](https://qiita-image-store.s3.amazonaws.com/0/266653/9473584a-acc3-8716-509b-0d18670b9b5e.png)

##銃弾再利用銃
Prefab名**"ResetPositionGun"**
*VRC_SceneResetPosition*を使って、弾をスポーンさせず使いまわす銃です。

銃をピックアップした際の処理が最も重要で、まず全ての銃弾のオーナーシップを銃の所持者に取得させます。
そして*Always*で全てのプレイヤーから*Collider*と*FollowTarget*をDisableさせてから、*Local*で同じ要素を逆にEnableにしています。
これは、同期の基準となるプレイヤーと、それ以外のプレイヤーの処理を分けるためのものでとても大事です。
![2018-06-27 (5).png](https://qiita-image-store.s3.amazonaws.com/0/266653/b87a6f81-65ac-1e2e-c86f-2e86b2b5a706.png)

*FollowTarget*は同期の基準となるプレイヤー以外でEnableになっていると*VRC_ObjectSync*の位置同期を上書きしてしまうので切っておき、基準プレイヤーでは銃弾が使用されていない時ワールド上空に格納しておく役割があるため*Local*で変更を行います。
後は弾が発射されたらアニメーションイベントから*ResetPosition関数*と*AddForce*のActionsが設定された*Custom*トリガーを呼び出しています。
![2018-06-27 (6).png](https://qiita-image-store.s3.amazonaws.com/0/266653/d466567d-87eb-7b7d-42a3-c2bd6a9c627d.png)

#4.おわりに
同期のアプローチ方法に正解は無く、手段は無限にあります。

行いたい処理によって最適な手段は変わってくるので、私が行っている手段もあくまで参考の一つとして、自分なりに最適な方法を探ってみて下さい。いい方法が見つかったらシェアしてくれると嬉しい。

VRChat 技術メモ帳
http://vrcworld.wiki.fc2.com/