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

#はじめに
これまで、MUNの記事を色々とまとめてきましたが、[公式リファレンス](http://www.monobitengine.com/doc/mun/)がかなり充実していることもあり、
基本的な内容は飛ばして紹介することもしばしばでした。

しかし、いくらリファレンスがあるとは言え、一から読むのもなかなか苦ですから、
サーバ接続/切断、ルーム入室/退室という機能に絞り「**とりあえずこれを書けば動かして確認できる**」という所までを一度まとめることにしました。
MUNを使ったルームマッチング機能を作る上で参考程度に読んでいただければと思います。

※対象読者さんはオンライン開発を始めたばかりの方ですので**初心者向け**です。

モノビットエンジンを使う準備がお済でない方は、まずはこちらをご覧ください。
・[モノビットエンジンクラウドでリアルタイム通信のオンラインゲームを作る：準備編](https://qiita.com/nisoka/items/78e6b4d609f25289024a)
・[モノビットエンジンでかんたんサーバ接続](https://qiita.com/nisoka/items/a7dfe4fad2e0b201ecaa)

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

#結果
サーバ**未接続**状態
![1.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/401086/4fd5e851-c0b8-6d19-2c9c-e4419e3b94f4.png)
サーバ**接続**状態
![2.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/401086/f02f4814-c8e9-d2dc-7822-7641ff92a59b.png)
**他クライアント**から見たロビー画面
![3.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/401086/edc85191-9ee2-90fb-38f4-8d121a75ce8d.png)

#作るもの
まず、以下の3つのスクリプトからなる**ネットワークマネージャクラス**を作ります。
**NetworkManager.cs**　…　ネットワーク管理クラス
**　|ーServerManager.cs**　…　サーバ接続/切断を管理するクラス
**　|ーMatchingManager.cs**　…　ルーム入室/退室、マッチングを管理するクラス

次に、使い方と動作確認のために**uGUI表示クラス**を作ります。
**InGameGUI.cs**
こちらはあくまで確認用なので必要のない方は読み飛ばしてもらって大丈夫です。

注意点として、NetworkManagerクラスはシングルトンクラスなので、Monobehaviourを汎用シングルトン用に改造した**SingletonMonobehaviourクラスを継承させます。**別の方法でシングルトンにしても全く問題ありません。
書くのが面倒な方はページの最後のほうにSingletonMonobehaviourクラスを載せているので合わせて使ってください。

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

public class NetworkManager : SingletonMonoBehaviour<NetworkManager> {

    ServerManager m_ServerManager = null;
    MatchingManager m_MatchingManager = null;

    void Awake () {
        m_ServerManager = gameObject.AddComponent<ServerManager>();
        m_MatchingManager = gameObject.AddComponent<MatchingManager>();
    }
    
    /// <summary>
    /// サーバに接続
    /// </summary>
    public void ConnectServer(string playerName) {
        if (m_ServerManager == null) return;
        m_ServerManager.ConnectServer(playerName);
    }

    /// <summary>
    /// サーバから切断
    /// </summary>
    public void DisconnectServer() {
        if (m_ServerManager == null) return;
        m_ServerManager.DisconnectServer();
    }

    /// <summary>
    /// ルーム作成
    /// </summary>
    public void CreateRoom(string roomName) {
        if (m_MatchingManager == null) return;
        m_MatchingManager.CreateRoom(roomName);
    }

    /// <summary>
    /// ルーム入室
    /// </summary>
    public void JoinRoom(string roomName) {
        if (m_MatchingManager == null) return;
        m_MatchingManager.JoinRoom(roomName);
    }

    /// <summary>
    /// ルーム退室
    /// </summary>
    public void LeaveRoom() {
        if (m_MatchingManager == null) return;
        m_MatchingManager.LeaveRoom();
    }

    /// <summary>
    /// サーバからの切断要求がある
    /// </summary>
    /// <returns></returns>
    public bool IsDisconnectRequest() {
        bool res = false;
        if (m_ServerManager != null) {
            res = m_ServerManager.m_IsDisconnectRequest;
        }
        return res;
    }

    /// <summary>
    /// オブジェクトが削除されたタイミングで呼ばれる
    /// </summary>
    protected override void OnDestroy() {
        base.OnDestroy();
    }
}
```
実際のゲームからはこのNetworkManagerクラスのメソッドを呼び出してサーバ接続/切断、ルーム入室/退室を行っていきます。
このクラスは見ての通り下位クラスメソッドを呼び出すための仲介役です。
細かい処理は下位クラスで行います。見ていきましょう。

###ServerManager
```C#:
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using MonobitEngine;

public enum ConnectionState {
    Connect,            // 接続中
    Disconnect,         // 切断中
}

public class ServerManager : MonobitEngine.MonoBehaviour {

    public ConnectionState m_State = ConnectionState.Disconnect;

    public bool m_IsDisconnectRequest = false;
    
    /// <summary>
    /// サーバに接続
    /// </summary>
    public void ConnectServer(string playerName) {
        if(!MonobitNetwork.isConnect) {
            MonobitNetwork.autoJoinLobby = true;
            MonobitNetwork.playerName = playerName;
            MonobitNetwork.ConnectServer("v1.0");
            m_State = ConnectionState.Disconnect;
        }
    }

    /// <summary>
    /// サーバを切断
    /// </summary>
    public void DisconnectServer() {
        if (MonobitNetwork.isConnect) {
            m_IsDisconnectRequest = true;
            if (MonobitNetwork.inRoom) {
                MonobitNetwork.LeaveRoom();
            } else if (MonobitNetwork.inLobby) {
                MonobitNetwork.LeaveLobby();
            }
        }
    }

    //--------------------------------------------------
    // 以下、MUNコールバック関数
    //--------------------------------------------------
    /// <summary>
    /// サーバに接続したときに呼ばれる
    /// </summary>
    public void OnConnectedToServer() {
        Debug.Log("OnConnectedToServer");
        m_State = ConnectionState.Connect;
    }

    /// <summary>
    /// サーバから切断されたときに呼ばれる
    /// </summary>
    public void OnDisconnectedFromServer() {
        Debug.Log("OnDisconnectedFromServer");
        m_State = ConnectionState.Disconnect;
        m_IsDisconnectRequest = false;
    }

    /// <summary>
    /// ロビーに入室したときに呼ばれる
    /// </summary>
    public void OnJoinedLobby() {
        if (MonobitNetwork.autoJoinLobby) {
            Debug.Log("OnConnectedToServer");
            m_State = ConnectionState.Connect;
        }
        Debug.Log("OnJoinedLobby");
    }
}
```
サーバ接続/切断を行うクラスです。
`ConnectServer()`はサーバ未接続なら接続要求を出します。
サーバがその要求を受理すると`OnConnectedToServer()`コールバックメソッドが呼ばれます。
([サーバへの接続 - MUN公式リンク](http://www.monobitengine.com/doc/mun/contents/FeatureClient/ConnectToServer.htm))
([コールバック関数一覧 - MUN公式リンク](http://www.monobitengine.com/doc/mun/))

`DisconnectServer()`は反対に切断の要求を出しますが、注意点として入室状態なら**ルーム退室→ロビー退室→サーバ切断**という順に処理を行います。([サーバからの切断 - MUN公式リンク](http://www.monobitengine.com/doc/mun/))

###MatchingManager
```C#:
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using MonobitEngine;

public enum RoomErrorType {
    CROWDED_ROOM,       // 部屋が満員
    NOT_EXIST_ROOM,     // 部屋が存在しない
    FAILED_CREATE_ROOM, // 部屋の作成に失敗
    ALREADY_JOIN_ROOM,  // 既に入室済み
    UNKNOWN_ERROR,      // その他のエラー
    NONE,               // エラー無し
}

public class MatchingManager : MonobitEngine.MonoBehaviour {
    
    Dictionary<RoomErrorType, string> m_RoomErrorMsgDict = new Dictionary<RoomErrorType, string>() {
        {RoomErrorType.CROWDED_ROOM,        "Crowded room."},
        {RoomErrorType.NOT_EXIST_ROOM,      "Not exist room."},
        {RoomErrorType.FAILED_CREATE_ROOM,  "Failed create room."},
        {RoomErrorType.ALREADY_JOIN_ROOM,   "Already join room."},
        {RoomErrorType.UNKNOWN_ERROR,       "Unknown error."},
    };

    /// <summary>
    /// ルームを作成する
    /// </summary>
    public RoomErrorType CreateRoom(string roomName) {
        var ret = RoomErrorType.NONE;

        do {
            if (MonobitNetwork.inRoom) {
                ret = RoomErrorType.ALREADY_JOIN_ROOM;
                break;
            }

            RoomSettings roomSetings = new RoomSettings() {
                isVisible = true,
                isOpen = true,
                maxPlayers = (uint)DefineValue.MAX_ROOM_PLAYERS_COUNT,
                roomParameters = null,
                lobbyParameters = new string[] { }
            };

            if (!MonobitNetwork.CreateRoom(roomName, roomSetings, null)) {
                ret = RoomErrorType.FAILED_CREATE_ROOM;
                break;
            }

        } while (false);

        ParseRoomErrorLog(ret);

        return ret;
    }

    /// <summary>
    /// ルームに入室する
    /// </summary>
    public RoomErrorType JoinRoom(string roomName) {
        var ret = RoomErrorType.NONE;

        ret = EnableJoinRoom(roomName);

        if (ret == RoomErrorType.NONE) {
            MonobitNetwork.JoinRoom(roomName);
        }

        ParseRoomErrorLog(ret);

        return ret;
    }

    /// <summary>
    /// 入室可能かどうか調べる
    /// </summary>
	public RoomErrorType EnableJoinRoom(string roomName) {
        var ret = RoomErrorType.NONE;
        bool isExist = false;
        foreach (RoomData roomData in MonobitNetwork.GetRoomData()) {
            if (roomData.name == roomName) {
                isExist = true;
                if (roomData.playerCount >= DefineValue.MAX_ROOM_PLAYERS_COUNT) {
                    ret = RoomErrorType.CROWDED_ROOM;
                }
                break;
            }
        }

        if (!isExist) {
            ret = RoomErrorType.NOT_EXIST_ROOM;
        }

        ParseRoomErrorLog(ret);

        return ret;
    }

    /// <summary>
    /// ルームを去る
    /// </summary>
    public void LeaveRoom() {
        if (MonobitNetwork.inRoom) {
            MonobitNetwork.LeaveRoom();
        }
    }

    /// <summary>
    /// エラー内容を解読して表示
    /// </summary>
    /// <param name="errorType"></param>
    public void ParseRoomErrorLog(RoomErrorType errorType) {
        if (m_RoomErrorMsgDict.ContainsKey(errorType)) {
            string log = m_RoomErrorMsgDict[errorType];
#if DEBUG_LOG
            Debug.LogWarning("ROOM_ERROR：" + log);
#endif
        }
    }

    //--------------------------------------------------
    // 以下、MUNコールバック関数
    //--------------------------------------------------
    /// <summary>
    /// ルーム作成が成功したときに呼ばれる
    /// </summary>
    public void OnCreatedRoom() {
        Debug.Log("OnCreatedRoom");
    }

    /// <summary>
    /// ルームに入室したときに呼ばれる
    /// </summary>
    public void OnJoinedRoom() {
        Debug.Log("OnJoinedRoom");
    }

    /// <summary>
    /// ルームを退室したとき呼ばれる
    /// </summary>
    public void OnLeftRoom() {
        Debug.Log("OnLeftRoom");
        if (NetworkManager.Instance.IsDisconnectRequest()) {
            MonobitNetwork.LeaveLobby();
        }
    }

    /// <summary>
    /// ロビーから退室したときに呼ばれる
    /// </summary>
    public void OnLeftLobby() {
        Debug.Log("OnLeftLobby");
        if (NetworkManager.Instance.IsDisconnectRequest()) {
            MonobitNetwork.DisconnectServer();
        }
    }
}

```
ルームの入退室に関する処理をするクラスです。
`CreateRoom()`のなかでは`MonobitNetwork.CreateRoom()`を使ってルームを作成をしています。第２引数にルーム設定情報を指定することができます。([ルームの作成 - MUN公式リンク](http://www.monobitengine.com/doc/mun/contents/FeatureClient/CreateRoom.htm))

`JoinRoom()`のなかでは`EnableJoinRoom()`を使って入室可能かどうか調べています。
`foreach (RoomData roomData in MonobitNetwork.GetRoomData())`で公開中の全ルームを取得し、ルーム名を比較しています。
([ルームへの入室 - MUN公式リンク](http://www.monobitengine.com/doc/mun/))

##InGameGUI
```C#:
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using MonobitEngine;

public class InGameGUI : MonobitEngine.MonoBehaviour {

    string m_PlayerName = "player1";
    string m_RoomName = "room1";

    void OnGUI() {
        if (!MonobitNetwork.isConnect) {
            OnGUI_Connect();
        }
        else if (!MonobitNetwork.inRoom) {
            OnGUI_RoomCreate();
            OnGUI_ChooseRoom();
        }
        else {
            OnGUI_LeaveRoom();
        }
        OnGUI_Disconnect();
    }

    void OnGUI_Connect() {
        GUILayout.BeginHorizontal();
        GUILayout.Label("Your Name");
        m_PlayerName = GUILayout.TextField(m_PlayerName, GUILayout.Width(150));

        if (GUILayout.Button("Connect Server", GUILayout.Width(150))) {
            if (!string.IsNullOrEmpty(m_PlayerName)) {
                NetworkManager.Instance.ConnectServer(m_PlayerName);
            }
        }
        GUILayout.EndHorizontal();
    }
    
    void OnGUI_Disconnect() {
        if (GUILayout.Button("Disconnect Server", GUILayout.Width(150))) {
            NetworkManager.Instance.DisconnectServer();
        }
    }

    void OnGUI_LeaveRoom() {
        if (GUILayout.Button("Leave Room", GUILayout.Width(150))) {
            NetworkManager.Instance.LeaveRoom();
        }
    }

    void OnGUI_RoomCreate() {
        GUILayout.BeginHorizontal();
        GUILayout.Label("Room Name");
        m_RoomName = GUILayout.TextField(m_RoomName, GUILayout.Width(150));

        if (GUILayout.Button("Create", GUILayout.Width(75))) {
            if (!string.IsNullOrEmpty(m_RoomName)) {
                NetworkManager.Instance.CreateRoom(m_RoomName);
            }
        }
        GUILayout.EndHorizontal();
    }

    void OnGUI_ChooseRoom() {
        foreach (RoomData roomData in MonobitNetwork.GetRoomData()) {
            if (roomData.playerCount < roomData.maxPlayers) {
                GUILayout.BeginHorizontal();
                GUILayout.Label("Room Name: " + roomData.name + "(" + roomData.playerCount + "/" + ((roomData.maxPlayers == 0) ? "-" : roomData.maxPlayers.ToString()) + ")", GUILayout.Width(200));

                if (GUILayout.Button("Join", GUILayout.Width(75))) {
                    MonobitNetwork.JoinRoom(roomData.name);
                }
                GUILayout.EndHorizontal();
            }
        }
    }
}
```

##SingletonMonoBehaviour
```C#:
using System.Collections;
using System.Collections.Generic;
using UnityEngine;

public class SingletonMonoBehaviour<T> : MonoBehaviour where T : MonoBehaviour {

    private static T m_Instance = null;

    /// <summary>
    /// インスタンス取得
    /// </summary>
    public static T Instance
    {
        get
        {
            if(m_Instance == null) {
                GameObject obj = new GameObject(typeof(T).Name);
                m_Instance = obj.AddComponent<T>();
                DontDestroyOnLoad(obj);
            }
            return m_Instance;
        }
    }

    /// <summary>
    /// 既にインスタンスが存在するか
    /// </summary>
    public static bool IsInstance() {
        var ret = true;
        if (m_Instance == null) {
            ret = false;
        }
        return ret;
    }
    
    /// <summary>
    /// オブジェクト削除処理
    /// </summary>
    protected virtual void OnDestroy() {
        m_Instance = null;
    }
}
```

#最後に
いかがだったでしょうか。
結果確認についてはビルド後に複数クライアントを立ち上げて確認することをお勧めします。
これで一通りMUNでネットワーク処理を作る土台はできたと思います。
続きが気になる方は以下のリンクから同期処理に挑戦してみてください。

#MUN関連記事
**かんたんなPinpong対戦ゲーム**を作った記事はこちらです

- [モノビットエンジンでリアルタイム通信のオンラインゲームを作る：座標同期を行う](https://qiita.com/nisoka/items/b6064946f3184f85d323)
- [モノビットエンジンでリアルタイム通信のオンラインゲームを作る：ボールを跳ね返す](https://qiita.com/nisoka/items/e00b15d42084d768dd21)

**リアルタイムシューティングゲーム**を作った記事はこちらです

- [【Unity】オンラインシューティングゲームを作る：弾の処理](https://qiita.com/nisoka/items/a10cddec1e108dc41e58)
- [【Unity】オンラインシューティングゲームを作る：敵をつくる](https://qiita.com/nisoka/items/ab49ab381c9fac05afb5)

**機能**に関する記事はこちらです

- [【Unity】入室可能なルームが存在するかチェックする方法【モノビットエンジン入門】](https://qiita.com/nisoka/items/9f5ec0868d2bbe00b476)
- [モノビットエンジン版 RPCメッセージを使いパラメータ同期をおこなう](https://qiita.com/nisoka/items/b75c07a0f0240414e9e5)

#参考リンク
- [Monobit Unity Networking2.0 - MUN公式リンク](http://www.monobitengine.com/doc/mun/)
- [サーバへの接続 - MUN公式リンク](http://www.monobitengine.com/doc/mun/contents/FeatureClient/ConnectToServer.htm)
- [サーバからの切断 - MUN公式リンク](http://www.monobitengine.com/doc/mun/)
- [ルームの作成 - MUN公式リンク](http://www.monobitengine.com/doc/mun/contents/FeatureClient/CreateRoom.htm)
- [ルームへの入室 - MUN公式リンク](http://www.monobitengine.com/doc/mun/)
- [ルームからの退室 - MUN公式リンク](http://www.monobitengine.com/doc/mun/)
- [ルーム一覧の取得 - MUN公式リンク](http://www.monobitengine.com/doc/mun/contents/FeatureClient/GetRoomData.htm)
- [コールバック関数一覧 - MUN公式リンク](http://www.monobitengine.com/doc/mun/)