【Unity】【PUN2】Photon2でPrefab以外の動的なオブジェクト生成を行う

この記事では、PUN2(Photon Unity Networking 2)[[1]](https://assetstore.unity.com/packages/tools/network/photon-unity-networking-classic-free-1786?locale=ja-JP)を用いて事前にPrefabとして用意できないGameObjectのインスタンス化を行うコードを示します。

# Photonとは
Photon(PUN2)とは、Unityで簡単にオンラインゲームが開発できるフレームワークです。リアルタイム・マルチプレイヤー対応で、サーバを用意する必要がありません。無料枠では最大20人、60GB/月の転送料を使用できます。

# 実行環境
Windows10
Unity 2021.3.8f1
Photon Unity Networking Free ver 1.105

# 通常のネットワークオブジェクトのインスタンス化
別のクライアントにも同じネットワークオブジェクトを生成する際には、事前にUnityのGameObjectをPrefabとして、Resourcesフォルダに入れておけば、

```csharp:instance.cs
using Photon.Pun;
using Photon.Realtime;

// Resourcesパスに置いたPrefabからネットワークオブジェクトの生成
PhotonNetwork.Instantiate("PREFAB_NAME", Vector3.zero, Quaternion.identity);
```

上記のコードを用いて、インスタンスを生成することができます。[[2]](https://doc.photonengine.com/pun/current/demos-and-tutorials/pun-basics-tutorial/player-instantiation)[[3]](https://zenn.dev/o8que/books/bdcb9af27bdd7d/viewer/e3765a#%E3%82%A4%E3%83%B3%E3%82%B9%E3%82%BF%E3%83%B3%E3%82%B9%E3%81%AE%E7%94%9F%E6%88%90)
この方法では、自動的に別クライアントで生成＋位置情報の共有までやってくれます。

しかし、事前にPrefab化できないオブジェクトを全クライアントでインスタンス化&共有したい際には、この方法ではできません。

# 動的なオブジェクトをネットワークオブジェクトとしてインスタンス化する

以下のようなコードを作成します。

```csharp:InstanceCustomNetworkObject.cs
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using Photon.Pun;
using Photon.Realtime;
using ExitGames.Client.Photon;

// OnEventコールバック, Photonを用いるために継承する
public class InstanceCustomNetworkObject : MonoBehaviourPunCallbacks, IOnEventCallback
{
    public GameObject wantCreateAsNetworkObject;
    private GameObject createdNetworkObject;
    // Eventごとに設定する番号。一意でなければならない
    private const byte CustomManualInstantiationEventCode = 10;

    // 生成したいオブジェクトを設定する
    public void SetGameObjectWantCreate(GameObject gameObject) 
    {
        wantCreateAsNetworkObject = gameObject;
    }

    public void Instance()
    {
        // 全てのクライアントで対象のゲームオブジェクトを生成
        photonView.RPC(nameof(CreateNetworkObj), RpcTarget.All);
        // ViewIDを与えてネットワークオブジェクトとして同期する
        AddViewID();
    }

    [PunRPC]
    private void CreateNetworkObj()
    {
        // インスタンス化を行う
        createdNetworkObject = Instantiate(wantCreateAsNetworkObject, Vector3.zero, Quaternion.identity);
        // Photonの同期に用いるコンポーネントのアタッチ
        var photonView = createdNetworkObject.AddComponent<PhotonView>();
        var photonTransformView = createdNetworkObject.AddComponent<PhotonTransformView>();

        // 初期化を行う
        photonView.ObservedComponents = new List<Component>();
        // Synchronizeするものを設定
        photonTransformView.m_SynchronizePosition = true;
        photonTransformView.m_SynchronizeRotation = true;
        photonTransformView.m_SynchronizeScale = true;
        photonTransformView.m_UseLocal = true;

        // photonTransformViewをPhotonViewに設定する
        photonView.ObservedComponents.Add(photonTransformView);
    }

    // 同期の為のViewIDを与えて、他のクライアントに共有する
    private void AddViewID() 
    {
        PhotonView photonView = createdNetworkObject.GetComponent<PhotonView>();

        // ViewIDの割り当て
        if (PhotonNetwork.AllocateViewID(photonView))
        {
            var data = new object[]
            {
                photonView.ViewID,
            };
            RaiseEventOptions raiseEventOptions = new RaiseEventOptions
            {
                // 自分以外のすべてに通知する
                Receivers = ReceiverGroup.Others,
                // 後から接続するクライアントが受信できるようにキャッシュする
                CachingOption = EventCaching.AddToRoomCache
            };
            SendOptions sendOptions = new SendOptions
            {
                Reliability = true
            };
            // 他のクライアントへ通知する
            PhotonNetwork.RaiseEvent(CustomManualInstantiationEventCode, data, raiseEventOptions, sendOptions);
        }
        else
        {
            Debug.LogError("Failed to allocate a ViewId.");
            Destroy(createdNetworkObject);
        }
    }
    
    
    public void OnEvent(EventData photonEvent)
    {
        // イベントを受け取り、ViewIDを同期する
        if (photonEvent.Code == CustomManualInstantiationEventCode)
        {
            if (createdNetworkObject == null) return;
            // 受信したViewIDを用いて同期する
            var data = (object[])photonEvent.CustomData;
            var photonView = createdNetworkObject.GetComponent<PhotonView>();
            photonView.ViewID = (int)data[0];
        }
    }
}
```

# 使い方

## 1.PhotonViewコンポーネントをアタッチしたオブジェクトに、上記で作成したコードをアタッチします。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1865650/7fe31d4c-6974-78b7-efd1-dafe4d5563f9.png)

## 2.SetGameObjectWantCreateの引数に、ネットワークオブジェクトとして生成したいGameObjectを設定します。
```csharp
InstanceCustomNetworkObject.SetGameObjectWantCreate(GameObject);
```
UnityエディタのInspectorViewから、直接InstanceCustomNetworkObjectコンポーネントのWantCreateAsNetworkObject欄に、GameObjectを設定しても大丈夫です。

## 3.Instanceメソッドの実行
```csharp
InstanceCustomNetworkObject.Instance();
```
Instanceメソッドを呼ぶことで、動的なオブジェクトの作成と位置や回転の同期ができます。

## 実行サンプル
以下のコードをPhotonViewコンポーネントをアタッチしたオブジェクトに、アタッチするとサンプルとして実行できます。サンプルではCudeをスクリプトから生成し、同期します。

```csharp:Test.cs
using UnityEngine;
using Photon.Pun;

public class CreateTest : MonoBehaviourPunCallbacks
{
    InstanceCustomNetworkObject instanceCustomNetworkObject;

    void Awake()
    {
        instanceCustomNetworkObject = GetComponent<InstanceCustomNetworkObject>();
    }

    // 全てのクライアントで作成したいGameObjectを設定する
    [PunRPC]
    public void SetInstance() 
    {
        // テストの為動的なオブジェクトを生成する
        var cube = GameObject.CreatePrimitive(PrimitiveType.Cube);
        // 生成対象オブジェクトとして設定する
        instanceCustomNetworkObject.SetGameObjectWantCreate(cube);
        // 同期対象ではないオブジェクトなので消去する
        Destroy(cube);
    }

    void Update()
    {
        if (Input.GetKeyDown(KeyCode.A)) 
        {
            // 生成対象オブジェクトを設定する
            photonView.RPC(nameof(SetInstance), RpcTarget.All);
            // 全てのクライアントで生成・同期する
            instanceCustomNetworkObject.Instance();
        }
    }
}
```

# 備考
Photonにおいては、ネットワークオブジェクト(同期されるオブジェクト)はViewIDを用いて管理されるようです。つまり、ViewIDさえ同じものを設定してしまえば位置や回転が同期されます。

今回新しく作ったInstanceCustomNetworkObjectクラスでは、全てのクライアントで同じゲームオブジェクトを生成します。その後、一意のViewIDをマスタークライアントでのみ設定し、Photonのイベントを用いて、他のクライアントへViewIDを送信して同期します。

Photonを介して、直接GameObjectを送信できれば楽なのですが、通信量などを考えるとシリアライズできたとしても現実的ではないのだと思われます。デフォルトで、Resouecesに置いたPrefabを生成できるようにしているのも通信量を抑えるためなのでしょうね。（GameObjectを送信する方法ってありますか？）

公式ドキュメントのやり方[[4]](https://doc.photonengine.com/ja-jp/pun/current/gameplay/instantiation)でも、あらかじめ生成するGameObjectはそれぞれのクライアントで共有されていることが前提のコードでした。(あと少し分かりにくい)
なんとかして楽な方法が欲しいところですね。

# 参考と活用事例
動的なオブジェクトを同期したい場面ってなんやねん、と思うと思うので……。
僕自身は、動的に生成した文字の形をしたオブジェクトを同期するために用いました。
[PRも積もれば山となる](https://amanatsutouko.github.io/works-prtumo.html)というゲームです。WebGLで書き出しましたが、上手く同期できていることが確認できると思います。

https://youtu.be/XV71RyyMJdY


# 参考記事
[1][Photon Unity Networking Classic - FREE | Unity Asset Store](https://assetstore.unity.com/packages/tools/network/photon-unity-networking-classic-free-1786?locale=ja-JP)

https://assetstore.unity.com/packages/tools/network/photon-unity-networking-classic-free-1786?locale=ja-JP

[2][PhotonEngine Turorial 8 - プレーヤーのインスタンス化](https://doc.photonengine.com/pun/current/demos-and-tutorials/pun-basics-tutorial/player-instantiation)

https://doc.photonengine.com/pun/current/demos-and-tutorials/pun-basics-tutorial/player-instantiation

[3][🔄 プレイヤーとネットワークオブジェクト｜PUN2（Photon Unity Networking 2）で始めるオンラインゲーム開発入門](https://zenn.dev/o8que/books/bdcb9af27bdd7d/viewer/e3765a#%E3%82%A4%E3%83%B3%E3%82%B9%E3%82%BF%E3%83%B3%E3%82%B9%E3%81%AE%E7%94%9F%E6%88%90)

https://zenn.dev/o8que/books/bdcb9af27bdd7d/viewer/e3765a#%E3%82%A4%E3%83%B3%E3%82%B9%E3%82%BF%E3%83%B3%E3%82%B9%E3%81%AE%E7%94%9F%E6%88%90

[4][Instantiation 手動でインスタンス化 | PhotonEngine](https://doc.photonengine.com/ja-jp/pun/current/gameplay/instantiation)

https://doc.photonengine.com/ja-jp/pun/current/gameplay/instantiation

[5][Photon PUN2で動的にネットワークオブジェクトを生成する - Qiita](https://qiita.com/FumiyaHr/items/60dec657953ccaf7a667)

https://qiita.com/FumiyaHr/items/60dec657953ccaf7a667