Help us understand the problem. What is going on with this article?

ミニゲームを作ってUnityを学ぶ! [ タンクウォーズ編 - 4. 弾の発射と管理 ]

More than 1 year has passed since last update.

ミニゲームを作ってUnityを学ぶ![タンクウォーズ編]

第4回目: 弾の発射と管理

今回は戦車について、以下のように弾の発射とそれを制御・管理する機能を実装します。

  • 左クリックのタイミングで砲台の向いている方向へ弾を発射する
  • 発射された弾を適切に制御・管理する

弾と各種エフェクト

インポートした戦車のアセットにはすでに撃ち出す弾やエフェクトのプレハブが用意されていますが、そのまま再利用するとどうにも見えにくい&迫力にかけるため、今回はイメージに合うよう弾は自作、爆発のエフェクトを他からお借りし、発射時のエフェクトのみをアセット内から利用します。

爆発エフェクトの準備

弾が戦車に当たった際に発生させる爆発エフェクトは通常の小さいバージョンと、戦車が完全に破壊されたとき用の大きいバージョンの2つを用意しておきます。

KTK_kumamotoさんの爆発エフェクト

  1. 上のリンクを辿って「eff_burst_sample」をプロジェクトにインポート
  2. アセット内の「eff_burst_flash」をシーンに配置して名前を「ExplosionL」に変更
  3. 子要素「eff_burst_flash」「eff_burst_ring」の2つを無効に設定
  4. 無効にした以外の要素すべてについて、Loopingのチェックを外す
  5. 「Kawaii_Tanks_Project/Kawaii_Tanks_Assets/Scripts/Delete_Timer_CS」をアタッチ
  6. この状態でExplosionLを複製し、新しい方の名前を「ExplosionS」に変更
  7. ExplosionSのScaleを(x=0.2, y=0.2, z=0.2)に変更
  8. それぞれのプレハブを書き出し
  9. シーンに配置したオブジェクトを破棄しておく

弾オブジェクトの準備

  1. 「3D Object」→「Sphere」をシーンに配置
  2. 名前を「Bullet」に変更
  3. Mesh RendererのCast ShadowsをOffに設定して影を非表示
  4. Rigidbodyをアタッチし、Use Gravityのチェックを外して重力を無効
  5. Freeze PositionのYにチェックを入れて高さの動きを制限
  6. Sphere ColliderのIs Trigerにチェックを入れる
  7. Bulletのプレハブを書き出し

tankwars_ss_4_1.jpg

BulletModelの作成

弾のオブジェクトとエフェクトを用意したところで、次は弾にアタッチするためのスクリプトを作成します。

  • BulletModelという名前でスクリプトを作成
  • BulletModelをBulletにアタッチ
  • Bulletのプレハブを更新し、シーンに配置されたオブジェクトは破棄しておく
BulletModel.cs
using System.Collections;
using System.Collections.Generic;
using UnityEngine;

    public class BulletModel : MonoBehaviour
    {

        private Transform mTrans;
        private Rigidbody mRigid;

        void Awake()
        {
            mTrans = GetComponent<Transform>();
            mRigid = GetComponent<Rigidbody>();
        }

    }

Awake()で弾自身のTransformとRigidbodyを取得。
他に必要な機能は後ほど追加していきます。

FireControllerの作成

次は弾を生成・管理する側のスクリプトを作成して、戦車に弾を発射する機能を実装します。

  • FireControllerという名前のスクリプトを作成
  • FireControllerをTankModelにアタッチ
FireController.cs
using System.Collections;
using System.Collections.Generic;
using UnityEngine;

    public class FireController : MonoBehaviour
    {

        [SerializeField]
        [Tooltip("弾オブジェクトを一か所にまとめるための空オブジェクト")]
        private Transform mTransBulletGroup; // 弾オブジェクトのルートとなる
        [SerializeField]
        [Tooltip("弾の発射地点")]
        private Transform mTransFirePoint; // 発射時の弾とエフェクトについて、位置と方向を決定する
        [SerializeField]
        [Tooltip("発射エフェクト")]
        private GameObject mMuzzleFirePrefab;
        [SerializeField]
        [Tooltip("弾")]
        private GameObject mBulletPrefab;

    }

