「自身がホストであるクライアント」だけが動かすようにする

目次

  概要
  敵出現処理に対しMonobitEngine.MonobitNetwork.isHostを適用する
  敵キャラクタを操作している箇所を探す
  NPC_PatrolにMonobitEngine.MonobitNetwork.isHostを適用する
  NPC_ShootPlayerにも同様の処置を適用する
  NPC_DamageReceiverにも同様の処置を適用する
  敵死亡時の同期制御


概要

なぜ「敵キャラクタの動作」に不一致が出てしまったのか?

  「オフラインゲーム開発」と「オンラインゲーム開発」の、もう1つの大きな障壁にぶつかりました。
  なぜ「敵キャラクタの動作」において不一致が発生してしまったのでしょうか?

  答えは「各々のクライアントで、別々の敵キャラクタの挙動ロジックを動かしているから」です。
  それでは一致するわけがありませんよね。

  通常のサーバ/クライアント型のネットワークであれば、この解決方法として
  「敵キャラクタなど、非プレイヤーの挙動については、すべてサーバサイドで動作させる」ことで解決させるのですが、
  MUN は完全なクライアントサイドの通信エンジンです。上記のような対応はできません。

ネットワークゲーム上の「管理役」として、ホストを利用する

  クライアントサイドの通信の場合、非プレイヤーの挙動については「ホスト」に任せるのが一般的です。
  現在のサンプルには、この「ホストに任せる」処理が入っていませんので、これを導入していきましょう。
ホストとは、ルームの「管理役」として定義されるもので、
ルーム内のクライアントのうちのいずれか1つのクライアントが、サーバより選ばれます。
(原則的に、ルーム作成者が最初のホストとして選ばれます。)

ホストの役割には、非プレイヤーの挙動管理の他に、
ゲームルールの監視(制限時間の管理、スコアの管理、勝敗の管理、etc...)などがあります。


敵出現処理に対しMonobitEngine.MonobitNetwork.isHostを適用する

敵出現に関し、ホスト側で管理するようにロジックを組みなおす

  実は、敵の出現に関しても、ロジックを組みなおす必要があります。
  というのも、先ほどのサンプルまでは分かりづらかったかも知れませんが、
  各々のクライアントで、ネットワーク越しに登場させていたため、ルームに入室しているプレイヤー人数に応じて、敵キャラクタが増えています。
1クライアントにつき50体表示させるので、ルーム内のプレイヤー人数×50体のテディベアが襲ってきていました。

  これはこれでゲームバランスが取れるのかもしれませんが、意図した動きではないので修正しましょう。

NPC_Spawner で管理している「Spawn」を開く

  まずは Hierarchy から NPC_Spawner をクリックします。
  NPC_Spawner に含まれている Spawner のスクリプトを、ダブルクリックして開きます。

該当するクラスの Update() について、MonobitEngine.MonobitNetwork.isHost による実行可否判定を入れる

  Spawner の 17 行目から、以下のコードを記述します。
		// ホスト以外は処理をしない
		if( !MonobitEngine.MonobitNetwork.isHost )
		{
			return;
		}
  MonobitEngine.MonobitNetwork.isHost が、「自身がホストか?」を示す、ホスト権限のフラグです。

  ここでは単純に、「ホスト権限を持たないクライアントでは、Update() を処理させない」というロジックを適用させます。

該当するクラスの Update() で実行している newNPC に対する設定箇所を変更する

  更に Spawner の 35 行目~37行目(以下の赤枠部分)について、コードから削除します。
  この設定箇所については、
     ・NPC_Spawner の子として NPC を生成する(位置情報の相対位置が NPC_Spawner に依存する)
     ・NPC をアクティブにする
  という処理ですが、これは MonobitNetwork.isHost 以外のユーザーに対しても、
  プレハブが Instantiate された後で適用しなければなりません。

  先の条件分岐で処理が除外されてしまいますので、ここで設定するのを断念し、
  別の場所に定義するようにします。

全てのクライアントでキャラクタの数を同期する

  このままではホスト以外、m_CurrentSpawnCount が更新されなくなってしまいますので、更に調整を加えます。
  まず Spawner の 15 行目に以下のコードを記述します。
	public void Start()
	{
		m_CurrentSpawnCount = transform.GetChild(0).childCount;
	}
  NPCオブジェクトの生成場所(親オブジェクト)に登録されている子オブジェクトの数を m_CurrentSpawnCount に代入します。

  この上で、 Spawner の 28行目に記述されている「出現条件」を以下のように変更します。
		if (m_IsSpawning && transform.GetChild(0).childCount < m_CurrentSpawnCount + SpawnCount)
  NPCの出現個数を SpawnCount に設定した値を超えないよう、
  ネットワーク越しに作成されたオブジェクトに対してもきちんと同期するように制御します。
ネットワークで同期するのは(GetChildを使用するよりも)全体的な高負荷や遅延が発生しますので、こういったテクニックを用います。


敵キャラクタを操作している箇所を探す

敵キャラクタのプレハブから探る

  続けて、敵キャラクタのプレハブを見てみましょう。
  Assets/Resources フォルダを開き、 NPC.prefab の右端にある「>」のマークをクリックします。
  すると、player.prefab の中身が展開されます。
  展開されたデータのうち、「Bear」と書かれたオブジェクトを選択します。

