Unity初心者が2Dタワーディフェンスを作っています。今回は砲台が敵を攻撃して破壊する処理を実装していきます。
- Mac mini (M1, 2020)
- Unity 2022.3.36f1
砲台の設定
攻撃範囲を設定
敵が砲台の攻撃範囲に入ったかどうかを判定するためにトリガーコライダーを使います。敵と砲台それぞれにコライダーを追加し、接触したら攻撃範囲に入ったと判断します。
まずは砲台の設定です。
「Assets/Prefabs」フォルダの「Turret」オブジェクトを選択し、インスペクターの右上にある「Open」ボタンをクリックしてプレハブの編集モードに切り替えます。「Turret」オブジェクトをダブルクリックでもOKです。

インスペクターの「Add Component」ボタンをクリックして「Rigidbody 2D」コンポーネントを追加します。コライダーを使って当たり判定をするにはRigidbodyが必要です。

インスペクターの「Rigidbody 2D > Body Type」を「Kinematic」に変更します。Kinematicは物理法則の影響を受けないようにする設定です。

「Constraints > Freeze Rotation」の「Z」にチェックを入れます。これでオブジェクトが回転しなくなります。

ヒエラルキーの「Turret」オブジェクトの上で右クリック「Create Empty」を選択して「Turret」の配下にオブジェクトを作成します。名前を「AttackRange」にします。

「AttackRange」オブジェクトのインスペクターで「Add Component」ボタンをクリックして「Circle Collider 2D」コンポーネントを追加します。

インスペクターで「Circle Collider 2D > Is Trigger」にチェックを入れます。

「Circle Collider 2D > Radius」を「2」に変更します。これが砲台の攻撃範囲になります。

Sceneビューには緑色のラインでコライダーの形が表示されます。
攻撃範囲に入った敵を破壊するスクリプト
「Assets/Scripts」で右クリック「Create > C# Script」を選択して新しいスクリプトを作成します。名前を「TurretController」にします。
TurretController.csの内容は次のとおりです。
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public class TurretController : MonoBehaviour
{
    private void OnTriggerStay2D(Collider2D collision)
    {
        // 攻撃範囲用のコライダーにTagがEnemyのオブジェクトが侵入したら
        if (collision.tag == "Enemy")
        {
            Debug.Log("敵発見");
            Destroy(collision.gameObject);
        }
    }
}OnTriggerStay2D()はオブジェクトのコライダーが他のコライダーと接触し続ける間、呼び出されるメソッドです。Collider2D collisionには接触したコライダーの情報が入っています。
if (collision.tag == "Enemy")は接触したコライダーのタグがEnemyだった場合に以下の処理を実行します。ということは、あとで敵オブジェクトにタグを設定するということですね。
「Assets . Prefabs」フォルダの「Turret」オブジェクトを選択します。インスペクターの右上にある「Open」ボタンをクリックしてプレハブの編集モードに切り替えます。
TurretController.csをTurretオブジェクトにドラッグ&ドロップしてアタッチします。
敵の設定
敵オブジェクトにタグとコライダーを設定します。
タグの設定
ヒエラルキーのなんらかのオブジェクトを選択して、インスペクターの「Tag」をクリックし、プルダウンの「Add Tag…」を選択します。

「Tags」の「+」をクリックしてタグを追加します。名前は「Enemy」にします。

「Assets_Prefabs」の「Enemy」オブジェクトをダブルクリックして、プレハブの編集モードに入ります。インスペクターでタグをEnemyに設定します。

ヒエラルキーの「Enemy」オブジェクトのタグもEnemyになっているか確認してください。なっていなければEnemyにしてください。
コライダーの設定
ヒエラルキーの「Enemy」オブジェクトを選択し、インスペクターで「Add Component」ボタンをクリックしてBox Collider 2Dコンポーネントを追加します。

コライダーにはいろいろな形状がありますが、今回はBox Colliderを使います。その理由は、最も負荷が低いからです。敵の形に合わせて形状を変えられるPolygon Colliderを使いたくなりますが、ゲーム画面上に複数の敵が同時に出現することを考えると負荷の低いBox Colliderが最適な選択でしょう。
コライダーの負荷ランキングはだいたい次のとおりです。
Polygon >>>>> Capsule > Circle > Box
Polygon以外はどれを使っても大差ないと思います。
インスペクターの「Box Collider 2d > Edit Collider」のアイコンをクリックするとSceneビューに緑色のラインが表示され、コライダーのサイズを変更できるようになります。

敵オブジェクトの大きさに合わせてコライダーのサイズを変更します。

