Photon PUN2で動的にネットワークオブジェクトを生成する

## PUN2（Photon Unity Networking 2）とは
Unityでサーバー構築などなしに、マルチプレイを実現できるアセットです。
詳しくは[公式ドキュメント](https://doc.photonengine.com/ja-jp/pun/v2/getting-started/feature-overview)や、[PUN2（Photon Unity Networking 2）で始めるオンラインゲーム開発入門](https://zenn.dev/o8que/books/bdcb9af27bdd7d)をご覧ください。

## 開発環境
- Windows 11
- Unity 2020.3.21f
- PUN 2 - FREE v2.40  
（その他）
- MRTK 2.7.3

## 通常のネットワークオブジェクトの生成
通常は表示するモデルのPrefabにPhotonViewをアタッチしておき、これを下記のように生成するのが一般的です。

```csharp
PhotonNetwork.Instantiate("MyPrefabName", new Vector3(0, 0, 0), Quaternion.identity, 0);
```

しかしこの方法だとPrefabで使用していたモデルを変更したり、新規にモデルを追加する時に、毎回PhotonViewを追加しなくてはいけません。
これが煩わしいので今回は動的にネットワークオブジェクトを生成します。

## 動的にネットワークスオブジェクトを生成

```csharp
public class ModelCreator : MonoBehaviour, IOnEventCallback
{
    private const byte CustomInstantiateEventCode = 1;

    [SerializeField]
    private GameObject prefab; // PhotonViewをアタッチしていないPrefab

    [SerializeField]
    public Camera camera;

    [SerializeField]
    public float offset = 2f;

    private void OnEnable()
    {
        PhotonNetwork.AddCallbackTarget(this);
    }

    private void OnDisable()
    {
        PhotonNetwork.RemoveCallbackTarget(this);
    }

    public void CreateModel()
    {
        CreateModel(prefab);
    }

    private GameObject CreateModel(GameObject prefab)
    {
        var obj = Instantiate(prefab, camera.transform.position + camera.transform.forward * offset, Quaternion.identity);

        // MRTK の設定（Prefabを操作できるようにする）
        var objManipulator = obj.AddComponent<ObjectManipulator>();
        objManipulator.TwoHandedManipulationType = TransformFlags.Move | TransformFlags.Rotate;
        obj.AddComponent<NearInteractionGrabbable>();

        // Photon
        var photonView = obj.AddComponent<PhotonView>();
        var photonTransformView = obj.AddComponent<PhotonTransformView>();

        // 初期化を行う
        photonView.ObservedComponents = new List<Component>();

        // Synchronizeするものを設定
        photonTransformView.m_SynchronizePosition = true;
        photonTransformView.m_SynchronizeRotation = true;
        photonTransformView.m_SynchronizeScale = false;
        photonTransformView.m_UseLocal = true;

        // PhotonViewに紐付ける
        photonView.ObservedComponents.Add(photonTransformView);

        // 同期にはViewIDが必要なため取得する
        if (PhotonNetwork.AllocateViewID(photonView))
        {
            // PrefabのtransformとViewIDを通知する準備をする
            var data = new object[]
            {
                obj.transform.position,
                obj.transform.rotation,
                photonView.ViewID
            };

            // 同じRoomの自分以外に通知
            var raiseEventOptions = new RaiseEventOptions
            {
                Receivers = ReceiverGroup.Others,
                CachingOption = EventCaching.AddToRoomCache
            };

            var sendOptions = new SendOptions
            {
                Reliability = true
            };

            // 同じRoom内の他のユーザーへ通知
            PhotonNetwork.RaiseEvent(CustomInstantiateEventCode, data, raiseEventOptions, sendOptions);
        }
        else
        {
            Debug.LogError("Failed to allocate a ViewId");
            Destroy(obj);
        }
    }

    // RaiseEventを受け取る
    public void OnEvent(EventData photonEvent)
    {
        var eventCode = photonEvent.Code;

        if (eventCode != CustomInstantiateEventCode)
        {
            return;
        }

        var data = (object[])photonEvent.CustomData;

        // 受信したtransformを設定
        var obj = Instantiate(prefab, (Vector3)data[0], (Quaternion)data[1]);

        // MRTK の設定（Prefabを操作できるようにする）
        var objManipulator = obj.AddComponent<ObjectManipulator>();
        objManipulator.TwoHandedManipulationType = TransformFlags.Move | TransformFlags.Rotate;
        obj.AddComponent<NearInteractionGrabbable>();

        // Photon
        var photonView = obj.AddComponent<PhotonView>();
        var photonTransformView = obj.AddComponent<PhotonTransformView>();

        // 初期化を行う
        photonView.ObservedComponents = new List<Component>();

        // Synchronizeするものを設定
        photonTransformView.m_SynchronizePosition = true;
        photonTransformView.m_SynchronizeRotation = true;
        photonTransformView.m_SynchronizeScale = false;
        photonTransformView.m_UseLocal = true;

        // PhotonViewに紐付ける
        photonView.ObservedComponents.Add(photonTransformView);

        // 受信したViewIDを用いて同期する
        photonView.ViewID = (int)data[2];
    }
}
```

### ポイント

```csharp
// 初期化を行う
photonView.ObservedComponents = new List<Component>();

// PhotonViewに紐付ける
photonView.ObservedComponents.Add(photonTransformView);
```

PhotonView.ObservedComponentsを初期化してから、PhotonTransformViewをAddしないと追加されません（追加されませんでした）。

```csharp
// 同期にはViewIDが必要なため取得する
if (PhotonNetwork.AllocateViewID(photonView))
```

同期にはViewIDが大事な要素になるため、ViewIDを作成します。

```csharp
// PrefabのtransformとViewIDを通知する準備をする
var data = new object[]
{
    obj.transform.position,
    obj.transform.rotation,
    photonView.ViewID
};

// 同じRoom内の他のユーザーへ通知
PhotonNetwork.RaiseEvent(CustomInstantiateEventCode, data, raiseEventOptions, sendOptions);
```

ViewIDをPhotonNetwork.RaiseEventを用いて、同じRoom内の他のユーザーへ通知を行います。


```csharp
// 受信したViewIDを用いて同期する
photonView.ViewID = (int)data[2];
```

OnEventで受信したViewIDを設定します。
これで同じViewIDを持つオブジェクト同士が同期されます。

## 実行

HoloLens2とUnityのEditorで同期した時の動画です。
HoloLens2側でオブジェクトを表示して、動かしているところを、UnityのEditorから見ている動画です。

https://twitter.com/fit51/status/1485971588965040128


## 参考サイト
- [Photon Unity Networkingで動的に同期オブジェクトを生成する方法](https://qiita.com/_karukaru_/items/f90fe4f269a72d2b46d0)
- [Instantiation | Photon Engine](https://doc.photonengine.com/ja-jp/pun/v2/gameplay/instantiation)