弾丸の同期制御

目次

  概要
  敵キャラクタの弾丸の「標的対象」を設定する
  敵の「弾」の発射タイミングを同期する
  プレイヤーの「弾」の発射タイミングを同期する
  プレイヤーの「弾」の発射する向きを同期する
  変更したプログラムの保存


概要

弾は圧倒的に数が多いので、「常時同期オブジェクト」には不適

  いよいよ終盤です。弾の同期処理について触れていきましょう。

  弾もプレハブから作成するため、「常時同期オブジェクト」として各種スクリプトを登録し、
  MonobitNetwork.Instantiate() にて生成するようにすれば比較的楽に同期処理を行なうことも可能です。

  ただし、そもそも一般的なネットワークゲームの場合、こういった「弾」に代表されるような、
  圧倒的に大量のオブジェクトが登場するものに「常時同期させる」処理を加えると、通信量の負荷過多は避けられません。

「弾を発射する瞬間」のみ同期させ、あとはクライアント制御に任せる

  一般的なオンラインゲームのデザインの場合、「弾を発射する瞬間」のみを通信で同期させ、
  あとは各自クライアント制御の挙動に任せる、という手法を取る場合が多いです。
  よって、今回もその手法を採用します。
なお、当然のことながら、「クライアントに任せる=ある程度のタイムラグを許容した作りにする」必要があります。
その手法については、ゲームシステムの調整によって異なる話ですので、今回は割愛します。


敵キャラクタの弾丸の「標的対象」を設定する

敵キャラクタの「弾」の標的対象を「ルーム内にいる全プレイヤー」にする

  前回複数クライアントで実行した際に、敵キャラクタの弾が、
  「動作させているクライアントのプレイヤー」しか狙わない、という現象が発生していました。

  この現象は、そもそも「Mecanim GDC2013 Sample Project」がオフライン専用で作成されているからです。
  同期処理を組む前に、先に敵の「弾」の標的対象を「ルーム内にいる全プレイヤー」にしましょう。

敵キャラクタのプレハブから「Bear」を選択する

  敵の弾の標的を設定しているのは、 Bear オブジェクトの中の NPC_ShootPlayer コンポーネントです。
  まずは「Bear」オブジェクトを展開し、このコンポネントを開きましょう。

  Bear オブジェクトの選択方法 Unity 2018.2 以前と Unity2018.3 以降でインタフェースが異なりますので、
  お使いの Unity のバージョン に合わせて、以下の項目を選択して進めてください。

 Assets/Resources フォルダを開き、 NPC.prefab の右端にある「>」のマークをクリックします。
  すると、NPC.prefab の中身が展開されます。
  展開されたデータのうち、「Bear」と書かれたオブジェクトを選択します。



  まず Assets/Resources フォルダーをクリックして選択し、その中にある NPC.prefab を選択します。
  NPC の Inspector にある「Open Prefab」のボタンを押します。
  すると Hierarchy の表示箇所に NPC.prefab がリスト化されて表示されますので、
  親オブジェクト「NPC」の左隣にある三角マークをクリックし、子オブジェクトの「Bear」を選択します。