動作確認
ゲームをプレイして動作確認をしてみます。
砲台の攻撃範囲に入った敵が破壊されました。いえい。
砲台の攻撃力と攻撃間隔の実装
今のところ、砲台が攻撃すると一撃で敵を倒してしまいます。砲台の攻撃力と敵のHPを設定して複数回の攻撃で敵を倒すようにします。
砲台に攻撃力と攻撃間隔を実装する
TurretController.csスクリプトを修正します。
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; // 敵
    private void OnTriggerStay2D(Collider2D collision)
    {
        //条件を変更
        // 攻撃範囲用のコライダーにTagがEnemyのオブジェクトが侵入したら
        //if (collision.tag == "Enemy")
        // 攻撃中ではない場合で、かつ敵の情報を未取得の場合
        if (!isAttack && !enemy)
        {
            Debug.Log("敵発見");
            if (collision.gameObject.TryGetComponent(out enemy)) // 敵を発見したら
            {
                // 攻撃状態にする
                isAttack = true;
                // 攻撃間隔の管理
                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フレーム処理を中断する
        }
    }
    /// <summary>
    /// 攻撃
    /// </summary>
    private void Attack()
    {
        Debug.Log("攻撃");
    }
}
7〜14行目、4つの変数を追加しました。
18〜22行目、敵を攻撃開始する条件を変更します。isAttackは28行目のisAttack = trueで設定され、enemyは25行目のcollision.gameObject.TryGetComponent(out enemy)で設定されます。
25行目、衝突したオブジェクトにEnemyControllerコンポーネントがあれば、それを取得して enemy に格納します。
30行目、攻撃間隔を管理するコルーチン(ManageAttacks())を開始します。
35〜53行目、攻撃間隔を管理するコルーチンです。攻撃中フラグ(isAttack)が有効な間、攻撃間隔(attackInterval)ごとに攻撃を繰り返します。
55〜61行目、攻撃関数です。今のところ、デバッグログを出力するだけですが、あとで敵のHPを減少させる処理を追加します。
動作確認
ゲームを実行して動作確認をします。Consoleに「敵発見」「攻撃間隔管理」「攻撃」のログが出力されていれば成功です。

フレームレートの固定
デバッグログで「攻撃」のログが大量に出力されています。攻撃間隔は60フレーム(1秒)に設定してるのになぜ!これはUnityの初期設定ではPCのスペックによってフレームレートが変動するためです。
フレームレートを固定しましょう。GameManagerスクリプトを作成します。
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public class GameManager : MonoBehaviour
{
    [SerializeField]
    private int targetFrameRate = 60; // フレームレートの目標値
    void Awake()
    {
        FixFrameRate(); // フレームレートを固定
    }
    /// <summary>
    /// フレームレートを固定
    /// </summary>
    private void FixFrameRate()
    {
        QualitySettings.vSyncCount = 0; // V-Sync(垂直同期)を無効化
        Application.targetFrameRate = targetFrameRate; // アプリケーションのフレームレートを設定
    }
}ヒエラルキーで空のオブジェクトを作成、名前を「GameManager」にします。そして、このスクリプトをドラッグ&ドロップしてアタッチします。
敵が攻撃範囲を外れたら攻撃をやめる
今のところ、敵が攻撃範囲の外に出ても砲台は攻撃を続けています。敵が攻撃範囲を外れたら攻撃をやめる処理を実装します。
TurretController.csスクリプトを修正します。
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public class TurretController : MonoBehaviour
{
    // ...省略...
    private void OnTriggerExit2D(Collider2D collision)
    {
        if (collision.gameObject.TryGetComponent(out enemy))
        {
            Debug.Log("敵なし");
            isAttack = false;
            enemy = null;
        }
    }
}9行目、OnTriggerExit2D()メソッドは砲台のコライダーから敵オブジェクトが退出したときに呼び出されます。引数collisionには敵オブジェクトの情報が入ります。
11行目、退出したオブジェクトが敵ならば以下の処理を実行します。
動作確認をします。敵が攻撃範囲を外れたときに「敵なし」のログが出力されたら成功です。

