【Unity(C#),PUN2】VR空間でアバター同士のリアルタイム通信実装

##PUN2(Photon Unity Networking2)とは？

簡単に言うと**マルチプレイ楽ちん実装ネットワークライブラリ**です。

##開発環境
PUN2(v2.16)
Unity(2018.2.3f1)
Unity Collaborate→[リンク](https://qiita.com/OKsaiyowa/items/4c314b33f583c9d1f798)

##準備
まずはアカウント開設等を行う必要があります。
基本的に下記リンクで全部解決です。

【参考リンク】：[PUN2で始めるオンラインゲーム開発入門【その１】](https://connect.unity.com/p/pun2deshi-meruonraingemukai-fa-ru-men-sono1)

開発環境のネットワークによっては`PhotonServerSettings`の設定が微妙に異なるようです。
私の試した際には`Protocol`が`Tcp`でないとダメでした。

![PunSS.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/244071/dffa8f02-1ff3-92e5-2223-66a4ea879c37.png)

`Use Name Server`にチェックを入れることでクラウドサーバーの利用が可能になるそうです。
もちろん、クラウドサーバーを利用する際には`Server`の欄は空欄でOKです。

なぜか繋がらない、、、というときはログの機能にチェックを入れて、
`PUN Logging`を`Full`にしておけば接続のステータスを確認できます。

##仕組み
仕組みについては下記リンクの図が最強にわかりやすいです。

【参考リンク】：[【PUN2】Unityでオンラインマルチプレイを爆速で実装する](https://qiita.com/UpAllNight/items/43e1b24301eb6029f18b#pun%E3%81%AE%E6%A6%82%E8%A6%81)

大まかな流れとしては

**①サーバーに接続
②ルームを作成
③ルームに参加
④アバター生成
⑤以後アバターを同期**

といった感じです。

#実装

##①サーバーに接続

最初にnamespaceを忘れずに書いときます。

```C#
using Photon.Pun;
using Photon.Realtime;
```

また、`MonoBehaviourPunCallbacks`も継承しておく必要があります。

あとはPUNが優秀すぎて1行で終了します。

```C#
    void Start()
    {
        PhotonNetwork.ConnectUsingSettings();     
    }
```

これでサーバーへの接続処理はおしまいです。

##②ルームを作成、③ルームに参加

`MonoBehaviourPunCallbacks`にコールバックが定義されているので
継承元のクラスに存在する`OnConnectedToMaster()`をオーバーライドして
ルームを作成します。

```C#
    //マスターサーバーへの接続が成功した時に呼ばれるコールバック
    public override void OnConnectedToMaster()
    {
        // "OnoTest"という名前のルームに参加する（ルームが無ければ作成してから参加する）
        PhotonNetwork.JoinOrCreateRoom("OnoTest", new RoomOptions(), TypedLobby.Default);
        print("ルーム作成完了");
    }
```
もし、ルームが既に存在していれば
自動でそのルームに入ってくれるのでルームに参加の処理もこれで完了してしまいます。
最強すぎますね。

##④アバター生成

公式ドキュメントにも記載がありますが、アバターは部屋に入った瞬間に生成します。

>In this callback, you could create player objects. For example in Unity, instantiate a prefab for the player.

【引用元】：[MonoBehaviourPunCallbacks Class Reference](https://doc-api.photonengine.com/en/pun/v2/class_photon_1_1_pun_1_1_mono_behaviour_pun_callbacks.html#add354fba0aebd1c970849bccfd708ff2)


```C#
    //部屋に入ったらアバター生成
    public override void OnJoinedRoom()
    {
        PhotonNetwork.Instantiate(生成したいPrefabの名前, 生成したい場所, 生成したい向き(角度));
    }
```

##マスターサーバー接続からアバター生成までのコード

```C#:適当なEmptyにアタッチ
using Photon.Pun;
using Photon.Realtime;
using UnityEngine;

public class MyPUN_Set : MonoBehaviourPunCallbacks
{
    [SerializeField]
    GameObject networkPlayer;

    [SerializeField]
    Transform cameraRig;

    [SerializeField]
    Transform[] playerPositions;

    GameObject player;

    void Start()
    {
        //PhotonServerSettingsに設定した内容を使ってマスターサーバーへ接続する
        PhotonNetwork.ConnectUsingSettings();     
    }

    //マスターサーバーへの接続が成功した時に呼ばれるコールバック
    public override void OnConnectedToMaster()
    {
        // "OnoTest"という名前のルームに参加する（ルームが無ければ作成してから参加する）
        PhotonNetwork.JoinOrCreateRoom("OnoTest", new RoomOptions(), TypedLobby.Default);
        print("ルーム作成完了");
    }

    //部屋に入ったらアバター生成
    public override void OnJoinedRoom()
    {
        int othersCount = PhotonNetwork.PlayerListOthers.Length;
        PhotonNetwork.Instantiate(networkPlayer.name, playerPositions[othersCount].position, Quaternion.identity);
        cameraRig.position = playerPositions[othersCount].position;
    }
}

```
`PhotonNetwork.PlayerListOthers`で人数を把握し、生成位置を変更しています。
これにより、同じ場所に別クライアントのアバターが生成されるのを防いでいます。
ただし、一度ロビーから出て、また入室した際には条件によっては同じ場所に出現してしまいます。
今回は入退室による生成位置被り対策の実装は省きました。

##⑤アバターを同期
今回は位置同期だけなので、同期したいオブジェクトが下記画像の状態になればOKです。
![PPNG.PNG](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/244071/3b4cc0e4-0cde-90af-7b1b-31518b2bfae7.png)




##アバターの構造は？
今回はVIVEで実装を試みました。
アバターとして使用したいオブジェクトは同期が必要なので、あらかじめPrefab化しておく必要があります。

PUNの実装方法として
**同期したいオブジェクトをPrefab化してPhotonフォルダ内のResources配下に置く & Instantiateで生成**
がお作法となっています。

ただ、私の試した限りでは、**SteamVRのCameraRigをそのままPrefab化してもダメ**でした。
(何かうまい方法知ってる方いたら教えてください。)

下記動画の手順のように、
**アバターとなるオブジェクトを別途用意し、プレーヤーの頭や手の動きに追従させる**必要がありました。
【参考リンク】：[How to write a multiplayer Oculus Rift game in Unity and Photon](https://www.youtube.com/watch?v=CraH51-_xJo)

ただ、単純なアバター(顔や手のモデルだけ)であればポジションを追従させる必要もなく、
親子関係にしてあげればいいだけなので動画の実装よりシンプルになります。

今回は顔と片手だけのアバターで実装しました。


##アバターの設定に関するコード

```C#
using UnityEngine;
using Photon.Pun;
/// <summary>
/// 同期させたいアバターのルートにアタッチ
/// </summary>
public class NetworkPlayer : MonoBehaviourPunCallbacks, IPunObservable
{
    [SerializeField]
    GameObject avater_Face, avater_RightHand;

    [SerializeField]
    string avater_LayerMask;

    Transform cameraTransform;
    Transform rightHandTransform;

    void IPunObservable.OnPhotonSerializeView(PhotonStream stream, PhotonMessageInfo info) { }

    void Start()
    {
        cameraTransform = Camera.main.GetComponent<Transform>();
        rightHandTransform = GameObject.Find("Controller (right)").GetComponent<Transform>();

        if (photonView.IsMine)
        {
            //親子関係を設定し、ローカルの座標系の原点に移動
            avater_Face.transform.parent = cameraTransform;
            avater_RightHand.transform.parent = rightHandTransform;

            avater_Face.transform.localPosition = Vector3.zero;
            avater_Face.transform.localRotation = Quaternion.identity;

            avater_RightHand.transform.localPosition = Vector3.zero;
            avater_RightHand.transform.localRotation = Quaternion.identity;

            //自分のアバターのみLayerを変更して視界に映らないようにする
            foreach (Transform child in  avater_Face.GetComponent<Transform>())
            {
                child.gameObject.layer = LayerMask.NameToLayer(avater_LayerMask);
            }         
        }
    }
}
```

###Start関数内で強引に取得
**PrefabはScene 上の GameObject を参照することはできない**ので
別途Prefabとして機能させたい(子にできない)CameraRig関連はStart関数内で取得しています。
それ以外のアバターとなるオブジェクトは独立しているので、`NetworkPlayer`の子にしてInspectorでアタッチしています。

![Pun.PNG](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/244071/6f9eacfc-dbea-cf4a-92fe-c403caa24cd3.png)


###IPunObservable
自前で同期させて行いたい処理がある場合は`IPunObservable`というインターフェースを実装します。
本来は`IPunObservable`の中にデータのやり取りを書きますが、
今回やり取りするデータはないので必要ありません。(使い方、解釈ミスってたら教えてください)


###photonView.IsMine
自分のクライアント側で生成されたオブジェクトかどうかを判定します。
これにより、相手側のアバターではなく自分のアバターに対して処理を行うことができます。

今回は、自分の顔のアバターが視界に入り込むのを防ぐために、
Layerを変更して、自分の顔のアバターのみカメラに映らない状態を作りだしています。
(CameraのCulling Maskも別途設定が必要)

##デモ
右手にレーザー銃を持ったドラゴンがマルチプレイで動くのを確認できました。
![D.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/244071/36be86ff-f31b-f13a-a321-b7123c2cf126.gif)


【アセット】：[Micro Dragon Fino - Faceted Style](https://assetstore.unity.com/packages/3d/characters/micro-dragon-fino-faceted-style-73536)
【アセット】：[Sci Fi Futuristic Hand Gun](https://assetstore.unity.com/packages/3d/props/guns/sci-fi-futuristic-hand-gun-90249)


##参考リンク

[3分間NetWorking](http://www5e.biglobe.ne.jp/aji/3min/)
[UDPはTCPと何が違うの？](https://qiita.com/tamago3keran/items/6864cb490e3c0bfc2b92)
[【PUN】アバター生成・同期](https://qiita.com/mczkzk/items/75324cc31709ec329d26)
[VRプログラミングの基礎を学ぶためのサンプルプログラム集](https://github.com/yumemi-inc/vr-studies/wiki/202-1-%E3%82%AA%E3%83%96%E3%82%B8%E3%82%A7%E3%82%AF%E3%83%88%E3%81%AE%E5%90%8C%E6%9C%9F%E6%96%B9%E6%B3%95)
[Unity Answers](https://answers.unity.com/questions/1307522/photon-pun-htc-vive-over-networkin.html)