Photon Unity Networkingで動的に同期オブジェクトを生成する方法

# [Photon Unity Networking(PUN)](https://www.photonengine.com/ja/PUN)とは
Unity でマルチプレイが簡単にできるようになるアセット。
オブジェクトを動的に生成するためにはあらかじめプレハブ化しておく方法と、ViewID などを手動で与えてあげてごにょごにょする方法がありますが、後者の情報がどこにもなかったのでメモ程度に残しておきます。

# 動作環境
- __Unity2017.4.7f1__
- __[Photon Unity Networking Free v1.91](https://assetstore.unity.com/packages/tools/network/photon-unity-networking-free-1786)__

それぞれダウンロードしてインポートしておいてください。
またPUNのセットアップも完了させておいてください。[コチラ](http://tech.pjin.jp/blog/2016/07/20/unity_pun_1/)のサイト様などが参考になるかと思います。

# Cube を動的に生成してみる
準備ができたら早速スクリプトを書いていきたいと思います。
今回は Cube をスクリプトから生成して、ViewID その他もろもろを与えて同期オブジェクトとして存在させたいと思います。
適当なオブジェクトに以下のスクリプトと PhotonView をアタッチして実行してみてください。

```CubeCreator.cs
using System.Collections;
using System.Collections.Generic;
using UnityEngine;

[RequireComponent(typeof(PhotonView))]
public class CubeCreator : MonoBehaviour
{
    [SerializeField]
    Material CubeMaterial;

    int Timer = 0;
    bool isJoinedRoom = false;
    PhotonView PhotonView;

    void Start()
    {
        //RPC のために PhotonView 取得
        PhotonView = GetComponent<PhotonView>();

        //Photon に接続
        PhotonNetwork.ConnectUsingSettings("CubeCreator");
    }

    // ロビーに入室すると呼ばれるイベント
    void OnJoinedLobby()
    {
        Debug.Log("ロビーに入室成功");

        //ルームに入室するか作成する
        PhotonNetwork.JoinOrCreateRoom("CubeCreator", null, null);
    }

    //ルームに入室すると呼ばれる
    void OnJoinedRoom()
    {
        Debug.Log("ルームに入室成功");
        isJoinedRoom = true;
    }

    void Update()
    {
        Timer++;
        //100フレーム経過していない、又はルームに入室していないならスキップ
        if (Timer < 100 || !isJoinedRoom)
            return;

        //Cube を作成する関数を全クライアントで実行
        PhotonView.RPC("CreateCube", PhotonTargets.AllBuffered, PhotonNetwork.AllocateViewID());
        Timer = 0;
    }

    [PunRPC]
    void CreateCube(int viewID)
    {
        //Cube を生成
        var _cube = GameObject.CreatePrimitive(PrimitiveType.Cube);

        //わかりやすいように上空に移動
        _cube.transform.position = new Vector3(0, 10, 0);

        //マテリアルを設定
        _cube.GetComponent<Renderer>().material = CubeMaterial;

        //Rigidbody を追加
        var _rigidbody = _cube.AddComponent<Rigidbody>();

        //PUN を追加
        var _photonView = _cube.gameObject.AddComponent<PhotonView>();
        var _photonTransformView = _cube.gameObject.AddComponent<PhotonTransformView>();
        var _photonRigidbodyView = _cube.gameObject.AddComponent<PhotonRigidbodyView>();

        //PhotonView の ObservedComponents リストを初期化
        _photonView.ObservedComponents = new List<Component>();

        //PhotonView に ViewID を設定
        _photonView.viewID = viewID;

        //到達保証の設定
        //詳しくは https://support.photonengine.jp/hc/ja/articles/224763767-PUN%E3%81%A7%E5%88%B0%E9%81%94%E4%BF%9D%E8%A8%BC%E3%81%AE%E8%A8%AD%E5%AE%9A%E3%82%92%E8%A1%8C%E3%81%86
        _photonView.synchronization = ViewSynchronization.ReliableDeltaCompressed;

        //PhotonTransformView の設定
        //位置の同期を有効にする
        _photonTransformView.m_PositionModel.SynchronizeEnabled = true;

        //回転の同期を有効にする
        _photonTransformView.m_RotationModel.SynchronizeEnabled = true;

        //リストに追加して同期対象に加える
        _photonView.ObservedComponents.Add(_photonTransformView);
        _photonView.ObservedComponents.Add(_photonRigidbodyView);

        Debug.Log("Create!");
    }
}

```
そうすると以下のような結果になるかと思います。
<blockquote class="twitter-tweet" data-lang="ja"><p lang="ja" dir="ltr">PhotonでCubeを動的に生成して同期するテスト① <a href="https://t.co/lakhSPHttw">pic.twitter.com/lakhSPHttw</a></p>&mdash; karukaru@C94三日目O-44b (@_karukaru_) <a href="https://twitter.com/_karukaru_/status/1017840236280217600?ref_src=twsrc%5Etfw">2018年7月13日</a></blockquote>
<script async src="https://platform.twitter.com/widgets.js" charset="utf-8"></script>

1人だけで実行しているのでわかりづらいですが、ビルドして同時に実行すれば生成スピードが倍になってるのがわかるかと思います。
<blockquote class="twitter-tweet" data-lang="ja"><p lang="ja" dir="ltr">PhotonでCubeを動的に生成して同期するテスト②<br>最初の1個がめっちゃ同期ズレしてるけど気にしないで <a href="https://t.co/ZT1izdnGk4">pic.twitter.com/ZT1izdnGk4</a></p>&mdash; karukaru@C94三日目O-44b (@_karukaru_) <a href="https://twitter.com/_karukaru_/status/1017850508864655360?ref_src=twsrc%5Etfw">2018年7月13日</a></blockquote>
<script async src="https://platform.twitter.com/widgets.js" charset="utf-8"></script>

# 解説
先ほどのスクリプトを（PUNの基本的な部分を覗いて）解説します。

## Cube を作成する関数を全クライアントで実行
```
        //Cube を作成する関数を全クライアントで実行
        PhotonView.RPC("CreateCube", PhotonTargets.AllBuffered, PhotonNetwork.AllocateViewID());
```
Cube を生成する関数を全クライアントで実行します。この時 ``PhotonNetwork.AllocateViewID()`` を引数として渡すことで、全クライアントに対して同じ ViewID が通知されます。
PhotonNetwork.AllocateViewID は新しい ViewID を生成して返してくる関数です。詳しいことは __[PUNのドキュメント](https://doc-api.photonengine.com/ja-jp/pun/current/class_photon_network.html#a2eefb7151b27169d11062be9996a0eab)__ を読んでください。

## PUN を追加
```
        //PUN を追加
        var _photonView = _cube.gameObject.AddComponent<PhotonView>();
        var _photonTransformView = _cube.gameObject.AddComponent<PhotonTransformView>();
        var _photonRigidbodyView = _cube.gameObject.AddComponent<PhotonRigidbodyView>();
```
同期に必要なスクリプトをアタッチして __保持します。__
後々リストに追加する必要があるので、追加したタイミングで保持しておくのが良いでしょう。

## PhotonView の ObservedComponents リストを初期化
```
        //PhotonView の ObservedComponents リストを初期化
        _photonView.ObservedComponents = new List<Component>();
```
PhotonView の ObservedComponents リストは同期対象のスクリプト（PhotonTransformView など）を入れるものですが、これは __スクリプトをアタッチした段階では初期化されておらず、自分でリストを生成してあげる必要__ があります。
これに気づくまでだいぶ時間を浪費しました。

## PhotonView に ViewID を設定
```
        //PhotonView に ViewID を設定
        _photonView.viewID = viewID;
```
ここで __引数として貰った ViewID を PhotonView に設定__ します。これを行うことで全てのクライアントでこの Cube の ViewID が同一のものとなり、PUN上で同一オブジェクトとして認識されます。

## PhotonTransformView の設定
```
        //PhotonTransformView の設定
        //位置の同期を有効にする
        _photonTransformView.m_PositionModel.SynchronizeEnabled = true;

        //回転の同期を有効にする
        _photonTransformView.m_RotationModel.SynchronizeEnabled = true;
```
__PhotonTransformView の位置と回転の同期を有効__ にします。これを行わないと Cube が生成されても同期してくれません。

## リストに追加して同期対象に加える
```
        //リストに追加して同期対象に加える
        _photonView.ObservedComponents.Add(_photonTransformView);
        _photonView.ObservedComponents.Add(_photonRigidbodyView);
```
最後に __PhotonView の ObservedComponents リストへ同期対象のスクリプトを追加__ します。

以上で重要なポイントの解説は終わりです。

# 最後に
PUNなんもわからん、どうやったら動的に生成できるんだと泣いている方の助けになれば幸いです。
これを応用すればどんなオブジェクトでも動的に生成して同期することができるようになるので、より自由度が高くなるかと思います。

# おまけ
__[PUNは同期オブジェクトが1000個まで](https://doc-api.photonengine.com/ja-jp/pun/current/general.html)__ と決まっていまして、先ほどのスクリプトを動かし続けるとそのうちエラーを吐きます。
> 制限事項
> Viewとプレイヤー
> 
> パフォーマンス上の理由で、PhotonNetwork APIがサポートするPhotonViewはプレイヤーあたり1000個まで、プレイヤー数は2,147,483人までです（これはハードウェアのサポート限界よりずっと多いのです！）。

_[Photon Unity Networking: 基本説明](https://doc-api.photonengine.com/ja-jp/pun/current/general.html) より引用。_

これを回避するためには、__同期する必要のなくなったオブジェクトをPUNから削除__ してあげる必要があります。
例として、生成した Cube に500フレーム経過したら自身を削除するスクリプトを追加します。

```CubeDestroyer.cs
using System.Collections;
using System.Collections.Generic;
using UnityEngine;

[RequireComponent(typeof(PhotonView))]
public class CubeDestroyer : MonoBehaviour
{
    int Timer = 0;

    void Update()
    {
        Timer++;
        //500フレーム経過するまでスキップ
        if (Timer < 500)
            return;

        //ViewID を PhotonView から取得
        int _viewID = GetComponent<PhotonView>().viewID;

        //PUN から ViewID を削除
        PhotonNetwork.UnAllocateViewID(_viewID);

        Debug.Log("Destroy!");

        //オブジェクトを削除
        Destroy(this.gameObject);
    }
}

```

ここで重要なのは ``PhotonNetwork.UnAllocateViewID(int ViewID)`` で、これは引数として渡した ViewID をPUNから削除します。
詳しいことは __[PUNのドキュメント](https://doc-api.photonengine.com/ja-jp/pun/current/class_photon_network.html#ae5c32625fb17b425e105de49ea267ddf)__ を読んでください。

これを実行すると以下のようになります。
<blockquote class="twitter-tweet" data-lang="ja"><p lang="ja" dir="ltr">PhotonでCubeを動的に生成して同期するテスト③<br>動的に生成した同期オブジェクトを削除してあげる <a href="https://t.co/57xL31Lyl9">pic.twitter.com/57xL31Lyl9</a></p>&mdash; karukaru@C94三日目O-44b (@_karukaru_) <a href="https://twitter.com/_karukaru_/status/1017868466810834944?ref_src=twsrc%5Etfw">2018年7月13日</a></blockquote>
<script async src="https://platform.twitter.com/widgets.js" charset="utf-8"></script>

使わなくなったオブジェクトは削除しないとネットワークを圧迫してしまうので、ちゃんと削除してあげましょう。