プレハブに登録されているコンポーネントから探る

  「Bear」に登録されているコンポーネントが Inspector に表示されます。
  プレイヤー同様に、色々な敵キャラクタ制御スクリプトが用意されているようです。


NPC_PatrolにMonobitEngine.MonobitNetwork.isHostを適用する

「Bear」のInspectorから「NPC_Patrol」を開く

  まずは NPC_Patrol のスクリプトをダブルクリックして開いてみましょう。

該当するクラスの Start() について、Spawner.Update() で削除した処理の代替を組み込む

  ここで先ほど Spawner の Update() メソッドで削除した処理について、その代替処理を Start() に入れましょう。
  NPC_Patrol の 28 行目から、以下のコードを記述します。
		transform.parent.parent  = GameObject.Find("NPC_Spawner").transform.GetChild(0).transform;
		gameObject.SetActive(true);
  実行タイミングが異なりますが、処理内容としては同じことをしています。

該当するクラスの Update() について、MonobitEngine.MonobitNetwork.isHost による実行可否判定を入れる

  更に、NPC_Patrol の 37 行目から、以下のコードを記述します。
		// ホスト以外は処理をしない
		if( !MonobitEngine.MonobitNetwork.isHost )
		{
			return;
		}
  「ホスト権限を持たないクライアントでは、Update() を処理させない」というロジックを適用させます。


NPC_ShootPlayerにも同様の処置を適用する

「Bear」のInspectorから「NPC_ShootPlayer」を開く

  同様の処置を他のスクリプトにも適用させます。
  「Bear」の Inspector から、NPC_ShootPlayer のスクリプトをダブルクリックして開いてみましょう。

該当するクラスの Update() について、MonobitEngine.MonobitNetwork.isHost による実行可否判定を入れる

  NPC_Shooter の 36 行目から、以下のコードを記述します。
		// ホスト以外は処理をしない
		if( !MonobitEngine.MonobitNetwork.isHost )
		{
			return;
		}
  「ホスト権限を持たないクライアントでは、Update() を処理させない」というロジックを適用させます。


NPC_DamageReceiverにも同様の処置を適用する

「Bear」のInspectorから「NPC_DamageReceiver」を開く

  「Bear」の Inspector から、NPC_DamageReceiver のスクリプトをダブルクリックして開いてみましょう。

該当するクラスの Update() について、MonobitEngine.MonobitNetwork.isHost による実行可否判定を入れる

  NPC_DamageReceiver の 25 行目から、以下のコードを記述します。
		// ホスト以外は処理をしない
		if( !MonobitEngine.MonobitNetwork.isHost )
		{
			return;
		}
  「ホスト権限を持たないクライアントでは、Update() を処理させない」というロジックを適用させます。


敵死亡時の同期制御

RPCメッセージを送信するために、MonobitEngine.MonoBehaviour を継承する

  敵の死亡タイミングを同期するために、RPCメッセージを送信します。
  RPC については こちら を参照してください。

  NPC_DamageReceiver の 7 行目 について、以下のコードに変更します。
public class NPC_DamageReceiver : MonobitEngine.MonoBehaviour {
  スクリプトからRPCメッセージを送信するためには、最低限以下の条件を満たす必要があります。
1) スクリプトをアタッチしている GameObject に、MonobitView コンポーネントが追加されていること。
2) スクリプトが MonobitEngine.MonoBehaviour を継承していること。
  1) の条件についてはすでに満たしていますので、ここでは 2) を実践します。

敵死亡のタイミングに合わせて RPC メッセージを発信する

  続けて、NPC_DamageReceiver の 34 行目を、以下のコードに変更します。
            monobitView.RPC("CharacterControllerOff", MonobitEngine.MonobitTargets.All, null);
  敵が死亡した後に「キャラクタコントローラを無効化する」処理が入っている箇所について、前章のチャットの際にも説明した「monobitView.RPC」を利用します。
  (ソース内に記された monobitView は、MonobitViewコンポーネント本体を取得するプロパティです。)

  このRPCメッセージの送信については、「ルーム内のプレイヤー全員」に対して実行するようにします。
  また、特に送信時に渡さなければならないパラメータはありませんので、ここでは null を代入しておきます。

  この上で、NPC_DamageReceiver の 45 行目以降に、以下のコードを追加します。
    [MunRPC]
    void CharacterControllerOff()
    {
        GetComponent<CharacterController>().enabled = false; // disable collision when dead.
    }
  これも前章のチャットの際にも説明しましたが、monobitView.RPC() メソッドを使ったRPCメッセージの送信に対し、
  受信する関数を以下のように定義します。
・メソッド名の接頭に[MunRPC]のアトリビュートを付記すること。
・monobitView.RPC() の第一引数と同じ名前のメソッド名で定義すること。
・monobitView.RPC() の第三引数以降に対応するデータ型の引数値を定義すること。
 (今回は引数にnullを指定しているので、引数は無し)
  先ほど送信側の関数名に「CharaterControllerOff」と名付けましたので、ここでは同名のメソッド名で記述します。
  記述する内容は、先ほど monobitView.RPC によって書き換えられた処理を組み込みます。