VRChatで同期をとるために頑張った話 RequestSerialization()を中心に

VRChatでワールド開発をしていて同期をとるのに苦労したので自分なりの解釈を書きます。
間違っている部分もあると思うのでご指摘いただけると幸いです。

## 概要
VRCで船を動かすワールドを作っており、ワールドの座標をプレイヤー間で共有することに苦労しました。最終的には以下の手順で同期処理を行いました。
1. Masterのみ`FixedUpdate()`関数内で座標を変数に格納
2. 新たなプレイヤーがJoinしてきた際`OnPlayerJoined(VRCPlayerApi player)`関数内で`RequestSerialization()`を実行し変数の値を同期し、オブジェクトの座標をセットしなおして座標の同期をとる
3. Master,非Masterにかかわらず、船の操縦に関するボタンをインタラクトした際は`SendCustomNetworkEvent(VRC.Udon.Common.Interfaces.NetworkEventTarget.All, "hoge")`ですべてのプレイヤーと共有し、船の動き自体はローカルで処理する

* 環境
  * Unity 2019.4.31f1
  * UdonSharp_v0.20.3

以降私が試したものを説明します。
コードが見たい方は[最終的なコード](#最終的なコード)まで飛ばしてください


## VRC_ObjectSync
「VRChat内でのオブジェクトの同期といえばこれ！」というぐらいに有名なやつです。
使い方は簡単でインスペクターのAdd ComponentでVRC_ObjectSyncを選ぶだけです。
私も当初はこれを使用していました。しかしこれは重大な問題を抱えており、常に座標の同期を行っているため通信環境によりパケットをロスした時に座標が更新されず数フレーム後に再度座標が取れるためカクカクして動いて見えてしまうのです。
私はこの春引っ越して新居にネット回線がなく賃貸のため工事ができない都合からやむを得ずいわゆる置くだけWi-Fiのホームルーターを使用しています。そのためほかのフレンドさんから「なんかこのワールド荒ぶってるんだけど」と言われ、初めて問題に気づかされました。
ここから私とVRChatの長く激しい戦いが始まりました。

## Udonsync
同期処理を考えるコードを書くのは初めてだったためググっていたところ以下に示すものが非常に参考になりました。
[シン・U# 入門 ② - ハツェの真時代傾向璋](https://hatuxes.hatenablog.jp/entry/2021/05/24/171942)
[UDONメモ - 黒鳥のメモ](https://kurotori4423.github.io/KurotoriMkDoc/VRChat/UDON%20%E3%81%A7%E3%82%AE%E3%83%9F%E3%83%83%E3%82%AF/)
これらの中でUdonSyncというワードが頻繁に出ており、クラス名の前に`[UdonBehaviourSyncMode(BehaviourSyncMode.Manual)]`
同期したい変数の前に
`[UdonSynced(UdonSyncMode.None)]`
をつけると同期処理ができるということがわかりました。
BehaviourSyncModeをContinuousにしていると常時同期をとるらしく。今回は上述の理由により常時同期を行うことが問題なので手動同期のManualを使用することに。
Manualを選択した際は`RequestSerialization()`で同期処理が実行されるためプレイヤーがJoinした際に実行するようにしました。しかし同期処理は取れませんでした。

## RequestSerialization()の罠
ここで`RequestSerialization()`の罠に遭遇してしまった。
名前からしてRequestとついているので非マスターがマスターに対し変数の値をリクエストし、マスターがリクエストを投げたプレイヤーにレスポンスを返すと解釈していた。
しかし実際は非Masterのプレイヤーが`RequestSerialization()`を実行したところで何も変化が起きないことが判明しました。
試しにMasterが
'RequestSerialization()'を実行したところ他のすべてのプレイヤーに値が送られた。
つまり`RequestSerialization()`は以下の挙動をすると考えられる。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/625910/08f0af47-6c88-24b7-9491-f1a589ee2f1b.png)

## 最終的なコード
めちゃくちゃ簡略化していますが、同期に関する部分は同じような感じです。

```C#:ManualSync.cs
using UdonSharp;
using UnityEngine;
using VRC.SDKBase;
using VRC.Udon;

[UdonBehaviourSyncMode(BehaviourSyncMode.Manual)]
public class ManualSync : UdonSharpBehaviour
{
    public GameObject Obj;
    [UdonSynced(UdonSyncMode.None)]public Vector3 Pos;
    [UdonSynced(UdonSyncMode.None)]public Vector3 Direction;
    [UdonSynced(UdonSyncMode.None)]public float Speed = 0;
    
    Public void Start()
    {
        Pos = Obj.transform.position;
        Direction = Obj.transform.forward;
    }
    
    Public void FixedUpdate()
    {
        if (Networking.IsOwner(Networking.LocalPlayer, this.gameObject))  //Masterのみ座標と方向を取得
        {
            Pos = Obj.transform.position;
            Direction = Obj.transform.forward;
        }
    }
    
    public override void OnPlayerJoined(VRCPlayerApi player)  //PlyaerがJoinしたら呼び出される
    {
        SendCustomNetworkEvent(VRC.Udon.Common.Interfaces.NetworkEventTarget.All, "Valuesync");
        sync(Pos, Direction);
        Obj.velocity = new Vector3(Speed,0,0);
    }
    
    public void Valuesync(){
        RequestSerialization();
    }
    
        public void sync(Vector3 Pos, Vector3 Direction)
    {
        Obj.transform.position = Pos;
        Obj.transform.forward = Direction;
    }
}
```
重要な点として、座標と方向を更新して変数に格納する処理はMasterのみが行います。
非Masterのプレイヤーがこの処理をすると値が同期されない問題が生じます。
次に重要な点としてプレイヤーがJoinした際すべてのプレイヤーに`RequestSerialization()`が実行されるようになっています。これはMasterが`RequestSerialization()`を実行しないと値が同期されず、Masterのみに実行させる方法がわからなかったのでめんどくさくなったのですべてのプレイヤーが実行するようになっています。
値が同期されて同期された座標、方向をセットするという処理を組み込めば完成です。
※Speedの値はほかのスクリプトから制御されるためここでは割愛しています。

以上私が考える回線が貧弱な環境向けの同期の取り方でした。