Monobit Unity Networking 2.0(MUN)を使ってみた話

#はじめに
仕事でMonobit Unity Networkingについて調べる機会があったので，備忘録がてら記事にしました。
チュートリアルの内容のまとめと，ちょっと使うときに便利かなと思った実装法を中心に書きます。
サーバとかの設定は公式が結構ちゃんとドキュメント出してくれてるので（[参考](http://www.monobitengine.com/doc/mun/)），この記事では基本的にコードとかの実装方面をまとめていきます。
かなりふわふわしてる記事なので，間違い等あればコメントで指摘していただけると幸いです。
あと掲載コードについては申し訳ないんですが，全部について動くかの検証はしてないです。動いたコードに近いコードを載せるように努力してますが，動かなかったら文句言ってください。気力や時間があったら検証します。

#Monobit Unity Networking とは
Monobit Unity Networking(MUN)とは，モノビットエンジンが提供する国産Unity用通信ミドルウェアです。つまりUnityでマルチプレイとか通信とかをやるためのアセットです。
通信量に合わせて課金されるタイプなので，全機能を使うのにお金はかかりません。
基本的にはUnityで作ったクライアント間のP2P通信で，この記事では取り上げませんが，オンプレミス版も提供されています。
詳しくは[ここ](https://www.monobitengine.com/)

#前提環境
Unity 2020.3.5f
MUN 2.8.0

#基本的な機能と実装

##基礎的な用語
ルーム：プレイヤーが実際に通信している部屋
ロビー：ルームの集まり，ここではプレイヤー間の通信はできない

##MonoBehaviorの上書き
Munを使う上で，`MonoBehavior`の代替クラスがあります。主に`MonobitEngine.MonoBehavior`と`MonobitEngine.MunMonoBehavior`があります。後者は接続やルームの入室のコールバックが定義されてます。
`MonobitEngine.MonoBehavior`は`MonobitEngine.MunMonoBehavior`を含めた大体のMonobitで定義されるコンポーネントの基底クラスになってるという特徴がありますが，多分ユーザで使う分には`MonobitEngine.MunMonoBehavior`を継承しとけば十分だと思います。だって継承されてるから`MonobitEngine.MonoBehavior`の全機能使えるし。


##サーバへの接続〜ルーム入室
ちなみにちょくちょく出てくる`MonobitEngine.MonobitNetwork`は静的クラスなのでMunを導入しておけばどこでも呼べます。
###サーバへの接続・切断
[公式記事（サーバへの接続）](http://www.monobitengine.com/doc/mun/contents/FeatureClient/ConnectToServer.htm)
[公式記事（サーバからの切断）](http://www.monobitengine.com/doc/mun/contents/FeatureClient/DisconnectFromServer.htm)

```C#
//サーバへの接続
//gameVersion:ゲームのバージョンを表す文字列
MonobitEngine.MonobitNetwork.ConnectServer(string gameVersion)

//サーバからの切断
MonobitEngine.MonobitNetwork.DisconnectServer()
```

この際，`MonobitEngine.MonobitNetwork.autoJoinLobby`を**事前に**`true`にしておくとデフォルトのロビーに入ってくれます。

###ロビーへの入室・退出・ほか操作
[公式記事（ロビー入室）](http://www.monobitengine.com/doc/mun/contents/FeatureClient/CreateAndJoinLobby.htm)
[公式記事（ロビー退出）](http://www.monobitengine.com/doc/mun/contents/FeatureClient/LeaveLobby.htm)
前述した`MonobitEngine.MonobitNetwork.autoJoinLobby`のフラグを立てて入室してやるとここら辺の処理は省略できます。

```C#
//デフォルトロビーへの入室
MonobitEngine.MonobitNetwork.JoinLobby()

//指定したロビーへの入室
//lobbyInfo:ロビー設定とか
//ロビーがなかったら勝手に作るので注意
MonobitEngine.MonobitNetwork.JoinLobby(MonobitEngine.LobbyInfo lobbyInfo)

//ロビーからの退出
MonobitEngine.MonobitNetwork.LeaveLobby()
```

`LobbyInfo`には`Name`と`Kind`のプロパティがありますが，`Kind`はドキュメントにも詳しい仕様が書いてなかったので正直よくわかりません。誰か見つけたら教えてください（他力本願）

```C#
//lobbyInfoの実装例
var info = new MonobitEngine.MonobitNetwork();
info.Kind = LobbyKind.Default;
lobby.Name = "MyLobby";
MonobitEngine.MonobitNetwork.JoinLobby(info);
```

[公式記事（ルーム一覧の取得）](http://www.monobitengine.com/doc/mun/contents/FeatureClient/GetRoomData.htm)
また，ロビーの中からは公開ルームの一覧を取得できます。

```C#
//公開ルームを全取得
//RoomData[]で帰ってきます
MonobitEngine.MonobitNetwork.GetRoomData()
```


###ルーム作成・入室・退出
[公式記事（ルーム作成）](http://www.monobitengine.com/doc/mun/contents/FeatureClient/CreateRoom.htm)
[公式記事（ルーム入室）](http://www.monobitengine.com/doc/mun/contents/FeatureClient/JoinRoom.htm)
[公式記事（ルーム退出）]
(http://www.monobitengine.com/doc/mun/contents/FeatureClient/LeaveRoom.htm)

```C#
//ルーム作成
//roomName: ルーム名
//roomSettings: ルームの設定
//lobbyInfo:属するロビー
MonobitEngine.MonobitNetwork.CreateRoom(string roomName, MonobitEngine.RoomSettings roomSettings, MonobitEngine.LobbyInfo lobbyInfo)

//ルーム入室
//なかったらfalseが帰ってきます
//roomName: 入りたいルーム名
MonobitEngine.MonobitNetwork.JoinRoom(string roomName)

//ルーム入室，なかったら作成
//roomName: ルーム名
//roomSettings: ルームの設定
//lobbyInfo:属するロビー
MonobitEngine.MonobitNetwork.JoinOrCreateRoom(string roomName, MonobitEngine.RoomSettings roomSettings, MonobitEngine.LobbyInfo lobbyInfo)

//ランダム入室
MonobitEngine.MonobitNetwork.JoinRandomRoom()

//条件に合うルームにランダム入室
//expectedCustomRoomProperties: 検索したいカスタムパラメータ，ルーム作成側はroomSettings.customParametersで設定できる（後述）
//expectedMaxPlayers: 検索したいルームの入室人数を指定，0なら考慮しない
MonobitEngine.MonobitNetwork.JoinRandomRoom(Hashtable expectedCustomRoomProperties, byte expectedMaxPlayers)

//ルーム退出
MonobitEngine.MonobitNetwork.LeaveRoom()
```

`roomSettings`周りが結構複雑です。これには次のプロパティを設定できます。

```
byte maxPlayers : 入室可能人数
bool isVisible : trueならロビーから検索用メソッド呼ぶと検索できる
bool isOpen : 他プレイヤーの入室の可否
Hashtable customRoomParameters: カスタムパラメータ，JoinRandomRoomとかの検索条件に指定できる
string[] customRoomParametersForLobby: ここに指定したカスタムパラメータはロビーから見えるようになり，検索用メソッドの条件に指定できる
```
特に`customRoomParameters`がよーわからないと思います。これはつまり`Hashtable`型のいろんなタグパラメータをつけられるっていう感じで，`JoinRandomRoom`とかの絞り込みに影響します。
[詳しくはこちら（公式記事）]
(http://www.monobitengine.com/doc/mun/contents/FeatureClient/RoomCustomParameter.htm)
例を示します。

```C#
var hashTable = new Hashtable();
hashTable["tag"] = "Unity";

var settings = new RoomSettings();
settings.roomParameters = hashTable;
MonobitNetwork.CreateRoom("RoomFoo", settings, LobbyInfo.Default);
```

というルームを作ると，

```C#
var hashTable = new Hashtable();
hashTable["tag"] = "Unity";

MonobitEngine.MonobitNetwork.JoinRandomRoom(hashTable, 0);
```
でそのルームにマッチングできるようになるはずです。
公式記事では`roomParameters`に対してインデックスを指定して書き込む方法で例が示されていますが，自分の環境ではそれをやると「`roomParameters`の中身なんもねぇよ！」って怒られたので，こちらでは自分で作った`Hashtable`で上書きしています。こちらだと通ることも確認しています。


##オブジェクトの位置とアニメーションの同期
[参考公式記事](http://www.monobitengine.com/doc/mun/contents/Tutorial/AddSynchronizedComponent.htm)
こちらはほぼスクリプトなしで実装できます。

同期したいオブジェクトに`Monobit View`を追加し，
![MonoView.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/731940/fb2d0e28-9919-161d-40ce-d054b3e40106.png)

位置ならば`Monobit Transform View`，アニメーションなら`Monobit Animation View`のスクリプトを追加して，
![MTAV.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/731940/497e3905-f1c0-38cf-ca69-02664b79021a.png)


`Monobit View`の`Observed Component Registration List`に`Monobit Transform View`や`Monobit Animation View`を追加します。
![OCRL.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/731940/38474d90-584e-4cdb-d496-b334544e9f4a.png)

これでとりあえず位置の同期は完了です。

##ネットワーク越しのオブジェクト生成・破棄
[公式記事（オブジェクト生成）](http://www.monobitengine.com/doc/mun/contents/FeatureClient/Instantiate.htm)
[公式記事（オブジェクト破棄）](http://www.monobitengine.com/doc/mun/contents/FeatureClient/Destroy.htm)
ネットワーク越しで生成や破棄を同期させたいオブジェクトに関しては，`UnityEngine.Instantiate`するとうまくいきません。（どううまくいかないかは検証して追記するかもしれないです）

```C#
//ネットワーク越しにオブジェクト生成
//string prefabName: プレファブ名（内部的にはResources.Loadしてるので正確にはパス名？要検証）
//positionとrotationはUnityEngine.Instantiateと同じなので略
//int group: 所属するグループ名（なんかわかったら追記します）
MonobitEngine.MonobitNetwork.Instantiate(string prefabName, Vector3 position, Quaternion rotation, int group)

//ネットワーク越しに対象viewに所属するオブジェクトを全部破棄
//MonobitView monoView: 破棄する対象のMonobitView
MonobitEngine.MonobitNetwork.Destroy(MonobitView monoView)

//ネットワーク越しに対象オブジェクト破棄
//引数gameObjectは技術ドキュメントだとMonobitView型ですがGameObject型です。（検証済み，あとAPIリファレンスはそうなっています）
MonobitEngine.MonobitNetwork.Destroy(GameObject gameObject)
```

`MonobitEngine.MonobitNetwork.Instantiate`は`Resources.Load`を使ってるようなので，**生成するプレファブは`Resources`フォルダにないといけません**。あと指定するのがプレファブの`GameObject`ではなく**プレファブ名**なのも注意。


##操作キャラの位置同期（オブジェクトの所有権の処理）
[公式記事（オブジェクトの所有権）](http://www.monobitengine.com/doc/mun/contents/FeatureClient/IsMine.htm)
[参考公式記事](http://www.monobitengine.com/doc/mun/contents/Tutorial/TestFailedMultiPlay.htm)
さて，ここまでの作業でとりあえずオブジェクトの生成と位置やアニメーションの同期はできますが，操作キャラだけはもうちょっと作業が要ります。というのも，同期された相手のオブジェクトはこちらのシーンに召喚される都合上，何も対策をしないと同期されてきた相手のオブジェクトも操作できてしまうため，位置の同期とこちらの操作で干渉してなんか変なことになります。
そのため，自分が所有権をもつオブジェクト以外は操作しないというスクリプトを書いてやらないといけなくなります。
具体的には，自分がオブジェクトの所有権をもつかは，`MonobitEngine.MonoBehavior`（または`MunMonoBehavior`）内の`monobitView.isMine`プロパティで判定できます。これを使って操作スクリプトを無効化してやればいいです。実装例は下の方にあります。

##ネットワーク越しのメソッド呼び出し
[公式記事](http://www.monobitengine.com/doc/mun/contents/FeatureClient/Destroy.htm)
`MonobitEngine.MonoBehavior`（または`MunMonoBehavior`）なら，ネットワーク越しでRPCという手法によってメソッドを呼び出してやれます。
まず，呼び出したいメソッドに`[MunRPC]`を指定します。
`MonobitView`を適当なオブジェクトにアタッチしてやり，`MonobitViewID`を0以外の適当な正の整数値にしてやります。
それを`[SerializedField]`なり`GetComponent`なりで取得してやって，`MonobitView`の`RPC()`メソッドを呼べばOKです。
なんのこっちゃってなってると思うので例を示します。

```C#
[MunRPC]
void targetMethod(string message, int id){
    Debug.Log(id + " says " + message);
}

void callRpc(){
    
    var view = GetComponent<MonobitView>();

    view.RPC(
        nameof(targetMethod) //関数の名前(string)
        , MonobitTargets.All //呼び出しの対象，公式ドキュメント参照
                             //(ここから呼び出し先の引数)
        , "Hi!"              //message
        , 0                  //id        
        );
}

```

##コールバック
サーバに繋いだ時や，ルームに入った時など，いろんな時に対してコールバックが用意されています。数が多いのでここでは紹介せず[こちら](http://www.monobitengine.com/doc/mun/contents/FeatureClient/CallbackFunction.htm)を参照していただきたいですが，一つだけハマりそうな部分なんですが，**コールバックの利用は`MunMonoBehavior`を継承する必要があります。**逆に言うと，**`MonobitEngine.MonoBehavior`ではコールバックが利用できません。**
理由はコールバックは`MunMonoBehavior`のメソッドをオーバーライドする形で実装されてるため，`MonobitEngine.MonoBehavior`ではそもそものメソッドが定義されてないんですよね。もう`MunMonoBehavior`だけでいいんじゃないかな。

#ちょっと一工夫

##オブジェクトの所有権の処理
[公式のチュートリアル](http://www.monobitengine.com/doc/mun/contents/Tutorial/EnableIsMineControl.htm)では操作スクリプトに直接所有権の判定を組み込んでいますが，操作スクリプト一個一個に組み込んでるとキリがないですし，基本的にゲーム自体の開発してる時は通信のことなんて考えたくないと思うので，個別に管理してくれるコンポーネントを作った方が管理しやすいと思います。

```C#:OwnerHandler.cs
/*
OwnerHandler.cs

Copyright (c) 2021 Dango

This software is released under the MIT License.
http://opensource.org/licenses/mit-license.php
*/

using System.Collections.Generic;
using MonobitEngine;
using UnityEngine;
using MonoBehaviour = UnityEngine.MonoBehaviour;

namespace MunCommunication {
    /// <summary>
    /// Configure MonoBehavior.enabled by ownership of attached object.
    /// </summary>
    public class OwnerHandler : MunMonoBehaviour {
        [SerializeField] private List<MonoBehaviour> enableIfOwner;
        [SerializeField] private List<MonoBehaviour> disableIfOwner;
        
        // Start is called before the first frame update
        void Start() {
            configComponents();
        }
        
        /// <summary>
        /// Check ownership, and configure "enabled" property.
        /// </summary>
        void configComponents() {
            bool isOwner = monobitView.isMine;
            enableIfOwner.ForEach(behaviour => { behaviour.enabled = isOwner;});
            disableIfOwner.ForEach(behaviour => { behaviour.enabled = !isOwner;});
        }
    }
}

```
これでインスペクタに所有権があるときに有効/無効にするリストができてくれるので設定してやればいいです。

##パスワードの設定
MUNはルームに対してパスワードを設定できたりはしないんですが，[こちらのPhotonの記事のように](https://support.photonengine.jp/hc/ja/articles/218961187-%E6%8B%9B%E5%BE%85%E8%80%85%E3%81%AE%E3%81%BF%E3%83%AB%E3%83%BC%E3%83%A0%E3%81%B8%E5%85%A5%E5%AE%A4%E3%82%92%E8%A8%B1%E5%8F%AF%E3%81%99%E3%82%8B%E6%96%B9%E6%B3%95)カスタムパラメータをつけて，RandomJoinをやってやればそれに近いことをできます。
**簡易的な**実装例

```C#:RoomSelector.cs
/*
RoomSelector.cs

Copyright (c) 2021 Dango

This software is released under the MIT License.
http://opensource.org/licenses/mit-license.php
*/

using System.Collections;
using System.Collections.Generic;
using MonobitEngine;
using UnityEngine;
using UnityEngine.Events;

using MonobitNetwork = MonobitEngine.MonobitNetwork;

namespace MunCommunication {
    public class RoomSelector : MunMonoBehaviour {
        
        Hashtable generateAuthTable(string roomName, string password) {
            var customParam = new Hashtable();

            customParam["name"] = roomName;
            customParam["password"] = password;

            return customParam;
        }
        
        public void createAuthorizedRoom(string roomName, string password) {
            var setting = generateAuthTable(roomName, password);

            var roomSetting = new RoomSettings {
                roomParameters = setting
                , isVisible = false
            };

            //ランダム文字列生成
            string randomLine = Guid.NewGuid().ToString("N").Substring(0, 10);

            MonobitNetwork.CreateRoom(randomLine, roomSetting, LobbyInfo.Default);
        }

        public void joinAuthorizedRoom(string roomName, string password) {
            var setting = generateAuthTable(roomName, password);

            MonobitNetwork.JoinRandomRoom(setting, 0);
        }
    }
}


```

ただ，これだと同じルーム名・パスワードが同時に発生した場合に対応ができないので，外部に認証サーバを設けてルーム名を管理してやらないと実用に足るものはおそらく作れないと思います。

#まとめ
MUNでできること
・ネットワーク越しのオブジェクトの位置やアニメーションの同期
・ネットワーク越しでのメソッド呼び出し
ちょっとよくわからない部分がなくもないですが，使ってる感じは問題ないような感じがします。

#参考
[MUNチュートリアル](http://www.monobitengine.com/doc/mun/)
[MUNのAPIリファレンス](http://www.monobitengine.com/doc/doxygen/client/html/index.html)
[パスワードのかけかたとか：Photonのドキュメント](https://support.photonengine.jp/hc/ja/articles/218961187-%E6%8B%9B%E5%BE%85%E8%80%85%E3%81%AE%E3%81%BF%E3%83%AB%E3%83%BC%E3%83%A0%E3%81%B8%E5%85%A5%E5%AE%A4%E3%82%92%E8%A8%B1%E5%8F%AF%E3%81%99%E3%82%8B%E6%96%B9%E6%B3%95)
[ランダム文字列](https://kagasu.hatenablog.com/entry/20150621/1434866124)