[Tips] RPCメッセージの送信量・頻度の軽減

目次

  概要
  送受信ロジックについて
  Tips(1) : シーン内の情報同期が目的である場合、MonobitView コンポーネントを持つ、単独の静的シーンオブジェクトでRPCメッセージをやり取りする
  Tips(2) : ある特定のオブジェクト(prefab)の情報同期が目的である場合、RPCメッセージを使わず、OnMonobitSerializeView() を使う


概要

RPCメッセージに関するパフォーマンス改善方法について

  ここでは、MUN における RPCメッセージの送受信処理にてパフォーマンスの低下が感じられる場合、
  MUN クライアント側で対処可能なテクニックについて紹介します。

  当然のことながら、RPCメッセージの送信データを削減すればパフォーマンスは向上します
  ここではそれ以外の、RPC送受信に伴うスパイク(大きな負荷)への対策として、効果的な手段を触れたいと思います。


送受信ロジックについて

RPCメッセージの送信方式

  MUN のRPCメッセージ送信は、MonobitNetwork.sendRate に設定された値に基づいて実行されます。
  (MonobitNetwork.sendRate の初期値は20で、1秒間に20回送信します)

  設定された送信間隔までの間、各種RPC送信命令にて実行されたRPCメッセージはRPCメッセージキューに蓄積され、
  送信タイミングに合わせ、RPCメッセージキューに登録された順に送信されます。
 MUN 2.2.0 以降の場合、RPCの送信帯域制限以上のRPCメッセージデータについては、次フレームまで送信が保留されます。

  MUN クライアント側から送信されたRPCメッセージは mun_room サーバが受信し、受信を受け取った段階で
  即時他の MUN クライアントにリレー送信します。

RPCメッセージの受信方式

  RPCメッセージを受信した MUN クライアント側では、受信後に「どのオブジェクトに登録されたRPCのメソッドを呼び出すのか」を検索します。
  この検索処理は以下のようなロジックで実行されるため、RPCメッセージの送受信処理において最も負荷が高くなってしまいます

  RPCメッセージによる負荷対策として最も効果的なのは、RPC の受信メソッドの検索回数を減らすことです。
		// シーン内の全てのMonoBehaviourからRPCメソッドを探索してコルーチンを実行する
		//     methodName       - RPCメソッド名
		//     methodArgs       - RPCメソッドパラメータの型
		//     methodParams     - RPCメソッドパラメータの実値
		//     behaviourInScene - シーン内に存在する、すべての MonobitView コンポーネントと同列に存在する MonoBehaviourの派生インスタンス
		public void ReceiveRpcMethod(string methodName, Type[] methodArgs, object[] methodParams, MonobitEngine.MonoBehaviour[] behaviourInScene)
		{
			foreach(MonoBehaviour behaviour in behaviourInScene)
			{
				Type monoType = behaviour.GetType();
				List<MethodInfo> methodList;

				// シーン内の全てのMonoBehaviourから[MunRPC]のアトリビュート付きのメソッドを探索する
				var result = MethodsCache.TryGetValue(monoType, out methodList);
				if (result != true)
				{
					MethodInfo[] methods = monoType.GetMethods(BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic);
					foreach (MethodInfo method in methods)
					{
						if (method.IsDefined(typeof(MunRPC), false))
							methodList.Add(method);
					}
					MethodsCache[monoType] = methodList;
				}
				if (methodList == null || methodList.Count == 0)
					continue;

				// メソッド名が同一であるものを抽出する
				var selectInfo = methodList.Where(m => m.Name.Equals(methodName));
				if (selectInfo == null) continue;

				// パラメータが同一であるものを抽出し、コルーチンを実行する
				foreach (MethodInfo info in selectInfo)
				{
					var paramInfo = info.GetParameters();
					if (paramInfo.Length == methodParams.Length && VerifyArgType(paramInfo, methodArgs))
					{
						object retValue = info.Invoke(behaviour, methodParams);
						if (retValue is System.Collections.IEnumerator)
						{
							behaviour.StartCoroutine(retValue as System.Collections.IEnumerator);
						}
					}
				}
			}
		}


Tips(1) : シーン内の情報同期が目的である場合、MonobitView コンポーネントを持つ、
      単独の静的シーンオブジェクトでRPCメッセージをやり取りする