弾発射の標的対象が「ルーム内のプレイヤー全員」になるようにアルゴリズムを変更する

  「Bear」に登録されているコンポーネントのうち、NPC_ShootPlayer をダブルクリックで開きます。
  では実際に変更を加えましょう。
  NPC_ShootPlayer.cs の 20 行目~ 21 行目に宣言されている変数について、配列型に変更します。
	GameObject[] 	m_Player = null;	 	
	Animator[]	 	m_PlayerAnimator = null;
	int			selectedPlayer = 0;
	Vector3		selectedPos = Vector3.zero;
  複数のプレイヤーに対応するために、プレイヤー参照変数を配列式にします。
  また、弾の照準として情報を保持するために、変数 selectedPlayer, selectedPos を追加します。

  修正の結果、NPC_ShootPlayer.cs の Start() メソッドにエラーが発生しますので、
  28 行目以降の Start() メソッドについて、以下のように処理を変更します。
	void Start () 
	{
		m_Animator = GetComponent<Animator>();
		m_Animator.logWarnings = false; // so we dont get warning when updating controller in live link ( undocumented/unsupported function!)
		m_Player = GameObject.FindGameObjectsWithTag("Player");
		m_PlayerAnimator = new Animator[m_Player.Length];
		for( int i = 0; i < m_Player.Length; i++ )
		{
			m_PlayerAnimator[i] = m_Player[i].GetComponent<Animator>();
		}
	}
  複数プレイヤーに対応できるように「Player」タグの追加キャラクタを複数取得し、そのAnimator情報を取得します。

  また、NPC_ShootPlayer.cs の ShouldShootPlayer() メソッドもエラーとなっていますので、
  69 行目以降の ShouldShootPlayer() メソッドについて、以下のように処理を変更します。
	bool ShouldShootPlayer()
	{
		// ルーム入室中のみ、プレイヤー情報の更新を行なう
		if(MonobitEngine.MonobitNetwork.isConnect && MonobitEngine.MonobitNetwork.inRoom)
		{
			if(m_Player.Length != MonobitEngine.MonobitNetwork.room.playerCount)
			{
				// プレイヤー情報の再取得
				m_Player = GameObject.FindGameObjectsWithTag("Player");
				m_PlayerAnimator = new Animator[m_Player.Length];
				for (int i = 0; i < m_Player.Length; i++)
				{
					m_PlayerAnimator[i] = m_Player[i].GetComponent<Animator>();
				}
			}
		}

		for (int i = 0; i < m_Player.Length; i++)
		{
			float distanceToPlayer = Vector3.Distance(m_Player[i].transform.position, transform.position);
			if (distanceToPlayer < m_AttackDistance)
			{
				AnimatorStateInfo info = m_PlayerAnimator[i].GetCurrentAnimatorStateInfo(0);
				// real bears don't shoot at dead player!
				if (!info.IsName("Base Layer.Dying") && !info.IsName("Base Layer.Death") && !info.IsName("Base Layer.Reviving"))
				{
					if(selectedPlayer < 0 )
					{
						selectedPlayer = i;
						selectedPos = m_Player[i].transform.position;
					}
					return true;
				}
			}
		}

		return false;		
	}

  NPC_ShootPlayer.cs の 118行目付近にある OnAnimatorMove() メソッド内のエラーも、
  m_Player.transform.position を selectedPos に置き換え、以下のように変更します。
	void OnAnimatorMove()
	{
		if(CheatRoot)
		{
			if(!enabled || !GetComponent<CharacterController>().enabled) return;
			
			//  Cheat the root to align to player target.
			if(m_Animator.GetBool("Shoot"))
			{
                if (selectedPlayer >= 0)
                {
                    m_LookAtPosition.Set(selectedPos.x, transform.position.y, selectedPos.z); // Kill Y.
                }

                transform.rotation = Quaternion.Lerp(transform.rotation, Quaternion.LookRotation(m_LookAtPosition-transform.position), Time.deltaTime * 5);
				m_Animator.rootRotation =  transform.rotation;
			}							
		
			GetComponent<CharacterController>().Move(m_Animator.deltaPosition);					
			transform.rotation = m_Animator.rootRotation;
		
			ForceOnFloor();		
		}
	}


  最後に、NPC_ShootPlayer.cs の 144 行目付近にある SpawnBullet() メソッド内のエラーも、同様に書き換えます。
	void SpawnBullet() 
	{
		GameObject newBullet = Instantiate(Bullet, BulletSpawnPoint.position , Quaternion.Euler(0, 0, 0)) as GameObject;		  										
		Destroy(newBullet, m_BulletDuration);
        Vector3 direction = selectedPos - BulletSpawnPoint.position;
		direction.y = 0;
		newBullet.GetComponent<Rigidbody>().velocity = Vector3.Normalize(direction)* m_BulletSpeed;								
		if(BulletParent)newBullet.transform.parent = BulletParent;
		m_HasShootInCycle = true;				
	}


敵の「弾」の発射タイミングを同期する

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

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

  まずは RPCメッセージを受信するため、NPC_ShootPlayer.cs の 7 行目付近に以下のコードを追加します。
