【Unity】タワーディフェンス(9) 砲弾の発射【クソゲー制作】

タワーディフェンスを作る(9)

Unity初心者が2Dタワーディフェンスを制作するドタバタ劇を公開しています。今回は本当にドタバタしています。

環境
  • Mac mini (M1, 2020)
  • Unity 2022.3.36f1
目次

攻撃時に砲身が敵の方向を向くようにする

まずは、砲台が敵を攻撃するときに砲身が敵の方向を向くようにしたいと思います。

TurretControllerスクリプトを次のように修正します。

using System.Collections;
using System.Collections.Generic;
using UnityEngine;

public class TurretController : MonoBehaviour
{
    [SerializeField]
    private int attackPower = 1; // 攻撃力
    [SerializeField]
    private float attackInterval = 60.0f; // 攻撃間隔(単位はフレーム)
    [SerializeField]
    private bool isAttack; // 攻撃中フラグ
    [SerializeField]
    private EnemyController enemy; // 敵
    // ここから 変数を追加
    [SerializeField]
    private Transform turretHead; // 砲身のTransform
    // ここまで

    // ここから 追加
    private void Update()
    {
        // 敵が存在する場合は砲身を敵の方向に向ける
        if (enemy)
        {
            RotateTurretHeadTowardsEnemy();
        }
    }
    // ここまで

    private void OnTriggerStay2D(Collider2D collision)
    {
        // ...省略...
    }

    /// <summary>
    /// 攻撃間隔管理
    /// </summary>
    public IEnumerator ManageAttacks()
    {
        // ...省略...
    }

    /// <summary>
    /// 攻撃
    /// </summary>
    private void Attack()
    {
        // ...省略...
    }

    private void OnTriggerExit2D(Collider2D collision)
    {
        // ...省略...
    }

    // ここから メソッド追加
    /// <summary>
    /// 砲身を敵の方向に回転させる
    /// </summary>
    private void RotateTurretHeadTowardsEnemy()
    {
        if (!enemy) return;
        // 敵の位置と砲身の位置の差分を計算
        Vector3 direction = enemy.transform.position - turretHead.position;
        // Z軸方向の回転角度を計算
        float angle = Mathf.Atan2(direction.y, direction.x) * Mathf.Rad2Deg - 90.0f;
        // 砲身を回転させる
        turretHead.rotation = Quaternion.Euler(0, 0, angle);
    }
    // ここまで
}

RotateTurretHeadTowardsEnemy()を追加しました。砲身を敵の方向に向かせるメソッドです。

  • 63行目、敵が存在しない場合処理を中断します。
  • 65行目、敵の位置と砲身の位置のベクトルの差を計算しています。この差分ベクトル(direction)は砲身から敵へ向かう方向をあらわします。
  • 67行目、ベクトルを角度に変換します。
    • Mathf.Atan2(x,y)は原点 (0,0) から (x, y) へ向かう角度(ラジアン単位)を計算する関数です。
    • Mathf.Rad2Deg をかけることで、ラジアンを度(°)に変換しています。
    • Mathf.Atan2(y, x)の結果は、右向きが 0° になりますが、一般的な2Dゲームでは上向きを0°にしたいので- 90.0fとしています。
  • 69行目、計算したangleを使って砲身(turrethead)を回転させます。

スクリプトを修正したら、「Assets/Prefabs/Turret」をダブルクリックしてTurretプレハブの編集画面に入ります。インスペクターの「Turret Controller (Script) > Turret Head」「TurretHead」オブジェクトをドラッグ&ドロップしてアサインします。

動作確認をしてみましょう。

タワーディフェンス103

攻撃中に砲身が敵の方向に向くようになりました。いい感じ〜。

砲弾を発射する

今回のメインディッシュ、砲台が敵を攻撃するときに砲弾を発射するようにします。

砲弾オブジェクトの作成

STEP

ヒエラルキーで右クリック「2D Object > Sprites > Circle」を選択してオブジェクトを作成します。名前を「Shell」にします。

タワーディフェンス104
STEP

Shellオブジェクトのインスペクターで「Transform > Scale」のX, Y, Zの値を0.2にします。これは砲弾の大きさの設定です。

