【Unity】オンラインシューティングゲームを作る：弾の処理

#はじめに
どうもnisokaです。
PingPongゲームもいい感じにできて次は何やろうかなーと考えたのですが。
シンプルなゲームといえばシューティングかな？と思って調べてみたところ、オンラインシューティングゲームの情報が少ないことに気が付きました。
ということで、また新たにオンラインシューティングゲームを作っています。

作るなかで弾の処理で詰まったので、今回はそれについて少し紹介したいと思います。

#開発環境
- Windows10 64bit
- Unity2018.3.3f1
- Monobit Unity Networking2.0 v2.6

#今回のトピック
- 弾の同期は必要なの？
- オブジェクトプールという考え方

#弾の同期は必要なの？
モノビットエンジンにはMonobitView.TransformView.csという便利なスクリプトが用意されています。これを同期させたいオブジェクトにアタッチすれば即座に各端末で毎フレーム座標を同期できます。
しかし、プレイヤーの環境によっては回線速度がよくない場合も考えられますし、**できる限り同期する処理は減らしたほうが良い**です。
そこでいろいろ試した結果、**弾を発射するタイミングだけを同期する**ようにしました。

実装は以下のような感じです。

```C#:弾を発射させるクラス
public class Player : MonoBehaviour
{
    // 弾の発射間隔時間
    public float bulletTimeOut = 1.0f;
    private float bulletTimeTrigger;
	
    // 自分のオブジェクトは操作する
    if(monobitView.isMine)
    {
        // 一定間隔で弾を発射
        if (Time.time > bulletTimeTrigger)
        {
        	// すべてのクライアントにRPC
            monobitView.RPC("RecvShotBullet", MonobitTargets.All, spawnSpot.position);
            bulletTimeTrigger = Time.time + bulletTimeOut;
        }
    }
    ...
    
    // 弾発射の通知を受け取るRPC
    [MunRPC]
    void RecvShotBullet(Vector3 position)
    {
    	// 弾発射
        ShootingBullet(position);
    }
    
    // 弾を発射する関数
    void ShootingBullet(Vector3 position)
    {
    	// 未使用の弾取得
        Bullet bullet = bulletManager.GetListData();
        // 弾初期化
    }
}
```

弾を発射したタイミングで全クライアントにRPCメッセージを投げます。
受信した側は即座にプールしてある弾から未使用のものを選び使用します。

#オブジェクトプールという考え方
UnityのInstantiateという機能について。
弾を発射する処理は、発射するときにPrefabをInstantiateするのが一番手っ取り早いですよね。でも**Instantiateという処理はUnityにとって非常に重くコストが高いみたい**で、発射する度に毎回呼ぶのはあまりお行儀が良くないみたいです。
そういうときはオブジェクトプールを使うとスマートだと聞いたので作りました。
オブジェクトプールとは、ゲーム開始時やシーン切り替え後などにあらかじめある程度まとめてInstantiateしておいてそれを使いまわす手法です。

実装は以下のような感じです。

```C#:オブジェクト溜めておくクラス
public class BulletManager : MonoBehaviour
{
    // 弾Prefab
    public Bullet bulletPrefab;
    // 最大弾数
    const int maxCount = 100;
    // 全弾データ
    List<Bullet> dataList = new List<Bullet>();

    void Start()
    {
        // 最大弾数分を生成する
        for (int i = 0; i < maxCount; i++)
        {
            Bullet bullet = Instantiate(bulletPrefab, Vector3.zero, Quaternion.identity);
            // 未使用状態にする
            bullet.gameObject.SetActive(false);
            // 配列に格納する
            dataList.Insert(0, bullet);
        }
    }

    // 未使用の弾データ取得
    public Bullet GetListData()
    {
        foreach (Bullet data in dataList)
        {
            // 未使用の弾だったら
            if (!data.gameObject.activeSelf)
            {
                // 使用状態にする
                data.gameObject.SetActive(true);
                return data;
            }
        }
        return null;
    }
}
```

```C#:オブジェクトを使いたいクラス
public class Player : MonoBehaviour
{
    // 弾マネージャー
    public BulletManager bulletManager;
    
    void Awake()
    {
    	// 弾マネージャー取得
        bulletManager = GameObject.Find("BulletManager").GetComponent<BulletManager>();
    }
    ...

    // 弾を発射する関数
    void ShootingBullet(Vector3 position)
    {
    	// 未使用の弾取得
        Bullet bullet = bulletManager.GetListData();
        // 弾初期化
    }
}
```
まずInstantiateしたオブジェクトをためておくBulletManagerクラスを作りました。
発射するタイミングだけ同期すれば良く、弾オブジェクト自体は同期対象ではないので、弾は普通のInstantiateで生成します。よって、各クライアントごとに弾が存在します。
次に弾を発射するPlayerクラスを作りました。
プールしたオブジェクトにどうやってアクセスするのかについては色々とやり方があって悩みましたが、私は単純にBulletManagerコンポーネントをFindする方法にしました。