受信時に呼び出されるメソッド検索対象の「インスタンス」を少なくする

  RPC の受信メソッドの所持候補となる、シーン内に存在するすべての MonobitView コンポーネント、
  および MonoBehaviourの派生インスタンスの実数を出来るだけ少なくする ことがまず第一の目標です。

  あるシーン内で、オブジェクトに依存しない情報の同期、例えば
・スコアやタイマーなどの数値計算、およびそのデータ共有
・NPCやアイテムなどの出現管理(スポーン処理)
・チャットの会話文の送受信
  といった機能については、すべて1つの MonoBehaviour を継承したクラスで実装することが肝要です。
  複数の MonoBehaviour を継承したクラスで実装することは、パフォーマンス低下につながります。
【避けるべきこと】
  × 1つのシーンに対し、「MonobitView コンポーネントを持つ静的オブジェクト」を2つ以上作る
 × 単独または複数の静的オブジェクトに対し、「MonoBehaviour を継承したクラス(スクリプト)」を2つ以上コンポーネントに持たせる
 × 単独または複数のMonoBehaviourに対し、「RPC 受信メソッド」を2つ以上作る
【推奨されるべきこと】
  〇 1つのシーンに対し、「MonobitView コンポーネントを持つ静的オブジェクト」を1つだけ作る
 〇 単独または複数の静的オブジェクトに対し、「MonoBehaviour を継承したクラス(スクリプト)」を1つだけコンポーネントに持たせる
 〇 単独または複数のMonoBehaviourに対し、「RPC 受信メソッド」を1つだけ作る
機能ごとにオブジェクトを分割し、各々で MonobitView コンポーネントを追加し、さらに各々のオブジェクトで
RPC メッセージを別々に呼び出す方法です。

プログラムの可読性や運用性のメリットはあるにしても、MUN のパフォーマンス向上のためにはあまり推奨されるやり方ではありません。
出来ることであれば、このような方法は避けるべきです。
同一の機能のまま、1つのシーンで1つの静的オブジェクト、1つの MonobitView、1つの MonoBehaviour で
RPCメッセージを送受信する方法です。

プログラム的には冗長になりますが、上記の悪例に比べて、単純比較で3倍のパフォーマンス向上が見込まれます。
同一の機能のまま、1つのシーンで1つの静的オブジェクト、1つの MonobitView、1つの MonoBehaviour で
さらに1つのRPCメッセージを送受信する方法です。
  ※ 省略していますが、Hierarchy および Inspector は上述の「単独の MonoBehaviour を継承したクラスで実装する事例」と同一です。

プログラム的には複雑になりますが、上記の例よりもさらにパフォーマンス向上が見込まれます。


Tips(2) : ある特定のオブジェクト(prefab)の情報同期が目的である場合、
      RPCメッセージを使わず、OnMonobitSerializeView() を使う

オブジェクトの位置情報以外のデータ同期について、RPCメッセージによるオーバーヘッドから脱する

  一方、ある特定のオブジェクトの情報同期が目的である場合、RPCメッセージによる情報共有は推奨されません
  もちろん RPC メッセージによるデータ共有は可能ですが、情報量と伝送効率を考えた場合、RPCメッセージを用いるのは効果的ではありません。

  MonobitTransformView で送信されるオブジェクトの位置姿勢情報、ならびに MonobitAnimatorView で送信される
  オブジェクトのアニメーション情報を除いた
・オブジェクト単体のステータス(体力値や攻撃力などの、一般的なキャラステータスデータ)
・キャラクタの特定操作に伴う挙動フラグなど
  の情報については、すべて OnMonobitSerializeView() の接続コールバックメソッド で実装することが理想的です。
  RPCメッセージで送信するのは、パフォーマンスチューニングから考えた場合、あまり効率的な対応方法ではありません。
【避けるべきこと】
  × オブジェクトの特定情報の同期について RPC メソッドを使う
【推奨されるべきこと】
  〇 オブジェクトの特定情報の同期については OnMonobitSerializeView() メソッドを使う
  なお、RPCメッセージ本来の同期タイミングについては MonobitNetwork.sendRate に依存しますが、
  OnMonobitSerializeView() メソッドを利用した場合、MonobitNetwork.updateStreamRate に依存します。
  あまり致命的な問題にはなりませんが、同期されるタイミングが本来の RPC メッセージと異なる場合がありますので注意してください。