まずはインスペクタから設定できるフィールドを4つ作成します。

  • mTransBulletGroup = 「BulletGroup」という名前でゼロポジションに作成した空オブジェクト(このタイミングで配置しておく)
  • mTransFirePoint = PlayerTank/Turret_Base/Cannon_Base/Barrel_Base/Fire_Point
  • mMuzzleFirePrefab = Kawaii_Tanks_Project/Kawaii_Tanks_Assets/Prefabs/Bullet_and_Effects/MuzzleFire
  • mBulletPrefab = 書き出したプレハブ「Bullet」

次に入力の監視と実際に弾を発射する部分のコードです。

FireController.cs
        // TankModelのUpdateから呼び出され、左クリックの入力を監視する
        public void CheckInput()
        {
            if (Input.GetMouseButtonDown(0)) Fire();
        }

        //------------
        // 弾の発射 //
        //------------

        // 所持している弾数
        private int mBulletCount = 3;

        public void Fire()
        {
            // 弾数が残っているならば発射処理
            if (mBulletCount > 0)
            {
                // 所持弾数をデクリメント
                mBulletCount--;

                // 発射エフェクト生成
                CreateMuzzleFire();

                // 弾生成
                CreateBullet();
            }
        }

        private void CreateMuzzleFire()
        {

        }

        private void CreateBullet()
        {

        }

TankModel.cs
        private TankMovement mMovementScript;
        private TurretController mTurretScript;
追加    private FireController mFireScript;

        void Start()
        {
            SetLayerCollision();
            mMovementScript = GetComponent<TankMovement>();
            mTurretScript = GetComponent<TurretController>();
追加        mFireScript = GetComponent<FireController>();
        }

        void Update()
        {
            if (IsPlayer && IsActive)
            {
                // 移動入力の受付
                mMovementScript.CheckInput();

                // 砲台角度を計算
                mTurretScript.CalRotation();

                // 弾発射の入力を受付
追加            mFireScript.CheckInput();
            }
        }

TankModelのUpdate()から呼び出されるInputCheck()によって左クリックの入力を監視し、Fire()では所持弾数が残っている場合に発射エフェクトと弾の生成を行っています。

所持弾数mBulletCountはボンバーマンでいう一度に置ける爆弾の数のようなイメージで、今回のプロジェクトでは1つの戦車が発射できる弾の数を画面内3つまでに制限しています。

FireController.cs
        /// <summary>
        /// 発射時のエフェクト生成
        /// </summary>
        private void CreateMuzzleFire()
        {
            GameObject muzzleFire = Instantiate(mMuzzleFirePrefab, mTransFirePoint.position, mTransFirePoint.rotation) as GameObject;
            muzzleFire.transform.parent = mTransFirePoint;
        }

Fire()で呼ばれるCreateMuzzleFire()は弾の発射エフェクトを生成するメソッドです。
インスタンス化された発射エフェクトは発射地点「Fire_Point」の子となり、自身にアタッチされているDelete_Timer_CSクラスによって2秒後に自動的に破棄されます。

オブジェクトプーリング

プレハブからオブジェクトをインスタンス化するInstantiate()や、逆に存在するオブジェクトを破棄するDestroy()は使用頻度が高く、それに加えてUnityの中でも重い処理として知られています。

今回のプロジェクトでは1つの戦車から発射できる弾数の制限をしているためそれほどではありませんが、一般的なシューティングゲームやアクションゲームなどでは生成・破棄の頻度がより高く、このコストが軽視できない問題となってきます。

その問題を解消する仕組みがオブジェクトプーリングです。

オブジェクトプーリング

オブジェクトを再利用する仕組みのこと。
シューティングゲームの画面外に出てしまった弾など、不要になったオブジェクトを
破棄するのではなく休眠状態にし、新しい弾が必要な場合は休眠状態のオブジェクト
を再利用することで生成と破棄のコストを抑えることができます。

せっかくなので弾オブジェクトについてはこの仕組みを利用することにして、BulletModelとFireControllerに対応するコードを書いていきます。

