Monobit MUN のクライアント実装をした際の備忘録。
主に official document から重要そうなものの抜粋と補足。
サーバーサイドはノータッチなので省略。
- 公式ページ
- 検証環境
- プログラミング・クイックスタート<シンプルなチャットの作成>
- プログラミング・チュートリアル<オフラインゲームのオンライン化>
- MUNクライアントの主要機能
- パフォーマンスチューニング
- MUN サーバについて
- 補足
- コールバックのテンプレート
- 再接続処理
公式ページ
検証環境
- Unity 2019.4.24f1
- MUN ver.2.8.0
プログラミング・クイックスタート<シンプルなチャットの作成>
実装目的ならこの辺から読むと良さそう
プログラミング・チュートリアル<オフラインゲームのオンライン化>
- 常時同期オブジェクトに同期用コンポーネントを追加
- MonobitTransformView のカスタマイズ
- 原則的に MonobitTransformView と MonobitView は同一のオブジェクトに存在させる必要がある
- 常時同期オブジェクトのプレハブ化
- MUN でマルチプレイを実践するうえでプレハブ化しない限り、同期処理を実装することはできません
MUNクライアントの主要機能
- 機能概要
- サーバへの接続
- サーバによる、接続時のユーザー分断制御は以下の項目で判定される
- 認証ID
- MUNライブラリバージョン
- ゲームバージョン(ConnectServerの引数の任意の文字列)
- サーバによる、接続時のユーザー分断制御は以下の項目で判定される
- サーバへの接続から切断までの一連の流れを作る(official documentからはリンク切れ)
-
ルームに入室している場合、ルーム入室直前のデータのまま更新されません。 ルーム一覧はあくまで「ロビーにいて、ルーム未入室のプレイヤー」に対して通知されます。 ルームに入室している状態では更新されませんので注意してください。
- ルームへの入室
- ルームからの退室
- RPC(Remote Procedure Call)
- 特定プレイヤーの検索
- 接続コールバック
- 再接続処理
パフォーマンスチューニング
- ルーム入室中の通信量を抑えるには?
- 通信負荷を減らすためには1フレーム内における「RPCメッセージ」「オブジェクト同期」「各種ルームパラメータ」の送信量を調整する方法しかありません
- MUN 2.2.0 以降で実装された「送信帯域制限機能」は、1フレーム内における「RPCメッセージ」「オブジェクト同期」の送信量を簡単に調整することができる
- RPC
- RPCメッセージの送信方式
- 1秒間に MonobitNetwork.sendRate の値だけ送信する
- RPCメッセージキューに蓄積され、送信タイミングに合わせ、RPCメッセージキューに登録された順に送信されます
- RPCメッセージの受信方式
- 受信後に「どのオブジェクトに登録されたRPCのメソッドを呼び出すのか」を検索
- RPCメッセージの送受信処理において最も負荷が高い
- 負荷対策は、RPC の受信メソッドの検索回数を減らすこと
- Tips
- 受信時に呼び出されるメソッド検索対象の「インスタンス」を少なくする
- ある特定のオブジェクト(prefab)の情報同期が目的である場合、RPCメッセージを使わず、OnMonobitSerializeView() を使う
- ある特定のオブジェクトの情報同期が目的である場合、RPCメッセージによる情報共有は推奨されません
- MonobitTransformView で送信されるオブジェクトの位置姿勢情報、ならびに MonobitAnimatorView で送信されるオブジェクトのアニメーション情報を除いた属性情報は、すべて OnMonobitSerializeView() の接続コールバックメソッド で実装することが理想的
- RPCメッセージ本来の同期タイミングについては MonobitNetwork.sendRate に依存しますが、OnMonobitSerializeView() メソッドを利用した場合、MonobitNetwork.updateStreamRate に依存する
- RPCメッセージの送信方式
- オブジェクト同期
- Tips
- MonobitView コンポーネントが付加されているだけで、オブジェクト同期通信自体が処理されてしまうのでシーン内に存在する「MonobitView コンポーネント」を持つオブジェクトは極力減らす
- シーン内の静的オブジェクトの中で「MonobitView コンポーネント」を持つオブジェクトは複数用意しない
- 静的オブジェクトの所有権を持つルームホストにだけ、オブジェクト同期の負荷が掛かる
- MonobitView コンポーネントを持つシーン静的オブジェクト内は1つだけにする
- 最も効果的なオブジェクト同期方法は、1つのスクリプト内で OnMonobitSerializeView() の接続コールバックメソッド を実装し、その中にMonobitTransformView や MonobitAnimatorView のコンポーネントの代替実装を行なう
- Tips
- カスタムパラメータ
- ルームカスタムパラメータ、およびプレイヤーカスタムパラメータによる送受信処理が現行最速
- メリット
- ルームカスタムパラメータ、およびプレイヤーカスタムパラメータは、命令の実行から実際に送信されるまでの遅延は送信命令実行フレームから1フレーム以内である。
- ルームカスタムパラメータ、およびプレイヤーカスタムパラメータは、受信から実際の値反映までにタイムラグが発生しにくい。
- デメリット
- 全てのパラメータを送受信しあう
- 全てのルーム内プレイヤーが共有する情報
- 全てのルーム内プレイヤーが自由に変更できる情報
- 原則
- できる限り変更頻度の低い、かつ、膨大ではない情報の送受信
- ルーム内プレイヤー全員に対し常に共有される情報の送受信
- 悪質なプレイヤーからの情報改ざん(チート)に伴うシステムの弊害が発生しないような情報の送受信
- メリット
- ルームカスタムパラメータ、およびプレイヤーカスタムパラメータによる送受信処理が現行最速
MUN サーバについて
補足
-
- MonobitView IDはScene内のMonobitViewを識別するID (シーン内で複数持てる)
- 静的オブジェクトの場合は、Ownerはシーン、Hostだけがオブジェクトを制御できる
- プレハブの場合は、Ownerは Instantiate したPlayer、そのPlayerがオブジェクトを制御できる
- MonobitView.isMine で所有権を持つオブジェクトか判定する
MonobitNetwork.player.id
- ルームごとに採番されるID
- ルーム入室ごとにIDをインクリメントし、ルームがなくなる(全員が抜ける)とIDは初期化される
コールバックのテンプレート
public class MUNCallbackTemplate : MonobitEngine.MunMonoBehaviour { private const string LOGPREFIX = "[MUN Callback] "; public override void OnConnectedToMonobit() { Debug.Log($"{LOGPREFIX}OnConnectedToMonobit "); } public override void OnConnectToServerFailed(DisconnectCause cause) { Debug.Log($"{LOGPREFIX}OnConnectToServerFailed : cause = {cause}"); } public override void OnMunCloudConnectionFailed(MunCloudConnectionFailedCause cause) { Debug.Log($"{LOGPREFIX}OnMunCloudConnectionFailed : cause = {cause}"); } public override void OnMonobitMaxConnectionReached() { Debug.Log($"{LOGPREFIX}OnMonobitMaxConnectionReached"); } public override void OnConnectedToServer() { Debug.Log($"{LOGPREFIX}OnConnectedToServer"); } public override void OnJoinedLobby() { Debug.Log($"{LOGPREFIX}OnJoinedLobby"); } public override void OnLobbyDataUpdate() { Debug.Log($"{LOGPREFIX}OnLobbyDataUpdate"); } public override void OnUpdatedSearchPlayers() { Debug.Log($"{LOGPREFIX}OnUpdatedSearchPlayers"); } public override void OnReceivedRoomListUpdate() { Debug.Log($"{LOGPREFIX}OnReceivedRoomListUpdate"); } public override void OnCreatedRoom() { Debug.Log($"{LOGPREFIX}OnCreatedRoom"); } public override void OnCreateRoomFailed(object[] codeAndMsg) { Debug.Log($"{LOGPREFIX}OnCreateRoomFailed : errorCode = {codeAndMsg[0]}, message = {codeAndMsg[1]}"); } public override void OnJoinedRoom() { Debug.Log($"{LOGPREFIX}OnJoinedRoom"); } public override void OnJoinRoomFailed(object[] codeAndMsg) { Debug.Log($"{LOGPREFIX}OnJoinRoomFailed : errorCode = {codeAndMsg[0]}, message = {codeAndMsg[1]}"); } public override void OnMonobitRandomJoinFailed(object[] codeAndMsg) { Debug.Log($"{LOGPREFIX}OnMonobitRandomJoinFailed : errorCode = {codeAndMsg[0]}, message = {codeAndMsg[1]}"); } public override void OnOtherPlayerConnected(MonobitPlayer newPlayer) { Debug.Log($"{LOGPREFIX}OnOtherPlayerConnected : playerName = {newPlayer.name}"); } public override void OnHostChanged(MonobitPlayer newHost) { Debug.Log($"{LOGPREFIX}OnHostChanged : playerName = {newHost.name}"); } public override void OnMonobitInstantiate(MonobitMessageInfo info) { Debug.Log($"{LOGPREFIX}OnMonobitInstantiate : creator name = {info.sender.name}"); } public override void OnMonobitCustomRoomParametersChanged(Hashtable parametersThatChanged) { Debug.Log($"{LOGPREFIX}OnMonobitCustomRoomParametersChanged"); } public override void OnMonobitPlayerParametersChanged(object[] playerAndUpdatedParameters) { Debug.Log($"{LOGPREFIX}OnMonobitPlayerParametersChanged"); } public override void OnMonobitSerializeViewWrite(MonobitStream stream, MonobitMessageInfo info) { Debug.Log($"{LOGPREFIX}OnMonobitSerializeViewWrite"); } public override void OnMonobitSerializeViewRead(MonobitStream stream, MonobitMessageInfo info) { Debug.Log($"{LOGPREFIX}OnMonobitSerializeViewRead"); } public override void OnOwnershipRequest(object[] viewAndPlayer) { Debug.Log($"{LOGPREFIX}OnOwnershipRequest"); } public override void OnConnectionFail(DisconnectCause cause) { Debug.Log($"{LOGPREFIX}OnConnectionFail : cause = {cause}"); } public override void OnCustomAuthenticationFailed(string rawData) { Debug.Log($"{LOGPREFIX}OnCustomAuthenticationFailed : rawData = {rawData}"); } public override void OnOtherPlayerDisconnected(MonobitPlayer otherPlayer) { Debug.Log($"{LOGPREFIX}OnOtherPlayerDisconnected : playerName = {otherPlayer.name}"); } public override void OnLeftRoom() { Debug.Log($"{LOGPREFIX}OnLeftRoom"); } public override void OnLeftLobby() { Debug.Log($"{LOGPREFIX}OnLeftLobby"); } public override void OnDisconnectedFromServer() { Debug.Log($"{LOGPREFIX}OnDisconnectedFromServer"); } }
再接続処理
サンプルコード
public class MunSampleClient : MonobitEngine.MunMonoBehaviour { private string address = "127.0.0.1"; private ushort port = 22222; private uint timeout = 5000; private uint keepAlive = 10000; private string roomName; private bool isClosing = false; public void Connect() { MonobitNetwork.autoJoinLobby = true; MonobitNetwork.updateStreamRate = 30; MonobitNetworkSettings.MonobitServerSettings.ServerConnectWaitTime = timeout; MonobitNetworkSettings.MonobitServerSettings.KeepAliveUpdateTime = keepAlive; // Self Server の場合 MonobitNetworkSettings.MonobitServerSettings.SelfServerAddress = address; MonobitNetworkSettings.MonobitServerSettings.SelfServerPort = port; MonobitNetwork.ConnectServer("Sample_v1.0"); } public void CreateRoom(string roomName) { if (MonobitNetwork.isConnect && !MonobitNetwork.inRoom) { var roomSettings = new RoomSettings { maxPlayers = 10, isVisible = true, isOpen = true, }; MonobitNetwork.CreateRoom(roomName, roomSettings, new LobbyInfo()); } } public void JoinRoom(string roomName) { if (MonobitNetwork.isConnect && !MonobitNetwork.inRoom) { MonobitNetwork.JoinRoom(roomName); } } public void LeaveRoom() { if (MonobitNetwork.inRoom) { roomName = null; MonobitNetwork.LeaveRoom(); } } public void Disconnect() { if (MonobitNetwork.isConnect) { isClosing = true; MonobitNetwork.DisconnectServer(); } } private void OnDestroy() { Disconnect(); } private void OnApplicationQuit() { Disconnect(); } public override void OnJoinedLobby() { Debug.Log($"OnJoinedLobby"); if (roomName != null) { JoinRoom(roomName); } } public override void OnJoinedRoom() { Debug.Log($"OnJoinedRoom"); roomName = MonobitNetwork.room.name; } public override async void OnDisconnectedFromServer() { Debug.Log($"OnDisconnectedFromServer"); if (isClosing) { isClosing = false; return; } await Task.Delay(3000); Connect(); } public override async void OnConnectToServerFailed(DisconnectCause cause) { Debug.Log($"OnConnectedToServer"); await Task.Delay(3000); Connect(); } public override void OnCreateRoomFailed(object[] codeAndMsg) { Debug.Log($"OnCreateRoomFailed : errorCode = {codeAndMsg[0]}, message = {codeAndMsg[1]}"); } public override void OnJoinRoomFailed(object[] codeAndMsg) { Debug.Log($"OnJoinRoomFailed : errorCode = {codeAndMsg[0]}, message = {codeAndMsg[1]}"); } }
実行ログ
Connect、AutoJoinLobby、CreateRoom、LeaveRoom、Disconnectした時のログ。
Connect、AutoJoinLobby、CreateRoomして、Unityを停止(アプリを終了)した時のログ。
Connect、AutoJoinLobby、CreateRoomして、ネットワークを切断、復旧した時のログ。(再接続処理)
ネットワークが切断した場合は、OnDisconnectedFromServer
が呼ばれる。 その後オフライン状態が続く場合は、タイムアウト後にOnConnectionFail
とOnDisconnectedFromServer
が呼ばれる。
ネットワークの切断は原因によってKEEPALIVE_TIMEOUT
やperhaps server down
などが発生する。
また、これらはOSに依存するため原因が同じでも発生する内容が異なる場合がある。
タイムアウトはMonobitServerSettings.asset
の Time Settings で設定している値(ms)となる。実際はエラーが発生するまでこの時間の倍±αくらいの時間がかかるようだ。