敵のHPの実装
敵にHPを設定して、砲台に攻撃されたらHPが減少していき0になったら死亡の処理を実装します。
EnemyController.csスクリプトを修正します。
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using DG.Tweening; // DOTweenを使うために必要な宣言
using System.Linq; // Linqを使うために必要な宣言
public class EnemyController : MonoBehaviour
{
    [SerializeField, Header("移動経路の情報")]
    private PathData pathData;
    [SerializeField, Header("移動速度")]
    private float speed;
    // 変数を追加
    [SerializeField, Header("最大HP")]
    private int maxHp;
    [SerializeField, Header("HP")]
    private int hp;
    private Tween tween; // DOPathメソッドの処理を代入しておく変数
    // ここまで
    private Vector3[] path; // pathDataから取得した座標を格納するための配列
    private Animator animator; // Animatorコンポーネントの取得
    void Start()
    {
        // 追加
        hp = maxHp; // 敵のHPを設定
        // ここまで
        // Animatorコンポーネントを取得して代入
        TryGetComponent(out animator);
        // 経路を取得
        path = pathData.pathArray.Select(x => x.position).ToArray();
        // 経路の総距離を計算
        float totalDistance = CalculatePathLength(path);
        // 移動時間を計算 (距離 ÷ 速度)
        float moveDuration = totalDistance / speed;
        // 経路に沿って移動する処理を変数に代入するように修正
        // 経路に沿って移動
        //transform.DOPath(path, moveDuration)
        //         .SetEase(Ease.Linear)
        //         .OnWaypointChange(x => ChangeWalkingAnimation(x));
        // 経路に沿って移動する処理をtween変数に代入
        tween = transform.DOPath(path, moveDuration)
                         .SetEase(Ease.Linear)
                         .OnWaypointChange(x => ChangeWalkingAnimation(x));
        // ここまで
    }
    /// <summary>
    /// 経路の総距離を計算
    /// </summary>
    /// <param name="path">経路座標の配列</param>
    /// <returns>経路の総距離</returns>
    private float CalculatePathLength(Vector3[] path)
    {
        //...省略...
    }
    /// <summary>
    /// 敵の進行方向を取得してアニメを変更
    /// </summary>
    private void ChangeWalkingAnimation(int index)
    {
        //...省略...
    }
    // メソッドを2つ追加
    /// <summary>
    /// ダメージ計算
    /// </summary>
    /// <param name="amount"></param>
    public void CalcDamage(int amount)
    {
        // HPの値を減算した結果値を最小値と最大値の範囲内に収まるようにして更新
        hp = Mathf.Clamp(hp -= amount, 0, maxHp);
        Debug.Log("残りHP : " + hp);
        if (hp <= 0) // HPが0以下になったら
        {
            DestroyEnemy(); // 敵を破壊
        }
    }
    /// <summary>
    /// 敵の破壊
    /// </summary>
    public void DestroyEnemy()
    {
        tween.Kill(); // tween変数に代入されている処理を終了する
        Destroy(gameObject); // 敵の破壊
    }
    // ここまで
}変数を3つ追加しました。
- maxHp: 敵の最大HPを設定します。
- hp: 敵の現在のHPを保持する変数です。
- tween: DOTweenによるアニメーション処理を代入する変数です。
26行目、敵の現在のHP(hp)を最大HP(maxHp)に初期化します。
43行目、DOPath処理をtween変数に代入しています。これにより、敵が破壊されるときに移動アニメーションを停止(Kill)できます。
CalcDamage()メソッドは敵がダメージを受けたときに呼び出されるメソッドです。TurretController.csスクリプトから呼び出します。ダメージ量(amount)をHPから減算し、0になったらDestroyEnemy()メソッドを呼び出して敵を破壊します。
DestroyEnemy()メソッドは敵を破壊するメソッドです。tween.Kill()で敵の移動アニメーションを停止し、Destroy(gameObject)で敵オブジェクトを破壊します。
プロジェクトビューの「Prefabs/Enemy」をダブルクリックして、プレハブの編集モードに入ります。インスペクターで「Max Hp」を「3」にします。「Hp」はスクリプトによって計算されるので変更しなくてOKです。

TurretController.csスクリプトを修正します。
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; // 敵
    private void OnTriggerStay2D(Collider2D collision)
    {
        // ...省略...
    }
    /// <summary>
    /// 攻撃間隔管理
    /// </summary>
    public IEnumerator ManageAttacks()
    {
        // ...省略...
    }
    /// <summary>
    /// 攻撃
    /// </summary>
    private void Attack()
    {
        Debug.Log("攻撃");
        // 追加
        // 敵にダメージを与える
        enemy.CalcDamage(attackPower);
        // ここまで
    }
    private void OnTriggerExit2D(Collider2D collision)
    {
        // ...省略...
    }
}
37行目、EnemyControllerのCalcDamage()メソッドを呼び出して、敵にダメージを与えます。
動作確認をしてみます。
攻撃を受けると敵の残りHPが減少してゆき、0になったら敵が破壊されました。
EnemyオブジェクトとPathオブジェクトをヒエラルキーから削除します。
次の手順で敵をプレハブから生成する処理を実装します。
さいごに
だんだんタワーディフェンスらしくなってきました。楽しー。
でわでわ


 
	
コメント