BulletModel.cs
        // 休眠状態
        private bool mIsSleep;
        public bool IsSleep { get { return mIsSleep; } }

        // 攻撃力
        private int mAttackValue;

        /// <summary>
        /// 生成時の初期化
        /// </summary>
        /// <param name="attackValue">攻撃力</param>
        /// <param name="bulletSpeed">弾の速さ</param>
        public void Spawn(int attackValue, float bulletSpeed)
        {
            mIsSleep = false;
            mAttackValue = attackValue;
            mRigid.velocity = mTrans.forward * bulletSpeed;
        }

        /// <summary>
        /// 再利用時の初期化
        /// </summary>
        /// <param name="attackValue">攻撃力</param>
        /// <param name="bulletSpeed">弾の速さ</param>
        /// <param name="position">リスポーン位置</param>
        /// <param name="rotation">リスポーン角度</param>
        public void Respawn(int attackValue, float bulletSpeed, Vector3 position, Quaternion rotation)
        {
            gameObject.SetActive(true);
            mTrans.SetPositionAndRotation(position, rotation);
            mIsSleep = false;
            mAttackValue = attackValue;
            mRigid.velocity = mTrans.forward * bulletSpeed;
        }

        public void Sleep()
        {
            mIsSleep = true;
            mRigid.velocity = Vector3.zero;
            gameObject.SetActive(false);
        }

新しくインスタンス化された弾はSpawn()によって自身の正面方向へ向かって飛んでいきます。
そして画面外に出るなど不要になった場合はSleep()によって休眠状態となり、再利用される際にはRespawn()によって生まれ変わります。

FireController.cs
        // 発射地点のTransformから正面方向に1.0f進んだ座標に弾を生成する
        private readonly float BULLET_OFFSET = 1.0f;

        // 生成された弾オブジェクトをプーリング
        private List<BulletModel> mBulletList = new List<BulletModel>();

        // 弾の攻撃力
        private int mBulletAttack = 10;

        // 弾の速さ
        private float mBulletSpeed = 40.0f;

        /// <summary>
        /// 弾オブジェクトの生成
        /// </summary>
        private void CreateBullet()
        {
            // 休眠状態の弾オブジェクトがある場合はそれを再利用する
            foreach(BulletModel model in mBulletList)
            {
                if (model.IsSleep)
                {
                    model.Respawn(mBulletAttack, mBulletSpeed, mTransFirePoint.position + mTransFirePoint.forward * BULLET_OFFSET, mTransFirePoint.rotation);
                    return;
                }
            }

            // 再利用できるオブジェクトが無かった場合は新しく生成してリストに格納する
            GameObject bulletGo = Instantiate(mBulletPrefab, mTransFirePoint.position + mTransFirePoint.forward * BULLET_OFFSET, mTransFirePoint.rotation) as GameObject;
            bulletGo.transform.parent = mTransBulletGroup;
            BulletModel bulletModel = bulletGo.GetComponent<BulletModel>();
            bulletModel.SetDelegate(this);
            bulletModel.Spawn(mBulletAttack, mBulletSpeed);
            mBulletList.Add(bulletModel);
        }

Fire()で呼ばれるCreateBullet()は弾を生成して発射するメソッドですが、弾オブジェクトをインスタンス化する前に休眠状態の弾オブジェクトが存在するかどうかを判定し、もしあるならばそれをRespawn()で再利用しています。

デリゲートで通知する

説明を後回しにしましたが、FireControllerの弾を生成するCreateBullet()にはデリゲートに関する記述があります。

bulletModel.SetDelegate(this);

この記述から呼び出されるBulletModelのコードを追加します。

BulletModel.cs
        private delegate void OnSleep();
        private OnSleep dOnSleep;

        public void SetDelegate(FireController fireController)
        {
            dOnSleep = fireController.OnSleepBullet;
        }

デリゲートは「関数を入れられる変数」といった説明がされますが、上記のコードでいえばBulletModel.OnSleepというデリゲートの中にFireController.OnSleepBulletを代入しているような形になります。

次に、BulletModelのSleep()でこのデリゲートが呼ばれるようコードを修正します。

BulletModel.cs
        public void Sleep()
        {
            mIsSleep = true;
            mThisRigid.velocity = Vector3.zero;
            gameObject.SetActive(false);

            // FireControllerに通知
追加        dOnSleep();
        }