「Sprite Renderer > Color」で砲弾の色を設定します。ここではオレンジにしました。

「Sprite Renderer > Additional Settings > Sorting Layer」Objectにします。これは表示の重なり順の設定ですね。いちばん上に表示されるようにします。

タワーディフェンス105
STEP

Shellオブジェクトに「Rigidbody 2D」コンポーネントを追加します。「Body Type」「Kinematic」に変更します。重力や力の物理演算の影響を受けないモードですね。砲弾の動きはこのあとに作成するShellControllerスクリプトで制御します。

タワーディフェンス106
STEP

Shellオブジェクトに「Circle Collider 2D」コンポーネントを追加します。「Is Trigger」にチェックを入れます。

タワーディフェンス107
STEP

Shellオブジェクトを「Assets > Prefabs」にドラッグ&ドロップしてプレハブ化します。

ヒエラルキーのShellオブジェクトは用済みなので削除します。バイバーイ。

TurretControllerスクリプトの修正

TurretControllerスクリプトを次のように修正します。

using System.Collections;
using System.Collections.Generic;
using UnityEngine;

public class TurretController : MonoBehaviour
{
    [SerializeField]
    private int attackPower = 1; // 攻撃力
    [SerializeField]
    private float attackInterval = 60.0f; // 攻撃間隔(単位はフレーム)
    [SerializeField]
    private bool isAttack; // 攻撃中フラグ
    [SerializeField]
    private EnemyController enemy; // 敵
    [SerializeField]
    private Transform turretHead; // 砲身のTransform
    // ここから 変数を追加
    [SerializeField]
    private GameObject shellPrefab; // 砲弾のプレハブ
    [SerializeField]
    private Transform firePoint; // 砲弾の発射位置
    private Coroutine attackCoroutine; // 現在の攻撃コルーチン
    // ここまで

    private void Update()
    {
        // ...省略...
    }

    private void OnTriggerStay2D(Collider2D collision)
    {
        // 攻撃中ではない場合で、かつ敵の情報を未取得の場合
        if (!isAttack && !enemy)
        {
            Debug.Log("敵発見");
            if (collision.gameObject.TryGetComponent(out enemy)) // 敵を発見したら
            {
                isAttack = true; // 攻撃状態にする
                // ここから 修正
                //StartCoroutine(ManageAttacks()); // 攻撃間隔の管理
                attackCoroutine = StartCoroutine(ManageAttacks()); // 攻撃間隔の管理
                // ここまで
            }
        }
    }

    /// <summary>
    /// 攻撃間隔管理
    /// </summary>
    public IEnumerator ManageAttacks()
    {
        Debug.Log("攻撃間隔管理");
        // ここから 修正
        //int timer = 0;
        // 攻撃状態の間ループ処理を繰り返す
        //while (isAttack)
        //{
        //    timer++; // タイマー
        //    if (timer > attackInterval) // 待機時間が経過したら
        //    {
        //        timer = 0; // タイマーをリセット
        //        Attack(); // 攻撃
        //    }
        //    yield return null; // 1フレーム処理を中断する
        //}
        // 攻撃状態の間ループ処理を繰り返す
        while (enemy != null && isAttack)
        {
            // 攻撃を実行
            Attack();
            // 次の攻撃まで待機
            yield return new WaitForSeconds(attackInterval / 60.0f); // フレーム間隔を秒数に変換
        }
        isAttack = false; // 攻撃を終了
        attackCoroutine = null; // コルーチン参照をクリア
        // ここまで
    }

    /// <summary>
    /// 攻撃
    /// </summary>
    private void Attack()
    {
        Debug.Log("攻撃");
        // ここから 修正
        // 敵にダメージを与える
        //enemy.CalcDamage(attackPower);
        if (shellPrefab && firePoint)
        {
            // 砲弾を生成
            GameObject shell = Instantiate(shellPrefab, firePoint.position, firePoint.rotation);
            // ShellControllerに敵情報を渡す
            ShellController shellController = shell.GetComponent<ShellController>();
            if (shellController != null)
            {
                shellController.Initialize(enemy, attackPower);
            }
        }
        // ここまで
    }