public class NPC_ShootPlayer : MonobitEngine.MonoBehaviour {
  RPCメッセージを受信するためには、最低限 MonobitEngine.MonoBehaviour を継承している必要があります。

RPCを制御するクラス内に、MonobitView コンポーネント用のフィールドを用意する

  更に、RPC を送信するためには MonobitView のコンポーネント情報が必要ですので、これを用意しましょう。
  NPC_ShootPlayer.cs の 9 行目付近に、以下のフィールドを追加してください。
    // MonobitView コンポーネント
    MonobitEngine.MonobitView m_MonobitView = null;
  前述までと同様、MonobitView コンポーネント本体の参照のための変数を用意します。

該当するクラスの Awake() で、MonobitView コンポーネントの情報を取得する

  更に、NPC_ShootPlayer.cs の 31 行目付近に、以下の Awake() メソッドのコードを追加します。
    void Awake()
    {
        // すべての親オブジェクトに対して MonobitView コンポーネントを検索する
        if (GetComponentInParent<MonobitEngine.MonobitView>() != null)
        {
            m_MonobitView = GetComponentInParent<MonobitEngine.MonobitView>();
        }
        // 親オブジェクトに存在しない場合、すべての子オブジェクトに対して MonobitView コンポーネントを検索する
        else if (GetComponentInChildren<MonobitEngine.MonobitView>() != null)
        {
            m_MonobitView = GetComponentInChildren<MonobitEngine.MonobitView>();
        }
        // 親子オブジェクトに存在しない場合、自身のオブジェクトに対して MonobitView コンポーネントを検索して設定する
        else
        {
            m_MonobitView = GetComponent<MonobitEngine.MonobitView>();
        }
    }

  ここも前述までにも説明した通り、親オブジェクトに存在する MonobitView を検索し、
  そのコンポーネント情報を m_MonobitView に格納します。

敵の「弾」の発射タイミングに合わせてRPCメッセージを送信する

  NPC_ShootPlayer.cs の 79 行目 に記述している SpawnBullet() メソッドの呼び出しについて、以下のコードに変更します。
                     //SpawnBullet();							
                    m_MonobitView.RPC("SpawnBullet", MonobitEngine.MonobitTargets.All, selectedPos);
  弾を投げる瞬間に、SpawnBullet() の実行を「ルーム内のプレイヤー全員」に対して実行するようにします。

  今回「弾を投げる先(selectedPos)」については、ホストが保持しますので、
  ルーム内にいるプレイヤー全員に対し、弾発射命令と同時に送信します。

SpawnBullet() 関数を「RPC受信関数」に書き換える

  NPC_ShootPlayer の 146 行目 から記述している SpawnBullet() メソッドを、「RPC受信関数」に書き換えます。
    // Spawns bullet
    [MunRPC]
    void SpawnBullet(Vector3 targetPos)
    {
        GameObject newBullet = Instantiate(Bullet, BulletSpawnPoint.position, Quaternion.Euler(0, 0, 0)) as GameObject;
        Destroy(newBullet, m_BulletDuration);
        Vector3 direction = targetPos - BulletSpawnPoint.position;
        direction.y = 0;
        newBullet.GetComponent<Rigidbody>().velocity = Vector3.Normalize(direction) * m_BulletSpeed;
        if (BulletParent) newBullet.transform.parent = BulletParent;
        m_HasShootInCycle = true;
        selectedPlayer = -1;
    }
  MonobitView.RPC() メソッドを使ったRPCメッセージの送信に対し、弾を投げる処理 SpawnBullet の「受信関数化」を行います。
  今回送信側(ホスト)で「弾を投げる先」を設定していますので、targetPos を引数として受け取り、その座標目がけて投げるように処理を変更します。


プレイヤーの「弾」の発射タイミングを同期する

プレイヤーキャラクタのプレハブから「Dude」を選択する

  敵キャラクタと同様に、プレイヤーのバズーカの「弾」を発射するタイミングについても、各クライアントで同期するように組み込みます。

  まずは、プレイヤーキャラクタのプレハブから、
  プレイヤーキャラクタの操作メソッドが含まれている「Dude」オブジェクトを選択しましょう。

  この箇所は Unity 2018.2 以前と Unity2018.3 以降でインタフェースが異なりますので、
  お使いの Unity のバージョン に合わせて、以下の項目を選択して進めてください。