このときの処理の流れは以下のようになります。

  1. dOnSleep()を実行
  2. dOnSleepの型であるデリゲートのOnSleepが実行される
  3. OnSleepに代入されているFireControllerのOnSleepBullet()が呼び出される

スクリプトの確認

FireControllerのOnSleepBullet()をまだ実装していませんが、これについてはここまでコード内容が飛び飛びでわかりづらかったことを踏まえて、現在のFireControllerクラスとBulletModelクラスについて整理したコードと共に掲示します。

BulletModel.cs
    public class BulletModel : MonoBehaviour
    {

        private Transform mTrans;
        private Rigidbody mRigid;

        void Awake()
        {
            mTrans = GetComponent<Transform>();
            mRigid = GetComponent<Rigidbody>();
        }

        // 攻撃力
        private int mAttackValue;

        //------------------------
        // 初期化・休眠・再活性化 //
        //----------------------------------------------------------------------------------------

        // 休眠状態
        private bool mIsSleep;
        public bool IsSleep { get { return mIsSleep; } }

        /// <summary>
        /// 生成時の初期化
        /// </summary>
        /// <param name="attackValue">攻撃力</param>
        /// <param name="bulletSpeed">弾の速さ</param>
        public void Spawn(int attackValue, float bulletSpeed)
        {
            mIsSleep = false;
            mAttackValue = attackValue;
            mRigid.velocity = mTrans.forward * bulletSpeed;
        }

        /// <summary>
        /// 休眠状態から再活性化の初期化
        /// </summary>
        /// <param name="attackValue">攻撃力</param>
        /// <param name="bulletSpeed">弾の速さ</param>
        /// <param name="position">リスポーン位置</param>
        /// <param name="rotation">リスポーン角度</param>
        public void Respawn(int attackValue, float bulletSpeed, Vector3 position, Quaternion rotation)
        {
            gameObject.SetActive(true);
            mTrans.SetPositionAndRotation(position, rotation);
            mIsSleep = false;
            mAttackValue = attackValue;
            mRigid.velocity = mTrans.forward * bulletSpeed;
        }

        public void Sleep()
        {
            mIsSleep = true;
            mRigid.velocity = Vector3.zero;
            gameObject.SetActive(false);

            // FireControllerに通知
            dOnSleep();
        }

        //-------------
        // デリゲート //
        //----------------------------------------------------------------------------------------

        private delegate void OnSleep();
        private OnSleep dOnSleep;

        public void SetDelegate(FireController fireController)
        {
            dOnSleep = fireController.OnSleepBullet;
        }

    }

