Unityでオンラインマルチプレイなゲームを作りたい その14 リザルト画面

[前回の記事](https://qiita.com/koyuki2007/items/5b582cdd807bcaa9fb6b)で当たり判定を作り、プレイヤー、敵、弾を使って遊べるようにできるところまでを書きました。
今回は、ゲームを終えてプレイヤーのスコアを表示するリザルト画面を作りたいと思います。

# 今回の目標
![scene_diagram_2.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/469983/870f8807-1242-01ad-463b-dc8a51fcc221.png)
赤〇で囲っているリザルト部分を作ります。

## スコアの共有
今回の目玉部分なんですが、実は[その8 プレイヤーの同期](https://qiita.com/koyuki2007/items/e7d16e020539f38c6e7c)で下準備を行っています。
そのため、この下準備に沿ったスコアの共有方法を書いていこうと思います。

## プレイヤーオブジェクトの共有
各プレイヤーのインスタンスにアクセスし同期されているスコアの値を取るために、各ルームメンバーで各プレイヤーのインスタンスを保持する必要があります。
ゲームの進行を管理するようなクラスにDictionaryを持たせてプレイヤーインスタンスが作成されたときに追加してやるだけでいいです。

例)ゲームシーン中のゲームを管理するSceneGameクラス

```c#:SceneGame.cs
using UnityEngine;

/// <summary></summary>
public class PlayerList
{
    /// <summary></summary>
    public static Dictionary<int, GameObject> Players{ get; private set; } = new Dictionary<int, GameObject>();

    /****************************ゲームを管理する色んな処理*************************************/
}
```

SceneGame.cs内で作られたPlayersをインスタンス生成時にAddできるようにPlayer.csを改造します。

### [接続コールバック/OnMonobitInstantiate メソッド](http://www.monobitengine.com/doc/mun/contents/FeatureClient/CallbackFunction.htm#OnMonobitInstantiate%20%E3%83%A1%E3%82%BD%E3%83%83%E3%83%89)

```c#
public void OnMonobitInstantiate(MonobitEngine.MonobitMessageInfo info);
```
<code>MonobitNetwork.Instantiate()</code>が呼ばれ、ネットワーク越しにオブジェクトが作成された際に各オブジェクト毎に呼ばれます。
引数のinfoには作成元のクライアントのIDやplayerNameといった情報が格納されています。

これをPlayer.csに追加し、SceneGameクラスで作成したDictionaryにAddするようにします。

※変更点以外は省いています。

```c#:Player.cs
using UnityEngine;

using MonobitEngine;
public class Player : MonobitEngine.MonoBehaviour
{
    /// <summary>インスタンス所持者のID</summary>
    private int m_Id;

    /// <summary>インスタンス所持者の名前</summary>
    private string m_Name;

    /// <summary>MonobitNetwork.Instantiate()で作成された際に呼ばれる</summary>
    /// <param name="info">作成元の情報</param>
    private void OnMonobitInstantiate(MonobitEngine.MonobitMessageInfo info)
    {
        m_Id = info.sender.ID;

        m_Name = info.sender.name;

        SceneGame.Players.Add(ID, gameObject);
    }
}
```
<code>OnMonobitInstantiate</code>が呼ばれた際に、作成元のIDをキーにしてインスタンスのgameObjectをAddしています。
この方法でネットワーク越しで作成されたオブジェクトを保持できます。
ホストで作られたインスタンスをホスト以外でも参照しないといけなくなったとき(ホスト切り替わり時の処理の権利移行等)にも使える方法なので覚えておくと便利です。

これで各クライアントのスコアを参照できるようになったのでリザルト画面でPlayersを利用し、スコアを表示させてあげます。

※表示位置調整等は省いています。

```c#:SceneResult.cs
/// <summary>リザルト画面</summary>
public class Result : MonobitEngine.MonoBehaviour
{
    /// <summary>リザルト画面UI用のキャンバスのオブジェクト</summary>
    [SerializeField]
    GameObject m_CanvasObj;

    /// <summary>スコアを表示するためのてき</summary>
    private GameObject[] m_ScoreTextObjs;

    /// <summary></summary>
    private GameObject m_TotalScoreText;

    // Start is called before the first frame update
    void Start()
    {

    }

    // Update is called once per frame
    private void Update()
    {

    }

    /// <summary>スコアを表示する</summary>
    public void ShowResult()
    {
        int length = MonobitNetwork.playerList.Length;

        m_ScoreTextObjs = new GameObject[length];
        for (int i = 0; i < length; ++i)
        {
            m_ScoreTextObjs[i] = Instantiate(GUIPrefubs.TextPrefub) as GameObject;

            RectTransform transform = m_ScoreTextObjs[i].GetComponent<RectTransform>();
            transform.SetParent(m_CanvasObj.transform);
        }

        // SceneGameで管理しているPlayersをスコア順にソートして取得する
        var sortedPlayers = SceneGame.Players.OrderByDescending(p => (int)p.Value.GetComponent<Player>().Score);

        int count = 0;
        int prevCount = 0;
        int prevScore = 0;
        int totalScore = 0;
        int st = 1;
        foreach (var playerObj in sortedPlayers)
        {
            Player player = playerObj.Value.GetComponent<Player>();

            int score = playerObj.Value.GetComponent<Player>().Score;

            st = (score != prevScore && count != 0) ? st + 1 : st;

            m_ScoreTextObjs[count].GetComponent<Text>().text = st + "位..." + player.ID + "." + player.name + "：" + score + " 点";

            prevScore = score;

            prevCount = count;

            totalScore += score;

            ++count;
        }

        m_TotalScoreText.GetComponent<Text>().text = "トータル：" + totalScore + " 点";
    }

    /// <summary>表示したスコア表示を消す</summary>
    public void Clear()
    {
        int length = m_ScoreTextObjs.Length;

        for (int i = 0; i < length; ++i)
        {
            Destroy(m_ScoreTextObjs[i]);
        }
    }
}
```
<code>ShowResult()</code>を呼ぶことでリザルト画面が表示されるようにしています。
やってることも簡単で、保持しているプレイヤーのインスタンスからスコアを参照しているだけです。

仕上げにリザルト画面からゲームを終えてロビー画面まで戻る処理を加えます。

```c#:SceneResult.cs
/// <summary>リザルト画面</summary>
public class Result : MonobitEngine.MonoBehaviour
{
    /// <summary>リザルト画面UI用のキャンバスのオブジェクト</summary>
    [SerializeField]
    GameObject m_CanvasObj;

    /// <summary>スコアを表示するためのてき</summary>
    private GameObject[] m_ScoreTextObjs;

    /// <summary></summary>
    private GameObject m_TotalScoreText;

    // Start is called before the first frame update
    void Start()
    {

    }

    // Update is called once per frame
    private void Update()
    {

    }

    /// <summary>スコアを表示する</summary>
    public void ShowResult()
    {
        int length = MonobitNetwork.playerList.Length;

        m_ScoreTextObjs = new GameObject[length];
        for (int i = 0; i < length; ++i)
        {
            m_ScoreTextObjs[i] = Instantiate(GUIPrefubs.TextPrefub) as GameObject;

            RectTransform transform = m_ScoreTextObjs[i].GetComponent<RectTransform>();
            transform.SetParent(m_CanvasObj.transform);
        }

        // SceneGameで管理しているPlayersをスコア順にソートして取得する
        var sortedPlayers = SceneGame.Players.OrderByDescending(p => (int)p.Value.GetComponent<Player>().Score);

        int count = 0;
        int prevCount = 0;
        int prevScore = 0;
        int totalScore = 0;
        int st = 1;
        foreach (var playerObj in sortedPlayers)
        {
            Player player = playerObj.Value.GetComponent<Player>();

            int score = playerObj.Value.GetComponent<Player>().Score;

            st = (score != prevScore && count != 0) ? st + 1 : st;

            m_ScoreTextObjs[count].GetComponent<Text>().text = st + "位..." + player.ID + "." + player.name + "：" + score + " 点";

            prevScore = score;

            prevCount = count;

            totalScore += score;

            ++count;
        }

        m_TotalScoreText.GetComponent<Text>().text = "トータル：" + totalScore + " 点";
    }

    /// <summary>表示したスコア表示を消す</summary>
    public void Clear()
    {
        int length = m_ScoreTextObjs.Length;

        for (int i = 0; i < length; ++i)
        {
            Destroy(m_ScoreTextObjs[i]);
        }
    }

    /// <summary>ルームを退室した際に呼ばれるコールバック</summary>
    public void OnLeftRoom()
    {
        SceneManager.LoadScene("Lobby");
    }

    /// <summary>戻るボタンを押した際に呼ばれる</summary>
    public void OnClickBack()
    {
        MonobitNetwork.LeaveRoom();
    }
}
```
戻るボタンを作成し、OnClickBackをAddListenerしてあげてください。

これでリザルト画面を作成できました。

これで恐らく一通り実装しようと思ってたことを実装できるようになったかなと思います。
次回、今まで使ってきた機能と使い方を纏めて〆ようと思います。