    private void OnTriggerExit2D(Collider2D collision)
    {
        // ここから 修正
        //if (collision.gameObject.TryGetComponent(out enemy))
        if (collision.gameObject.TryGetComponent(out EnemyController exitingEnemy) && exitingEnemy == enemy)
        // ここまで
        {
            Debug.Log("敵なし");
            isAttack = false;
            // コルーチンが実行中の場合は停止
            if (attackCoroutine != null)
            {
                StopCoroutine(attackCoroutine);
                attackCoroutine = null;
            }
            enemy = null;
        }
    }

    /// <summary>
    /// 砲身を敵の方向に回転させる
    /// </summary>
    private void RotateTurretHeadTowardsEnemy()
    {
        // ...省略...
    }
}

41行目、ManageAttacksのコルーチンを変数に代入して管理することにしました。これはコルーチンが多重起動しないように制御するためです。

ManageAttacksメソッドを修正しています。timer変数を使うのをやめてyield return new WaitForSeconds()で攻撃間隔を管理します。

Attackは単に敵にダメージを与えるメソッドでしたが、プレハブから砲弾を生成しShellControllerスクリプトに敵情報を渡す仕事を任されることになりました。敵にダメージを与える役割は、あとで生成するShellControllerスクリプトが担います。

ShellControllerスクリプトの作成

STEP

「Assets > Scripts」下にC#スクリプトを作成します。名前を「ShellController」にします。

using System.Collections;
using System.Collections.Generic;
using UnityEngine;

public class ShellController : MonoBehaviour
{
  [SerializeField]
  private float speed = 5.0f; // 砲弾の速度
  private EnemyController targetEnemy; // ターゲットとなる敵
  private int attackPower; // 砲弾の攻撃力

  private void Update()
  {
    if (targetEnemy == null) // ターゲットがいない場合
    {
      Destroy(gameObject); // 砲弾を破壊
      return;
    }
    // 敵に向かって移動
    Vector3 direction = (targetEnemy.transform.position - transform.position).normalized;
    transform.position += direction * speed * Time.deltaTime;
  }

  /// <summary>
  /// 砲弾の初期化
  /// </summary>
  public void Initialize(EnemyController enemy, int power)
  {
    targetEnemy = enemy;
    attackPower = power;
  }

  /// <summary>
  /// 砲弾が敵に当たったときの処理
  /// </summary>
  private void OnTriggerEnter2D(Collider2D collision)
  {
    if (collision.TryGetComponent(out EnemyController enemy))
    {
      // 対象の敵かどうか確認
      if (enemy == targetEnemy)
      {
        enemy.CalcDamage(attackPower); // ダメージを与える
        Destroy(gameObject); // 砲弾を破壊
      }
    }
  }
}
STEP

「Assets > Prefabs > Shell」をダブルクリックして、プレハブの編集モードに入ります。

ShellControllerスクリプトをドラッグ&ドロップしてアタッチします。

砲台オブジェクトの設定

STEP

「Assets > Prefabs > Turret」をダブルクリックしてTurretプレハブの編集画面に入ります。

STEP

TurretHeadオブジェクトの下位に空のオブジェクトを作成して(Create Empty)、名前を「FirePoint」にします。これは、先ほどTurretControllerスクリプトに追加した変数firePointに対応するオブジェクトで、砲弾の発射位置をあらわします。

「Transform > Position」を調整して砲弾の発射位置を決めます。

タワーディフェンス108
STEP

Turretオブジェクトのインスペクターで「Turret Controller (Script) > Shell Prefab」「Assets > Prefabs > Shell」をドラッグ&ドロップしてアサインします。

STEP

同じく「Turret Controller (Script) > Fire Point」にヒエラルキーの「Fire Point」をドラッグ&ドロップしてアサインします。

動作確認

動作確認です。砲台から敵に向けてオレンジ色の砲弾が発射されていますね。

タワーディフェンス109

いちばん近い敵を攻撃するようにする

砲弾の発射を実装して気づいたことがあります。現在の仕様では砲台の攻撃範囲に複数の敵がいる場合、最初の敵を攻撃し続けます。砲台に最も近い敵を攻撃するようにしたい。