FireController.cs
    public class FireController : MonoBehaviour
    {

        [SerializeField]
        [Tooltip("弾オブジェクトを一か所にまとめるための空オブジェクト")]
        private Transform mTransBulletGroup; // 弾オブジェクトのルートとなる
        [SerializeField]
        [Tooltip("弾の発射地点")]
        private Transform mTransFirePoint; // 発射時の弾とエフェクトについて、位置と方向を決定する
        [SerializeField]
        [Tooltip("発射エフェクト")]
        private GameObject mMuzzleFirePrefab;
        [SerializeField]
        [Tooltip("弾")]
        private GameObject mBulletPrefab;


        //------------------------------
        // Called by TankModel#Update //
        //----------------------------------------------------------------------------------------

        public void CheckInput()
        {
            if (Input.GetMouseButtonDown(0)) Fire();
        }

        //--------
        // 発射 //
        //----------------------------------------------------------------------------------------

        // 発射地点のTransformから正面方向に1.0f進んだ座標に弾を生成する
        private readonly float BULLET_OFFSET = 1.0f;

        // 生成された弾オブジェクトをプーリング
        private List<BulletModel> mBulletList = new List<BulletModel>();

        // 所持している弾数
        private int mBulletCount = 3;

        // 弾の攻撃力
        private int mBulletAttack = 10;

        // 弾の速さ
        private float mBulletSpeed = 40.0f;

        public void Fire()
        {
            // 弾数が残っているならば発射処理
            if (mBulletCount > 0)
            {
                // 所持弾数をデクリメント
                mBulletCount--;

                // 発射エフェクト生成
                CreateMuzzleFire();

                // 弾生成
                CreateBullet();
            }
        }

        /// <summary>
        /// 発射時のエフェクト生成
        /// </summary>
        private void CreateMuzzleFire()
        {
            GameObject muzzleFire = Instantiate(mMuzzleFirePrefab, mTransFirePoint.position, mTransFirePoint.rotation) as GameObject;
            muzzleFire.transform.parent = mTransFirePoint;
        }

        /// <summary>
        /// 弾オブジェクトの生成
        /// </summary>
        private void CreateBullet()
        {
            // 休眠状態の弾オブジェクトがある場合はそれを再利用する
            foreach (BulletModel model in mBulletList)
            {
                if (model.IsSleep)
                {
                    model.Respawn(mBulletAttack, mBulletSpeed, mTransFirePoint.position + mTransFirePoint.forward * BULLET_OFFSET, mTransFirePoint.rotation);
                    return;
                }
            }

            // 再利用できるオブジェクトが無かった場合は新しく生成してリストに格納する
            GameObject bulletGo = Instantiate(mBulletPrefab, mTransFirePoint.position + mTransFirePoint.forward * BULLET_OFFSET, mTransFirePoint.rotation) as GameObject;
            bulletGo.transform.parent = mTransBulletGroup;
            BulletModel bulletModel = bulletGo.GetComponent<BulletModel>();
            bulletModel.SetDelegate(this);
            bulletModel.Spawn(mBulletAttack, mBulletSpeed);
            mBulletList.Add(bulletModel);
        }

        //---------------------
        // デリゲート受け取り //
        //----------------------------------------------------------------------------------------

        /// <summary>
        /// 発射した弾が休眠状態になった際に、残弾数を1つ回復する
        /// called by BulletModel
        /// </summary>
        public void OnSleepBullet()
        {
            mBulletCount++;
        }

    }

FireControllerクラスの最下部にBulletModelクラスのデリゲートを受け取るためのOnSleepBullet()を追加しています。

弾を発射する際に所持弾数を減らす一方で、発射された弾が不要になったタイミングでBulletModel.Sleep()が実行され、弾が休眠状態になったことを通知されたFireControllerが所持弾数を回復させることでシーン内に自弾が3発までという制限を実現しています。

画面外に出た弾の処理

弾が休眠状態になる条件の1つ、画面外に出た場合の処理を作成します。

  • DestroyAreaという名前のCubeをシーンに配置して以下のように設定
Scale: (x=68, y=4, z=40)
Position: (x=0, y=2, z=0)
  • MeshRendererのチェックを外してコライダーのみの透明な状態に変更
  • ColliderのIs Triggerにチェックを入れる
  • タグを「DestroyArea」に設定

tankwars_ss_4_3.jpg

BulletModel.cs
        private readonly string TAG_DESTROY_AREA = "DestroyArea";

        /// <summary>
        /// 弾が領域外に出た際の処理
        /// このオブジェクトを休眠状態に遷移する。
        /// </summary>
        /// <param name="collider"></param>
        void OnTriggerExit(Collider collider)
        {
            if(collider.tag == TAG_DESTROY_AREA)
            {
                Sleep();
            }
        }

弾が画面外に出たかどうかの判定はチュートリアルのシューティングゲームで行っている処理と同じモノになります。

画面外に出た場合に呼び出されるSleep()では

  • 移動の停止
  • 自身を非アクティブに
  • 休眠状態のフラグを立てる

を行った後、FireControllerにその旨を通知します。

tankwars_ss_4_2.jpg

最後にプロジェクトを実行して、左クリックで砲台の向いている方向に弾が発射されることと、弾が最大で3発までしか画面に存在できないことを確認します。


次のページに進む
イントロダクションに戻る

Why not register and get more from Qiita?
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away
Comments
No comments
Sign up for free and join this conversation.
If you already have a Qiita account
Why do not you register as a user and use Qiita more conveniently?
You need to log in to use this function. Qiita can be used more conveniently after logging in.
You seem to be reading articles frequently this month. Qiita can be used more conveniently after logging in.
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away
ユーザーは見つかりませんでした