Monobit Unity Networkingを使ってスコアマネージャーを作る【モノビットエンジン入門】

#はじめに
以前にMUNを使ったスコアの同期についてちょろっと紹介しました。
今回はその内容を発展させ、マネージャクラスまで作ろうというお話です。

注意点として、ここからはサーバ接続/ルーム作成/ルーム入退室について理解している前提で進めます。
初心者向けの記事なので大丈夫だと思いますが、心配な方は前回の記事をサッと読んでおくと内容が入りやすいと思います。
「[Monobit Unity Networkingを使ってネットワークマネージャクラスを作る【モノビットエンジン入門】](https://qiita.com/nisoka/items/c86a941d6e2c1e2b1e0b)」

また、MUNをまだ触ったことがない方は、まずはこちらの記事をご覧ください。
「[モノビットエンジンでかんたんサーバ接続](https://qiita.com/nisoka/items/a7dfe4fad2e0b201ecaa)」

環境は以下の通りです
*win10 64bit, Unity2018.3.3f1, Monobit Unity Networking2.0 v2.6*

#作るもの
スコア管理クラスを作ります。
機能は**「同期」「変更」「ソート」**だけのシンプルなものしかありません。
おおまかな処理の流れとしては、それぞれのクライアントがスコアの変更を要求すると、
ホストがその通知を受け取り、変更の処理が正常に行えればゲスト全員に配る感じです。

#実装
###ScoreManager
```C#:
using System.Collections.Generic;
using UnityEngine;
using MonobitEngine;

/** 得点データ構造体 */
public class ScoreData {
    public ScoreData(int id) {
        player_id = id; // MonobitNetwork.player.ID
        score = 0;      // 得点
    }
    public int player_id;
    public int score;
};

public class ScoreManager : MonobitEngine.MonoBehaviour
{
    /** 全員の得点リスト */
    List<ScoreData> m_ScoreList = new List<ScoreData>();
    
    /// <summary>
    /// 自分の得点データを取得する
    /// </summary>
    public ScoreData GetScoreData() {
        return SearchData(MonobitNetwork.player.ID);
    }

    /// <summary>
    /// 全員の得点データを取得する
    /// </summary>
    public List<ScoreData> GetAllScoreData() {
        return m_ScoreList;
    }

    /// <summary>
    /// 初期化処理
    /// </summary>
    public void Init() {
        for (int i = 0; i < MonobitNetwork.room.playerCount; i++) {
            m_ScoreList.Add(new ScoreData(MonobitNetwork.playerList[i].ID));
        }
        // 得点をリセット
        Reset();
    }

    /// <summary>
    /// 得点を変更する通知を送信
    /// </summar
    public void Add(int value) {
        monobitView.RPC("RecvAddScore", MonobitTargets.Host, value, MonobitNetwork.player.ID);
    }

    /// <summary>
    /// 得点加算の通知を受信
    /// </summary>
    [MunRPC]
    void RecvAddScore(int value, int playerID) {
        // 加算する
        int temp = SearchData(playerID).score;
        temp += value;

        // 無効な値かチェック
        if (temp >= 0) {
            // 全クライアントに得点同期通知
            monobitView.RPC("RecvSyncScore", MonobitTargets.All, temp, playerID);
        }
    }

    /// <summary>
    /// 得点を反映する通知を受信する(全クライアントが処理)
    /// </summary>
    [MunRPC]
    void RecvSyncScore(int value, int playerID) {
        // 得点リストを変更
        SearchData(playerID).score = value;

        // 得点リストを降順ソート
        m_ScoreList.Sort((a, b) => {
            if (b.score - a.score < 0) return -1;           // Score値 b < a
            if (b.score - a.score > 0) return 1;            // Score値 b > a
            if (b.player_id - a.player_id > 0) return -1;   // PlayerID b < a (この時点でScore値は等しい)
            if (b.player_id - a.player_id > 0) return 1;    // PlayerID b > a (この時点でScore値は等しい)
            return 0;                                       // この時点でScore値はaとb共に等しく、PlayerIDもaとb共に等しい
        });
    }

    /// <summary>
    /// 得点リストをリセット
    /// </summary>
    void Reset() {
        foreach (ScoreData data in m_ScoreList) {
            data.score = 0;
        }
    }

    /// <summary>
    /// 得点リストからデータを取得する
    /// </summary>
    ScoreData SearchData(int playerID) {
        if (m_ScoreList.Count < 0 || m_ScoreList == null) {
            return null;
        }
        return m_ScoreList.Find(s => s.player_id == playerID);
    }
}
```
#解説
###データ構造体
この構造体では`MonobitNetwork.player.ID`とセットでスコア値を保持します。
MonobitNetwork.player.IDとは、ネットワーク上に登録された、自分自身のプレイヤーIDを指します。
参考：[プレイヤーパラメータ-MUN公式](http://www.monobitengine.com/doc/mun/contents/FeatureClient/PlayerParameter.htm#MonobitEngine.MonobitNetwork.player.ID%20%E3%83%97%E3%83%AD%E3%83%91%E3%83%86%E3%82%A3)

###初期化
```C#:
public void Init() {
    for (int i = 0; i < MonobitNetwork.room.playerCount; i++) {
        m_ScoreList.Add(new ScoreData(MonobitNetwork.playerList[i].ID));
    }
    // 得点をリセット
    Reset();
}
```
この初期化関数はルームに入室している状態で呼ぶのが正しいので、全員のプレイヤーが入室し終わったときが良いと思います。
処理としては、ルームにいるプレイヤー全員分のScoreDataListを作っています。

###変更
```C#:
public void Add(int value) {
    monobitView.RPC("RecvAddScore", MonobitTargets.Host, value, MonobitNetwork.player.ID);
}
```
変更の要求は`monobitView.RPC`で行います。関数の第一引数にコールする関数名、第二引数にコール対象、以降はコールする関数へ渡したい値です。参考：[RPC(Remote Procedure Call)-MUN公式](http://www.monobitengine.com/doc/mun/)

```C#:
[MunRPC]
void RecvAddScore(int value, int playerID) {
    // 加算する
    int temp = SearchData(playerID).score;
    temp += value;

    // 無効な値かチェック
    if (temp >= 0) {
        // 全クライアントに得点同期通知
        monobitView.RPC("RecvSyncScore", MonobitTargets.All, temp, playerID);
    }
}
```
コールされたホストは、引数のPlayerIDに紐づくスコア値を取得し、計算を行ったのちに無効な値かどうかチェックをします。問題なければ計算結果をクライアント全員に対してRPCで配ります。このときホスト自身も含まれます。

###同期とソート
```C#:
[MunRPC]
void RecvSyncScore(int value, int playerID) {
    // 得点リストを変更
    SearchData(playerID).score = value;

    // 得点リストを降順ソート
    m_ScoreList.Sort((a, b) => {
        if (b.score - a.score < 0) return -1;           // Score値 b < a
        if (b.score - a.score > 0) return 1;            // Score値 b > a
        if (b.player_id - a.player_id > 0) return -1;   // PlayerID b < a (この時点でScore値は等しい)
        if (b.player_id - a.player_id > 0) return 1;    // PlayerID b > a (この時点でScore値は等しい)
        return 0;                                       // この時点でScore値はaとb共に等しく、PlayerIDもaとb共に等しい
    });
}
```
得点リストを変更すれば、全クライアントで値が同一のものとなるので同期されたことになります。続けて、リストを変更したあとソートを行います。順序規則はゲームの仕様によって様々ですが、今回のケースではまずScore値で比較を行い、もし同一ならPlayer.IDの順で並べるようにしました。

#最後に
いかがだったでしょうか。
うまくいかないという方はMUNの基本的な手順が抜けてしまっているかもしれないので、そこを重点的に見直すと良いかもしれません。(MonobitView.csのアタッチ忘れなど)
今後も引き続きMUNの機能を紹介していけたらと思います。

#参考リンク
- [プレイヤーパラメータ-MUN公式](http://www.monobitengine.com/doc/mun/contents/FeatureClient/PlayerParameter.htm#MonobitEngine.MonobitNetwork.player.ID%20%E3%83%97%E3%83%AD%E3%83%91%E3%83%86%E3%82%A3)
- [RPC(Remote Procedure Call)-MUN公式](http://www.monobitengine.com/doc/mun/)