TurretControllerスクリプトを修正します。

using System.Collections;
using System.Collections.Generic;
using UnityEngine;

public class TurretController : MonoBehaviour
{
    [SerializeField]
    private int attackPower = 1; // 攻撃力
    [SerializeField]
    private float attackInterval = 60.0f; // 攻撃間隔(単位はフレーム)
    // ここから 変数を削除
    //[SerializeField]
    //private bool isAttack; // 攻撃中フラグ
    //[SerializeField]
    //private EnemyController enemy; // 敵
    // ここまで
    [SerializeField]
    private Transform turretHead; // 砲身のTransform
    [SerializeField]
    private GameObject shellPrefab; // 砲弾のプレハブ
    [SerializeField]
    private Transform firePoint; // 砲弾の発射位置

    // ここから 変数を追加
    private List<EnemyController> enemiesInRange = new List<EnemyController>(); // 攻撃範囲内の敵リスト
    private EnemyController targetEnemy = null; // 現在のターゲット
    private bool isAttacking = false; // 攻撃中フラグ
    // ここまで
    private Coroutine attackCoroutine; // 現在の攻撃コルーチン

    private void Update()
    {
        // ここから 追加
        UpdateTargetEnemy(); // 最も近い敵を探す
        // ここまで
        // ここから 修正
        // 敵が存在する場合は砲身を敵の方向に向ける
        //if (enemy)
        if (targetEnemy) // ターゲットが存在する場合
        {
            RotateTurretHeadTowardsEnemy(); // 砲身をターゲットの方向に向ける
        }
        // ここまで
    }

    // ここから メソッド削除
    //private void OnTriggerStay2D(Collider2D collision)
    //{
    //    // 攻撃中ではない場合で、かつ敵の情報を未取得の場合
    //    if (!isAttack && !enemy)
    //    {
    //        Debug.Log("敵発見");
    //        if (collision.gameObject.TryGetComponent(out enemy)) // 敵を発見したら
    //        {
    //            isAttack = true; // 攻撃状態にする
    //            attackCoroutine = StartCoroutine(ManageAttacks()); // 攻撃間隔の管理
    //        }
    //    }
    //}
    // ここまで

    // ここから メソッドを3つ追加
    private void OnTriggerEnter2D(Collider2D collision)
    {
        if (collision.TryGetComponent(out EnemyController enemy)) // 侵入してきたのが敵ならば
        {
            // 敵リストに追加
            if (!enemiesInRange.Contains(enemy))
            {
                enemiesInRange.Add(enemy);
            }

            // 攻撃していなければ開始
            if (!isAttacking)
            {
                isAttacking = true;
                attackCoroutine = StartCoroutine(ManageAttacks());
            }
        }
    }

    private void OnTriggerExit2D(Collider2D collision)
    {
        if (collision.TryGetComponent(out EnemyController enemy)) // 出ていったのが敵ならば
        {
            enemiesInRange.Remove(enemy); // リストから削除

            // ターゲットが出て行った場合、別のターゲットを選択
            if (targetEnemy == enemy)
            {
                targetEnemy = null;
                UpdateTargetEnemy();
            }

            // もし範囲内に敵がいなくなったら攻撃をやめる
            if (enemiesInRange.Count == 0)
            {
                isAttacking = false;
                if (attackCoroutine != null)
                {
                    StopCoroutine(attackCoroutine);
                    attackCoroutine = null;
                }
            }
        }
    }

    /// <summary>
    /// 砲台に最も近い敵を選択
    /// </summary>
    private void UpdateTargetEnemy()
    {
        if (enemiesInRange.Count == 0) // 攻撃範囲に敵がいなければ
        {
            targetEnemy = null; // ターゲットなし
            return;
        }
        float closestDistance = float.MaxValue; // 最も近い敵までの距離に最大値を代入
        EnemyController closestEnemy = null; // 最も近い敵
        // 攻撃範囲内のすべての敵をチェック
        foreach (var enemy in enemiesInRange)
        {
            // 砲台と敵の距離を計算
            float distance = Vector2.Distance(transform.position, enemy.transform.position);
            // より近い敵を記録
            if (distance < closestDistance)
            {
                closestDistance = distance;
                closestEnemy = enemy;
            }
        }
        targetEnemy = closestEnemy; // 最も近い敵をターゲットとして設定
    }
    // ここまで