 Assets/Resources フォルダを開き、 Player.prefab の右端にある「>」のマークをクリックします。
  すると、player.prefab の中身が展開されます。
  展開されたデータのうち、「Dude」と書かれたオブジェクトを選択します。



  まず Assets/Resources フォルダーをクリックして選択し、その中にある Player.prefab を選択します。
  Player の Inspector にある「Open Prefab」のボタンを押します。
  すると Hierarchy の表示箇所に Player.prefab がリスト化されて表示されますので、
  親オブジェクト「Player」の左隣にある三角マークをクリックし、子オブジェクトの「Dude」を選択します。

プレイヤーの「弾」の発射を制御しているスクリプトを開く

  バズーカの「弾」発射を管理しているスクリプトを開きましょう。
  「Dude」のInspector に 「Player_Shoot」 コンポーネントがありますので、このスクリプトファイルをダブルクリックしましょう。

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

  こちらも発射タイミングを同期するために、RPCメッセージを送信します。

  それと同時にRPCメッセージを受信するため、Player_Shoot.cs の 7 行目付近のクラス定義文について
  MonobitEngine.MonoBehaviour を継承するように変更を加えましょう。
public class NPC_ShootPlayer : MonobitEngine.MonoBehaviour {

プレイヤーの弾を発射する「RPC受信関数」を作成する

  プレイヤーの弾の発射処理は、Player_Shoot の 41 行目 ~ 47 行目で行っています。
  このコードと同じ処理を、「RPC 受信関数」としてまとめます。
  Player_Shoot.cs の 23 行目付近から、以下のメソッドを追記してください。
	[MunRPC]
	void SpawnBazookaBullet()
	{
		GameObject newBullet = Instantiate(Bullet, BulletSpawnPoint.position, Quaternion.Euler(0, 0, 0)) as GameObject;
		Destroy(newBullet, m_BulletDuration);
		newBullet.GetComponent<Rigidbody>().velocity = -BulletSpawnPoint.forward * m_BulletSpeed;
		newBullet.GetComponent<DamageProvider>().SetScaleBullet();
		newBullet.SetActive(true);

		if (BulletParent) newBullet.transform.parent = BulletParent;
	}

プレイヤーの「弾」の発射タイミングに合わせてRPCメッセージを送信する

  あとは、プレイヤーの弾の発射処理のみです。
  先ほど示した、Player_Shoot.Update() メソッド内に記述されていた箇所を「RPC送信関数」に置き換えます。

  Player_Shoot の 75 行目について、以下のように書き換えます。
m_MonobitView.RPC("SpawnBazookaBullet", MonobitEngine.MonobitTargets.All, null);


プレイヤーの「弾」の発射する向きを同期する

プレイヤーの「弾」の発射方向を制御しているスクリプトを開く

  更に、プレイヤーの「弾」の発射方向ついても、各クライアントで同期するように組み込みます。

  バズーカの「弾」の発射方向を管理しているスクリプトを開きましょう。
  「Dude」のInspector から、「LookAhead」をダブルクリックしてください。

該当するクラスが MonobitEngine.MonoBehaviour を継承するように処理を変更する

  まずは LookAhead.cs の 10 行目付近にあるクラス定義文を、以下のコードに変更します。
public class LookAhead : MonobitEngine.MonoBehaviour {
  何度も繰り返しになりますが、RPCメッセージを受信するためには、最低限 MonobitEngine.MonoBehaviour を継承している必要があります。

RPCを制御するクラス内に、MonobitView コンポーネント用のフィールドを用意する

  更に、RPC を送信するためには MonobitView のコンポーネント情報が必要ですので、これを用意しましょう。
  LookAhead.cs の 12 行目付近に、以下のフィールドを追加してください。
    // MonobitView コンポーネント
    MonobitEngine.MonobitView m_MonobitView = null;
  前述までと同様、MonobitView コンポーネント本体の参照のための変数を用意します。

該当するクラスの Awake() で、MonobitView コンポーネントの情報を取得する

