yotiky Tech Blog

とあるエンジニアの備忘録

Unity - Monobit MUN のクライアント実装

Monobit MUN のクライアント実装をした際の備忘録。
主に official document から重要そうなものの抜粋と補足。 サーバーサイドはノータッチなので省略。

公式ページ

検証環境

  • Unity 2019.4.24f1
  • MUN ver.2.8.0

プログラミング・クイックスタート<シンプルなチャットの作成>

実装目的ならこの辺から読むと良さそう

プログラミング・チュートリアル<オフラインゲームのオンライン化>

MUNクライアントの主要機能

パフォーマンスチューニング

  • ルーム入室中の通信量を抑えるには?
    • 通信負荷を減らすためには1フレーム内における「RPCメッセージ」「オブジェクト同期」「各種ルームパラメータ」の送信量を調整する方法しかありません
    • MUN 2.2.0 以降で実装された「送信帯域制限機能」は、1フレーム内における「RPCメッセージ」「オブジェクト同期」の送信量を簡単に調整することができる
    • RPC
      • RPCメッセージの送信方式
        • 1秒間に MonobitNetwork.sendRate の値だけ送信する
        • RPCメッセージキューに蓄積され、送信タイミングに合わせ、RPCメッセージキューに登録された順に送信されます
      • RPCメッセージの受信方式
        • 受信後に「どのオブジェクトに登録されたRPCのメソッドを呼び出すのか」を検索
        • RPCメッセージの送受信処理において最も負荷が高い
        • 負荷対策は、RPC の受信メソッドの検索回数を減らすこと
      • Tips
        • 受信時に呼び出されるメソッド検索対象の「インスタンス」を少なくする
          • シーン内に存在するすべての MonobitView コンポーネント、および MonoBehaviourの派生インスタンスの実数を出来るだけ少なくする
          • オブジェクトに依存しない情報の同期は、すべて1つの MonoBehaviour を継承したクラスで実装する
        • ある特定のオブジェクト(prefab)の情報同期が目的である場合、RPCメッセージを使わず、OnMonobitSerializeView() を使う
          • ある特定のオブジェクトの情報同期が目的である場合、RPCメッセージによる情報共有は推奨されません
          • MonobitTransformView で送信されるオブジェクトの位置姿勢情報、ならびに MonobitAnimatorView で送信されるオブジェクトのアニメーション情報を除いた属性情報は、すべて OnMonobitSerializeView() の接続コールバックメソッド で実装することが理想的
          • RPCメッセージ本来の同期タイミングについては MonobitNetwork.sendRate に依存しますが、OnMonobitSerializeView() メソッドを利用した場合、MonobitNetwork.updateStreamRate に依存する
    • オブジェクト同期
      • Tips
        • MonobitView コンポーネントが付加されているだけで、オブジェクト同期通信自体が処理されてしまうのでシーン内に存在する「MonobitView コンポーネント」を持つオブジェクトは極力減らす
        • シーン内の静的オブジェクトの中で「MonobitView コンポーネント」を持つオブジェクトは複数用意しない
          • 静的オブジェクトの所有権を持つルームホストにだけ、オブジェクト同期の負荷が掛かる
          • MonobitView コンポーネントを持つシーン静的オブジェクト内は1つだけにする
        • 最も効果的なオブジェクト同期方法は、1つのスクリプト内で OnMonobitSerializeView() の接続コールバックメソッド を実装し、その中にMonobitTransformView や MonobitAnimatorView のコンポーネントの代替実装を行なう
    • カスタムパラメータ
      • ルームカスタムパラメータ、およびプレイヤーカスタムパラメータによる送受信処理が現行最速
        • メリット
          1. ルームカスタムパラメータ、およびプレイヤーカスタムパラメータは、命令の実行から実際に送信されるまでの遅延は送信命令実行フレームから1フレーム以内である。
          2. ルームカスタムパラメータ、およびプレイヤーカスタムパラメータは、受信から実際の値反映までにタイムラグが発生しにくい。
        • デメリット
          • 全てのパラメータを送受信しあう
          • 全てのルーム内プレイヤーが共有する情報
          • 全てのルーム内プレイヤーが自由に変更できる情報
        • 原則
          • できる限り変更頻度の低い、かつ、膨大ではない情報の送受信
          • ルーム内プレイヤー全員に対し常に共有される情報の送受信
          • 悪質なプレイヤーからの情報改ざん(チート)に伴うシステムの弊害が発生しないような情報の送受信

MUN サーバについて

補足

  • MonobitViewコンポーネント

    • 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した時のログ。

f:id:yotiky:20210715014004p:plain

Connect、AutoJoinLobby、CreateRoomして、Unityを停止(アプリを終了)した時のログ。

f:id:yotiky:20210715014924p:plain

Connect、AutoJoinLobby、CreateRoomして、ネットワークを切断、復旧した時のログ。(再接続処理)

f:id:yotiky:20210715015223p:plain

ネットワークが切断した場合は、OnDisconnectedFromServerが呼ばれる。 その後オフライン状態が続く場合は、タイムアウト後にOnConnectionFailOnDisconnectedFromServerが呼ばれる。

ネットワークの切断は原因によってKEEPALIVE_TIMEOUTperhaps server downなどが発生する。 また、これらはOSに依存するため原因が同じでも発生する内容が異なる場合がある。

タイムアウトMonobitServerSettings.assetの Time Settings で設定している値(ms)となる。実際はエラーが発生するまでこの時間の倍±αくらいの時間がかかるようだ。

f:id:yotiky:20210803085010p:plain