    /// <summary>
    /// 攻撃間隔管理
    /// </summary>
    public IEnumerator ManageAttacks()
    {
        Debug.Log("攻撃間隔管理");
        // ここから 修正
        // 攻撃状態の間ループ処理を繰り返す
        //while (enemy != null && isAttack)
        //{
        //    Attack(); // 攻撃を実行
        //    // 次の攻撃まで待機
        //    yield return new WaitForSeconds(attackInterval / 60.0f);
        //}
        //isAttack = false; // 攻撃を終了
        //attackCoroutine = null; // コルーチン参照をクリア
        while (isAttacking)
        {
            if (targetEnemy) // ターゲットが存在する場合
            {
                Attack(); // 攻撃を実行
            }
            // 次の攻撃まで待機
            yield return new WaitForSeconds(attackInterval / 60.0f);
        }
        // ここまで
    }

    /// <summary>
    /// 攻撃
    /// </summary>
    private void Attack()
    {
        Debug.Log("攻撃");
        // ここから 修正
        //if (shellPrefab && firePoint)
        //{
        //    // 砲弾を生成
        //    GameObject shell = Instantiate(shellPrefab, firePoint.position, firePoint.rotation);
        //    // ShellControllerに敵情報を渡す
        //    ShellController shellController = shell.GetComponent<ShellController>();
        //    if (shellController != null)
        //    {
        //        shellController.Initialize(enemy, attackPower);
        //    }
        //}
        if (!targetEnemy || !shellPrefab || !firePoint) return;
        // 砲弾を生成
        GameObject shell = Instantiate(shellPrefab, firePoint.position, firePoint.rotation);
        // ShellControllerに敵情報を渡す
        ShellController shellController = shell.GetComponent<ShellController>();
        if (shellController)
        {
            shellController.Initialize(targetEnemy, attackPower);
        }
        // ここまで
    }

    // ここから メソッド削除
    //private void OnTriggerExit2D(Collider2D collision)
    //{
    //    if (collision.gameObject.TryGetComponent(out EnemyController exitingEnemy) && exitingEnemy == enemy)
    //    {
    //        Debug.Log("敵なし");
    //        isAttack = false;
    //        // コルーチンが実行中の場合は停止
    //        if (attackCoroutine != null)
    //        {
    //            StopCoroutine(attackCoroutine);
    //            attackCoroutine = null;
    //        }
    //        enemy = null;
    //    }
    //}
    // ここまで

    /// <summary>
    /// 砲身を敵の方向に回転させる
    /// </summary>
    private void RotateTurretHeadTowardsEnemy()
    {
        // ここから 修正
        //if (!enemy) return;
        // ターゲットが存在しない場合、何もしない
        if (!targetEnemy) return;
        // 敵の位置と砲身の位置の差分を計算
        //Vector3 direction = enemy.transform.position - turretHead.position;
        Vector3 direction = targetEnemy.transform.position - turretHead.position;
        // ここまで
        // Z軸方向の回転角度を計算
        float angle = Mathf.Atan2(direction.y, direction.x) * Mathf.Rad2Deg - 90.0f;
        // 砲身を回転させる
        turretHead.rotation = Quaternion.Euler(0, 0, angle);
    }
}

はい、かなり大幅な修正になりました。

ざっくり解説すると、攻撃範囲内にいる敵のリストをenemiesInRangeに入れて、その中から砲台に最も近い敵をUpdateTargetEnemyメソッドで選択しています。

動作確認をしましょう。

タワーディフェンス110

砲台はいちばん近い敵を攻撃するようになりました。

さいごに

今回は終盤で予定していなかった作業が発生してしまいバタバタしました。スクリプトを大幅に修正することになりムダが多かったですね。これが初心者。

でわでわ

タワーディフェンスを作る(9)

この記事が気に入ったら
いいね または フォローしてね!

シェアしてね

コメント

コメントする

目次