【Unity】.io系ゲーム等によくみられるスコア表をPUN2で実装

# はじめに
Unityで気軽に遊べるシンプルなオンラインゲームを作ろうとしているときに、Agar.ioやSlither.ioのような.io系ゲームによくあるスコア表を実装してみたいと思い、PUN2を使って作ってみました。

※Unityのバージョンは2019.4.7f1を使用しています。
# 1. スコアを表示するテキストを追加
はじめにスコアを表示するテキストをCanvas上に追加しておきます。今回は上位5名のスコアを表示させたいので、1st から5thまでの5つのテキストを追加します。一つのテキストでまとめて表示することもできそうですが、それぞれの順位でテキストの設定をしやすくしたかったので5つに分けました。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1204321/c37c3584-28b9-8a77-05f8-c6141f6db259.png)
このように各テキストを順番に配置していきます。後ろのパネルやTOP5のテキストを追加するとそれっぽくなりました。
# 2. 各プレイヤーのスコアを同期
次に、各プレイヤーのスコアの同期を行います。今回はPUN2の**カスタムプロパティ**という同期方法を使ってみます。カスタムプロパティに関しては私も理解が不十分なところがありますが、大まかにはPhotonに接続している各プレイヤーや接続先のルームに対して、ネットワーク上の変数を設定できる機能のようなものです。値の設定にはハッシュテーブルを用います。以下のスクリプトは、プレイヤーが操作するオブジェクトにアタッチしたスクリプトの中身のうち、スコアの同期に関係する処理を抜粋したものです。

```C#
using ExitGames.Client.Photon;
using UnityEngine;
using UnityEngine.SceneManagement;
using Photon.Pun;

public class SphereController : MonoBehaviour
{
    Hashtable playerHash = new Hashtable();

    void Start()
    {
        playerHash.Add("score", 0);
        PhotonNetwork.LocalPlayer.SetCustomProperties(playerHash);
    }
    void Update()
    {
        if (transform.position.y <= -20)
        {
            if (playerFlag == true && lastCollision != null)
            { 
               playerHash["score"] = (int)lastCollision.GetComponent<PhotonView>().Owner.CustomProperties["score"] + 1;
               lastCollision.GetComponent<PhotonView>().Owner.SetCustomProperties(playerHash);
            }
            PhotonNetwork.LeaveRoom();
            SceneManager.LoadScene("Launcher");
        }
    }
}
```
まず`Hashtable playerHash = new Hashtable();`でExitGames.Client.Photon内のHashtableクラスのインスタンスを生成します。このとき、System.Collections内のHashtableクラスとの混同を避けるため、`using ExitGames.Client.Photon;`を追加し、`using System.Collections;`を削除しています。次にStart関数内で自身のスコアの初期値をカスタムプロパティに設定します。ハッシュテーブルには、文字列のキー("score")と値(0)のペアを追加しています。続いてUpdate関数内では、自身の操作するオブジェクトが落下したときに、最後に衝突した相手(lastCollision)のカスタムプロパティのスコアを1だけ増やし、自身は部屋を退出してタイトル画面に戻るという処理を書いています。
# 3. 各プレイヤーのスコアをソートし、スコア表を表示
各プレイヤーのカスタムプロパティに設定されているスコアを降順にソートし、テキストに表示させます。以下のスクリプトは、Canvasにアタッチしたスクリプトの中身のうち、一部を抜粋したものです。

```C#
using UnityEngine;
using Photon.Pun;
using Photon.Realtime;
using UnityEngine.UI;
using System;

public class Ranking : MonoBehaviour
{
    public Text[] scoreText;
    Player[] players;
    float elapsedTime;

    void Start()
    {
        elapsedTime = 0f;
    }
    void Update()
    {
        elapsedTime += Time.deltaTime;
        if (elapsedTime > 0.1f)
        {
            players = PhotonNetwork.PlayerList;
            Array.Sort(players, (a, b) => GetScore(b) - GetScore(a));
            elapsedTime = 0f;
            if (players.Length >= 5)
            {
                for(int i = 0; i < 5; i++)
                {
                    scoreText[i].text = (i + 1).ToString() + ". " + players[i].NickName.ToString() + "( " + GetScore(players[i]).ToString()+")";
                    TextColor(i);
                    
                    
                }
            }
            else
            {
                for (int i = 0; i < players.Length; i++)
                {
                    scoreText[i].text = (i + 1).ToString() + ". " + players[i].NickName.ToString() + "(" + GetScore(players[i]).ToString()+")";
                    TextColor(i);
                    

                }
                for(int i = players.Length; i < 5; i++)
                {
                    scoreText[i].text = "";
                    
                }
            }
        }
    }

    int GetScore(Player player)
    {
        return (int)player.CustomProperties["score"];
    }
    void TextColor(int i)
    {
        if (PhotonNetwork.NickName == players[i].NickName)
        {
            scoreText[i].color = new Color(180f/255f, 60f/255f, 60f/255f);
        }
        else
        {
            scoreText[i].color = new Color(75f/255f, 75f/255f, 75f/255f);
        }
    }

}
```
まず、`public Text[] scoreText;`でスコア表のテキストの配列を宣言し、`Player[] players;`でPhoton.Realtime内のプレイヤークラスの配列を宣言します。elapsedTimeは何秒ごとにスコア表を更新するかを表し、0.1秒に設定しています。Update関数内では、プレイヤークラスの配列に現在の接続プレイヤーの配列を代入し、`Array.Sort(players, (a, b) => GetScore(b) - GetScore(a));`でプレイヤークラスの配列をカスタムプロパティのスコアの降順にソートします。以降のif文とfor文の処理では、接続プレイヤー数が5人以上のときはスコア表のテキストに上位5名のスコアを表示させます。接続プレイヤー数が5人未満のときは、その数だけスコア表のテキストにスコアを表示させ、プレイヤーが存在しない順位には何も表示させないという処理になります。TextColor関数では、プレイヤークラスの配列の要素のプレイヤー名と自身のプレイヤー名が一致していれば、その順位の色を赤めの色に変え、それ以外のときは黒めの色に変えるという処理を行っています。つまり、自分がランキングに載ったときに色を変えてわかりやすくするというものです。
# 4. 実際に動かす
プレイヤーの入退室やプレイヤーのスコア変動がスコア表に反映されています。
![ezgif-7-3e974e1223b4.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1204321/4bb09cc4-cb7c-3a2f-c957-57cf9282eb83.gif)
# 最後に
何とかそれらしいスコア表を実装できましたが、C#やPUN2の理解が不十分な中で作ったので冗長な部分やミスがあるかと思います。コメント等でご指摘いただけると幸いです。
# 参考サイト
[PUN2（Photon Unity Networking 2）で始めるオンラインゲーム開発入門](https://zenn.dev/o8que/books/bdcb9af27bdd7d)