VRC Udon Networking : OwnerShip

## VRChat UdonNetworking
VRCでは、オブジェクトの同期を行うために `VRC Object Sync` というスクリプトを提供しており、これにより開発者がオブジェクト同期を直接実装する必要がなくなっています。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3682031/98cac0e9-c634-d8df-6987-b42b80b1906f.png)

グローバル化したいオブジェクトにこのスクリプトを追加するだけで、自動的に同期が行われます。
このスクリプトに満足できず、独自に同期を実装したスクリプトも存在しますが、特別な状況でなければ `VRC Object Sync` で十分対応可能です。

UdonSharpでは、`VRC.SDK3.Components.VRCObjectSync` クラスとして使用可能で、メソッドが5つありますが、実際に使用されるのは `FlagDiscontinuity()` と `Respawn()` の2つです。
`Respawn()` はオブジェクトを初期位置に戻すメソッドです。
ワールドでよく見かけるリスポーンボタンを押すと、グローバルオブジェクトが一掃されますが、これを利用しています。
`FlagDiscontinuity()` はオブジェクトを瞬時に移動させたいときに使用します。この関数を呼び出すと、そのフレームで発生したオブジェクトの移動がスムージング処理なしで、文字通り瞬間移動のように見えます。

実際には `TeleportTo()` というメソッドも存在しますが、記憶に残っている限りではあまり良くないため、以下のように別途ライブラリを作成して使用する方が良いです。

```cs
public void PositioningGameObject(GameObject inputObject, Vector3 inputPosition, Quaternion inputAngle)
{
    Networking.SetOwner(Networking.LocalPlayer, inputObject);
    inputObject.GetComponent<Rigidbody>().velocity = new Vector3(0f, 0f, 0f);
    inputObject.GetComponent<Rigidbody>().angularVelocity = new Vector3(0f, 0f, 0f);
    inputObject.GetComponent<Rigidbody>().position = inputPosition;
    inputObject.GetComponent<Rigidbody>().rotation = inputAngle;
    inputObject.transform.SetPositionAndRotation(inputPosition, inputAngle);
    if (inputObject.GetComponent<VRC.SDK3.Components.VRCObjectSync>())
    {
        inputObject.GetComponent<VRC.SDK3.Components.VRCObjectSync>().FlagDiscontinuity();
    }
}
```

大まかに `Rigidbody` の物理量を初期化し、位置を移動させた後に `FlagDiscontinuity()` をトリガーする方法です。
スクリプトの最初の行ではオブジェクトのオーナーを設定していますが、実際には `VRCObjectSync` を通じた同期はオブジェクトのオーナーを中心に行われます。
ワールドマスター中心の変数やイベントの同期とは異なる部分なので、注意が必要です。

オーナーシップの移動は、プレイヤーがオブジェクトを掴んだときに自動的にそのオブジェクトのオーナーになり、直接制御したい場合は `Networking.SetOwner()` を使用してリクエストすることも可能です。
イベントなどを通じて直接オブジェクトを動かしたい場合は、ワールドマスターにオーナーを譲渡してから行う方がデバッグも容易で良好です。
オーナーシップ移動に関する詳細は、以下の画像でまとめられていますので参照してください。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3682031/fea9e06b-362c-3dd1-ebe8-281ebbb612ca.png)

最後に、`VRCObjectSync` は `Transform` と `Rigidbody` を同期しますが、該当オブジェクトの `active` 状態は同期要素ではないことを理解しておく必要があります。
そのため、オブジェクトのグローバルなオンオフは別途イベントで実装するか、`VRC.SDK3.Components.VRCObjectPool` を使用することも可能です。

`VRCObjectPool` は多数のグローバルオブジェクトを管理する際に便利です。このプールの利点は、プール内のオブジェクトのアクティブ状態を自動的に同期してくれる点です。
後から参加したプレイヤーのために、別途オンオフ状態の更新を実装する必要がありません。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3682031/58c8d27c-c76e-671f-879a-fe0f3568aa26.png)

クラスプロパティである `GameObject[] Pool` を通じて各オブジェクトにアクセスできます。
`VRCObjectPool` に割り当てられたオブジェクトは、ランタイム時に全て非アクティブ状態でロードされます。
オブジェクトは `TryToSpawn()` を通じてアクティブ化し、`Return()` を通じて非アクティブ化できます。これらを使用することで、正しくアクティブ状態が同期されます。

しかし、非常に複雑な点として `TryToSpawn()` にはオブジェクトの引数を渡すことができず、プールのインデックス順にアクティブ化されるため不便です。
さらに `Return()` にはオブジェクトを渡すことが可能なのが奇妙です。
とにかく、望むオブジェクトだけをアクティブ化するためには、プールをループしながら `TryToSpawn()` で全てをアクティブ化し、不要なものは `Return()` で戻すという手間のかかる作業が必要になります。

```cs
Debug.Log("cardIndex: " + cardIndex);
for (int i = 0; i < cards.Length; ++i)
{
    cardPool.TryToSpawn();
}
for (int i = 0; i < 4; ++i)
{
    Debug.Log("i: " + i);
    if (i != cardIndex)
    {
        Debug.Log("Return " + cards[i].name);
        cardPool.Return(cards[i]);
    }
}
```

そのため、基本的なゲームオブジェクトの `SetActive()` を使用し、同期は別途イベントで実装する方が簡便な場合もあります。好みに応じて選択してください。