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


PUN2(Photon Unity Networking2)とは?

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

開発環境

PUN2(v2.16)
Unity(2018.2.3f1)
Unity Collaborate→リンク

準備

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

【参考リンク】:PUN2で始めるオンラインゲーム開発入門【その1】

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

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

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

仕組み

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

【参考リンク】:【PUN2】Unityでオンラインマルチプレイを爆速で実装する

大まかな流れとしては

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

といった感じです。

実装

①サーバーに接続

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

using Photon.Pun;
using Photon.Realtime;

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

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

    void Start()
    {
        PhotonNetwork.ConnectUsingSettings();     
    }

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

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

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

    //マスターサーバーへの接続が成功した時に呼ばれるコールバック
    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

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

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

適当な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です。

アバターの構造は?

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

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

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

下記動画の手順のように、
アバターとなるオブジェクトを別途用意し、プレーヤーの頭や手の動きに追従させる必要がありました。
【参考リンク】:How to write a multiplayer Oculus Rift game in Unity and Photon

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

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

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

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でアタッチしています。

IPunObservable

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

photonView.IsMine

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

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

デモ

右手にレーザー銃を持ったドラゴンがマルチプレイで動くのを確認できました。

【アセット】:Micro Dragon Fino - Faceted Style
【アセット】:Sci Fi Futuristic Hand Gun

参考リンク

3分間NetWorking
UDPはTCPと何が違うの?
【PUN】アバター生成・同期
VRプログラミングの基礎を学ぶためのサンプルプログラム集
Unity Answers