  更に、LookAhead.cs の 19 行目付近に、以下の Awake() メソッドのコードを追加します。
    void Awake()
    {
        // すべての親オブジェクトに対して MonobitView コンポーネントを検索する
        if (GetComponentInParent<MonobitEngine.MonobitView>() != null)
        {
            m_MonobitView = GetComponentInParent<MonobitEngine.MonobitView>();
        }
        // 親オブジェクトに存在しない場合、すべての子オブジェクトに対して MonobitView コンポーネントを検索する
        else if (GetComponentInChildren<MonobitEngine.MonobitView>() != null)
        {
            m_MonobitView = GetComponentInChildren<MonobitEngine.MonobitView>();
        }
        // 親子オブジェクトに存在しない場合、自身のオブジェクトに対して MonobitView コンポーネントを検索して設定する
        else
        {
            m_MonobitView = GetComponent<MonobitEngine.MonobitView>();
        }
    }

  ここも前述までにも説明した通り、親オブジェクトに存在する MonobitView を検索し、
  そのコンポーネント情報を m_MonobitView に格納します。

プレイヤーの弾の発射方向を変数で管理し、初期化設定を行なう

  一方で、LookAhead.cs の 19 行目付近に、以下のコードを追加してください。
    Vector3 lookAheadPosition;
  弾の発射方向を設定する変数 lookAheadPosition を用意します。

  この変数を Start メソッド内で初期化しましょう。
  LookAhead の 44 行目に、以下のコードを追加してください。
        lookAheadPosition = HeadTransform.position + (HeadTransform.forward * 10);
  Inspectorで登録された HeadTransform(プレイヤーキャラクタの頭のスケルトン)の位置・方向に合わせて初期値を設定します。

プレイヤーの「弾」の発射方向を設定する「RPC受信関数」を用意する

  上記で用意した lookAheadPosition を、RPCの受信によって書き換える関数を用意します。
  LookAhead の 21 行目に、以下のコードを追加してください。
    [MunRPC]
    void UpdateLookAhead(Vector3 position)
    {
        lookAheadPosition = position;
    }

プレイヤーの「弾」の発射方向を設定する「RPC送信関数」を用意する

  最後に、プレイヤーの「弾」の発射方向を設定する「RPC送信関数」を用意します。
  少々複雑ですが、LookAhead.cs 内の OnAnimationIK() メソッド内に発射制御がありますので、そこに組み込みます。

  LookAhead の 30 行目から、以下のコードを追加してください。
	void OnAnimatorIK(int layerIndex)
	{			
		if(!enabled) return; 
			
		if(layerIndex == 0) // do IK pass on base layer only
		{
            if (m_MonobitView.isMine)
            {
                float vertical = m_Animator.GetCurrentAnimatorStateInfo(0).IsName("Locomotion.Idle") ? 10 : 0;

                Vector3 lookAheadPosition = HeadTransform.position + (HeadTransform.forward * 10) +
                    (HeadTransform.up * vertical * Input.GetAxis("Vertical")) + (HeadTransform.right * 20 * Input.GetAxis("Horizontal"));
                m_Animator.SetLookAtPosition(lookAheadPosition);
                m_Animator.SetLookAtWeight(1.0f, 0.1f, 0.9f, 1.0f, 0.7f);
                m_MonobitView.RPC("UpdateLookAhead", MonobitEngine.MonobitTargets.All, lookAheadPosition);
            }
            else
            {
                m_Animator.SetLookAtPosition(lookAheadPosition);
                m_Animator.SetLookAtWeight(1.0f, 0.1f, 0.9f, 1.0f, 0.7f);
            }
        }
	}

  ここでは、プレイヤー自身の操作( MonobitView.isMine == true )の場合、既存の lookAheadPosition を使った頭位置(弾の発射方向)を変更し、
  MonobitView.RPC 関数を使って、全クライアントとの lookAheadPosition の同期をとります。

  自身の操作以外( MonobitView.isMine == false )の場合には、同期を取った lookAheadPosition を使って
  頭位置(弾の発射方向)のみを変更します。


変更したプログラムの保存

ここまで変更を加えた全スクリプトを保存する

  変更したスクリプトファイルを、すべて保存しましょう。
  Visual Studio のメニューから [ファイル] > [すべて保存] を選択し、保存してください。