【Unity(C#),PUN2】ルームの入退室に対応したプレイヤー生成位置、色の振り分け

##やりたかったこと
まず前提として、**ネットワーク経由で同期し、
ルームに好きなタイミングで出入りできるアプリケーション**の
作成を行っていました。

その中で、**プレーヤーごとの生成位置及び色をどうきめるか**
という問題に直面しました。

**位置も色も被ることなくプレイヤーに反映したい！**　
というのが今回の記事の本筋です。

##試行錯誤
最初に思いついたのは
ルームの人数に応じたプレイヤーの生成位置、色の振り分けです。

ルーム入室時の人数が
0人なら1番目の位置、色
1人なら2番目の位置、色
2人なら3番目の位置、色
3人なら4番目の位置、色 といった具合です。

ただし、このやり方だと再入室時に位置被り、色被りが発生してしまいます。

---
###`既に部屋に3人いる場合`
![部屋には3人.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/244071/e2dea8ca-7bbc-cfa1-ca5d-ed84a0cf9ade.png)

---
###`4人目の場所と色を割り当てる`

![4人目の場所と色.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/244071/1aa257cb-effc-7d09-d72c-04b935979f66.png)

---
###`3人目のプレイヤーが退出`

![抜けます.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/244071/d71da092-4b79-be38-078b-3801666e7d37.png)

---
###`再入室(もしくは別プレイヤーの入室)`

![再入室部屋には3人.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/244071/fb5328a3-df17-94c2-ec68-d90aaa81d1eb.png)

---
###`部屋の人数は4人なので既に存在する4番目の位置、色に割り当てられたプレイヤーと被る`

![4人目の場所と色？.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/244071/68a527c7-a555-f177-feb2-b6193403273b.png)

そこで別のやり方を考えました。

##プレイヤーごとに番号を割り当てる

入室時にプレイヤーごとに番号を割り当てるという方法を思いつきました。
まずはルーム内で使われていない番号をチェックします。

使われていない番号が
1なら番号1をプレイヤーに割り当て
2なら番号2をプレイヤーに割り当て
3なら番号3をプレイヤーに割り当て
4なら番号4をプレイヤーに割り当て といった具合です。

あとは各々のクライアントが各自のローカルで生成された
同期オブジェクトの所有権を持つプレイヤーを取得し、
番号をチェックして処理を行います。

![プレイヤー番号振り.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/244071/6a6564df-5eee-57e3-e367-536d25cb9658.png)

番号ごとに各々のクライアントが自分のローカル環境で処理を行うので、
位置情報や色を送る必要がなくなります。

つまり、この方法を使えば**通信に必要な情報は
プレイヤーごとに割り振られた番号のみ**で済みます。


##カスタムプロパティ
入室時にプレイヤーごとに番号を割り当てるという実装を行う上で便利そうな
**カスタムプロパティというPUN2の機能**に関する記事を発見しました。

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

今回はカスタムプロパティを利用して、
ルームの入退室に対応したプレイヤー生成位置、色の振り分けを
実装していきます。


**カスタムプロパティはプレイヤー単位で設定できて、
必要な時に通信して呼び出す**形で利用できるので
今回の目的との親和性は高いと思います。

##拡張メソッド

**カスタムプロパティを使う上で拡張メソッドは必須**かなと個人的には思ってます。

理由としては、カスタムプロパティの定義には文字列のキーが必要だからです。

とある文字列Xに対して、値を紐づけていくようなイメージです。

コードに落とし込むとこんな感じになります。

```C#
using Photon.Realtime;
using UnityEngine;
using Hashtable = ExitGames.Client.Photon.Hashtable;

public static class PlayerPropertyExtensions
{
    private const string PLAYER_ASSIGN_NUMBER = "n"; 
    
    private static Hashtable _hashtable = new Hashtable();

    //＝＝＝＝＝＝＝＝＝＝＝＝＝＝＝＝＝＝＝＝＝＝＝＝＝＝＝＝＝＝＝＝＝＝＝＝＝＝＝＝＝
    //　プレイヤーの番号
    //＝＝＝＝＝＝＝＝＝＝＝＝＝＝＝＝＝＝＝＝＝＝＝＝＝＝＝＝＝＝＝＝＝＝＝＝＝＝＝＝＝
    
    //Hashtableにプレイヤーに割り振られた番号があれば取得する
    private static bool TryAndGetPlayerNum(this Hashtable hashtable, out int playerAssignNumber)
    {
        if (hashtable[PLAYER_ASSIGN_NUMBER] is int value)
        {
            playerAssignNumber = value;
            return true;
        }

        playerAssignNumber = 0;
        return false;
    }

    //プレイヤー番号を取得する
    public static int GetPlayerNum(this Player player)
    {
        player.CustomProperties.TryAndGetPlayerNum(out int playerNum);
        return playerNum;
    }

    //プレイヤーの割り当て番号のカスタムプロパティを更新する
    public static void UpdatePlayerNum(this Player player, int assignNum)
    {
        _hashtable[PLAYER_ASSIGN_NUMBER] = assignNum;
        player.SetCustomProperties(_hashtable);
        _hashtable.Clear();
    }
}
```

任意の文字列キーのカスタムプロパティを取得、更新する拡張メソッドを用意することで、
**利用側は文字列キーの誤りを気にする必要性がなくなります。**

##使用済みの番号を特定し、未使用の番号でプレイヤーの番号を更新

拡張メソッドで利用側はかなり楽になったので、
プレイヤーごとの番号割り当ての実装に移ります。

順番としては見出し通り下記です。
**①使用済みの番号を特定
②未使用の番号でプレイヤーの番号を更新**

コードは以下です。

```C#
private const int _PLAYER_UPPER_LIMIT = 4;

/// <summary>
/// プレイヤーに番号を与える
/// </summary>
private void SetMyCustomProperties()
{ 
    //自分のクライアントの同期オブジェクトにのみ
    if (photonView.IsMine)
    {
        List<int> playerSetableCountList = new List<int>();
        
        //制限人数までの数字のリストを作成
        //例) 制限人数 = 4 の場合、{0,1,2,3}
        int count = 0;
        for (int i = 0; i < _PLAYER_UPPER_LIMIT; i++)
        {
            playerSetableCountList.Add(count);
            count++;
        }

        //他の全プレイヤー取得
        Player[] otherPlayers = PhotonNetwork.PlayerListOthers;

        //他のプレイヤーがいなければカスタムプロパティの値を"0"に設定
        if (otherPlayers.Length <= 0)
        {
            //ローカルのプレイヤーのカスタムプロパティを設定
            int playerAssignNum = otherPlayers.Length;
            PhotonNetwork.LocalPlayer.UpdatePlayerNum(playerAssignNum);
            return;
        }

        //他のプレイヤーのカスタムプロパティー取得してリスト作成
        List<int> playerAssignNums = new List<int>();
        for (int i = 0; i < otherPlayers.Length; i++)
        {
            playerAssignNums.Add(otherPlayers[i].GetPlayerNum());
        }

        //リスト同士を比較し、未使用の数字のリストを作成
        //例) 0,1にプレーヤーが存在する場合、返すリストは2,3
        playerSetableCountList.RemoveAll(playerAssignNums.Contains);

        //ローカルのプレイヤーのカスタムプロパティを設定
        //空いている場所のうち、一番若い数字の箇所を利用
        PhotonNetwork.LocalPlayer.UpdatePlayerNum(playerSetableCountList[0]);
    }
} 
```

コメントの通りの処理を行っています。


##番号に応じた生成位置と色を割り当て
未使用の番号でプレイヤーの番号を更新　することができたので、
割り当てられた番号を利用した処理を追加していきます。

```C#:PhotonViewがアタッチされたアバターオブジェクトにアタッチ
using System.Collections.Generic;
using ExitGames.Client.Photon;
using Photon.Pun;
using Photon.Realtime;
using UnityEngine;

/// <summary>
/// プレーヤー生成時に行う初期設定処理
/// </summary>
public class PlayerInitializeSetting : MonoBehaviourPunCallbacks
{
    [SerializeField] private MeshRenderer _avatarObjectMeshRenderer;
    [SerializeField] private Material[] _playerMaterials;
    [SerializeField] private Transfrom[] _playerInitTransform;

    private void Start()
    {
        //プレーヤーのカスタムプロパティ更新
        SetMyCustomProperties();
    }
     
    /// <summary>
    /// カスタムプロパティ更新時のコールバック
    /// </summary>
    /// <param name="target">更新されたカスタムプロパティを持つプレーヤー</param>
    /// <param name="changedProps">更新されたカスタムプロパティ</param>
    public override void OnPlayerPropertiesUpdate(Player target, Hashtable changedProps)
    {
        //自分のクライアントの同期オブジェクトの設定
        if (photonView.IsMine)
        {
            this.gameObject.transform.rotation = _playerInitTransform[PhotonNetwork.LocalPlayer.GetPlayerNum()].rotation;
            this.gameObject.transform.position = _playerInitTransform[PhotonNetwork.LocalPlayer.GetPlayerNum()].position;
            _avatarObjectMeshRenderer.sharedMaterial = _playerMaterials[PhotonNetwork.LocalPlayer.GetPlayerNum()];
        }
        //他のクライアントの同期オブジェクトの設定
        else
        {
            this.gameObject.transform.rotation = _playerInitTransform[photonView.Owner.GetPlayerNum()].rotation;
            this.gameObject.transform.position = _playerInitTransform[photonView.Owner.GetPlayerNum()].position;
            _avatarObjectMeshRenderer.sharedMaterial = _playerMaterials[photonView.Owner.GetPlayerNum()];
        }
    }
    }
}
```

`OnPlayerPropertiesUpdate`という
**通信完了後、カスタムプロパティが更新し終えたことを
通知するコールバック**　~~内でフラグを立てています。~~

~~このフラグを利用し、Start関数内でasync/awaitして完了待ち~~を実装しています。


##デモ


<img width="723" alt="手話VRSS2.PNG" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/244071/3dda01d4-f9bd-42aa-9931-b9e5f7757ad5.png">

実際に今回メモした実装方法でプレイヤーの生成位置、色の設定を
入退室に対応させたアプリのデモムービーです。

【参考リンク】：[Sign Language in VR](https://www.youtube.com/watch?v=xWc91UAA3EI&feature=youtu.be)

オンライン上でVR空間内での手話トレーニングを実施し、
動画やビデオ通話以上のトレーニング効果を期待したVRアプリです。

##最後に

カスタムプロパティで初期設定を行う方法を使用しましたが、
完全に同じタイミングでカスタムプロパティを
更新した場合のエラーハンドリングなどが未実装です。

私が把握している問題は現状これだけですが、
まだまだ対策しないといけないことが多そうです。

引き続き深みにはまっていこうと思います。。。

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