Parallels Desktop アースデイセール実施中 25%OFF

【Unity】タワーディフェンス(14) 砲台のアップグレード機能 その2【クソゲー制作】

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

Unity初心者が2Dタワーディフェンスを作っています。今回は前回に引き続き砲台のアップグレード機能です。コマゴマとした機能を追加していきます。

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

砲台設置直後に情報パネルを表示する

現在の仕様では、砲台を設置した直後に砲台情報パネルが表示されません。設置したらすぐに情報パネルを表示したいですね。

TurretGeneratorスクリプトを修正します。たった1行の追加、とても簡単でした。

public class TurretGenerator : MonoBehaviour
{
    // ...省略...

    /// <summary>
    /// 砲台生成
    /// </summary>
    /// <param name="gridPos"></param>
    private void GenerateTurret(Vector3Int gridPos)
    {
        // 砲台が選択されていなければ何もしない
        if (selectedTurretData == null)
        {
            Debug.Log("砲台が選択されていません");
            return;
        }
        // 配置済みの場合は処理を中断
        if (occupiedCells.Contains(gridPos))
        {
            Debug.Log("このセルにはすでに砲台が配置されています");
            return;
        }
        // クリックした位置に砲台を配置
        GameObject turret = Instantiate(turretPrefab, gridPos, Quaternion.identity);
        // 砲台の位置がタイルの左下を 0,0 として生成しているので、タイルの中央にくるように位置を調整
        turret.transform.position = new Vector2(turret.transform.position.x + 0.5f, turret.transform.position.y + 0.5f);
        // TurretControllerを取得する
        TurretController turretController = turret.GetComponent<TurretController>();
        // 砲台データの初期化
        turretController.InitializeTurret(selectedTurretData, 1);
        Debug.Log($"生成された砲台: {selectedTurretData.name}");
        // ここから
        turretController.OnTurretClicked(); // マップの砲台がクリックされたときの処理
        // ここまで
        // 配置されたセルを登録
        occupiedCells.Add(gridPos);
        // 砲台を設置したら選択をリセット
        selectedTurretData = null;
        selectedTurretIcon.SetActive(false); // 追随アイコンを非表示
    }

    // ...省略...
}

GenerateTurretメソッド内でOnTurretClickedを実行します。OnTurretClickedはマップ上の砲台がクリックされたときの処理を実行するメソッドで、情報パネルを表示してくれます。

マップ上の砲台にレベルを表示する

砲台のレベルはサイドバーにも表示されるのですが、マップ上の砲台にもレベルをあらわすマークを表示したいんです。以下のように「∧」の数で表現してみます。

タワーディフェンス160
STEP

level1.pnglevel5.pngの画像を用意します。

タワーディフェンス161

これらを「Assets > Images > Icon」にドラッグ&ドロップしてインポートします。

STEP

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

Turretオブジェクトの配下に「2D Object > Sprites > Square」オブジェクトを作成します。名前を「LevelIcon」に変更します。

これがレベルアイコンを表示するためのオブジェクトです。

STEP

表示の重なり順の設定です。

LevelIconのインスペクターで「Sprite Renderer > Additional Settings > Sorting Layer」「Object」に、「Order in Layer」「2」に設定します。これで砲台の前面にレベルアイコンが表示されるようになります。

タワーディフェンス162
STEP

レベルアイコンの位置と色の設定をします。

インスペクターの「Sprite Renderer > Sprite」level5.pngをドラッグ&ドロップしてアサインします。「Rect Transform」で位置とスケールを調整し、「Sprite Renderer > Color」で色を設定します。

タワーディフェンス163
STEP

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

public class TurretController : MonoBehaviour
{
    // ...省略...
    // ここから
    [SerializeField] private SpriteRenderer levelIconSpriteRenderer; // レベルアイコンのSpriteRenderer
    [SerializeField] private Sprite[] levelIcons; // レベルアイコンの配列
    // ここまで
    // ...省略...

    // ...省略...

    /// <summary>
    /// 砲台データを初期化
    /// </summary>
    public void InitializeTurret(TurretSetting.TurretData data, int level)
    {
        turretData = data;
        turretLvData = DBManager.instance.turretSetting.GetTurretData(data.id, level);
        attackPower = turretLvData.attackPower; // 攻撃力を設定
        attackInterval = turretLvData.attackInterval; // 攻撃間隔を設定
        attackRange.radius = turretLvData.attackRange; // 攻撃範囲を設定
        turretHeadSpriteRenderer.sprite = turretData.turretHeadSprite; // 砲身の画像を設定
        // ここから
        // レベルアイコンを更新
        if (level >= 1 && level <= levelIcons.Length)
        {
            levelIconSpriteRenderer.sprite = levelIcons[level - 1];
        }
        else
        {
            Debug.LogWarning($"レベル {level} のアイコンが設定されていません");
        }
        // ここまで
        Debug.Log($"砲台を初期化: {turretData.name}");
    }

    // ...省略...
}

InitializeTurretメソッド内にレベルアイコンを更新する処理を入れました。

STEP

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

Turretオブジェクトのインスペクターの「Turret Controller (Script) > Level Icon Sprite Renderer」「LevelIcon」オブジェクトをドラッグ&ドロップしてアタッチします。また、「Level Icons」level1.pnglevel5.pngをドラッグ&ドロップしてアタッチします。

タワーディフェンス164

以上で、マップ上の砲台にレベルアイコンが表示されるようになりました。いえい。

砲台のアップグレードに時間をかける

現在のところ、強化ボタンを押すと瞬時に砲台がアップグレードされるのですが、時間をかけてアップグレードするようにします。アップグレード中は敵を攻撃できないといういぢわる機能の実装です。

STEP

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

public class TurretController : MonoBehaviour
{
    // ...省略...


    // ここから
    /// <summary>
    /// 砲台をアップグレードするコルーチン
    /// </summary>
    //public void UpgradeTurret()
    public IEnumerator UpgradeTurretCoroutine()
    // ここまで
    {
        int newLevel = turretLvData.level + 1; // アップグレード後のレベル
        // ここから
        float upgradeTime = newLevel * 6.0f - 11; // 待機時間
        Debug.Log($"アップグレード開始: レベル {turretLvData.level} → {newLevel} (所要時間 {upgradeTime} 秒)");
        yield return new WaitForSeconds(upgradeTime); // 指定時間待機
        // ここまで
        // アップグレード後の砲台情報を取得
        TurretSetting.TurretLvData newTurretLvData = DBManager.instance.turretSetting.GetTurretData(turretData.id, newLevel);
        if (newTurretLvData == null)
        {
            Debug.LogWarning($"砲台 {turretData.id} のレベル {newLevel} のデータが見つかりません");
            // ここから
            //return;
            yield break;
            // ここまで
        }
        turretLvData = newTurretLvData; // データを更新
        InitializeTurret(turretData, newLevel); // 砲台を初期化
        // ...省略...
    }
}

UpgradeTurretメソッドをコルーチンに変更しました。

アップグレードの待機時間(upgradeTime)はTurretSettingに持たせることもできるのですが、今回はnewLevel * 6.0f - 11で計算することにしました。レベル2へのアップグレードなら1秒、レベル3へのアップグレードなら7秒ですね。

STEP

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

public class SideBarManager : MonoBehaviour
{
    // ...省略...

    /// <summary>
    /// 選択された砲台をアップグレードする
    /// </summary>
    private void UpgradeSelectedTurret()
    {
        if (selectedTurret != null)
        {
            // ここから
            //selectedTurret.UpgradeTurret();
            StartCoroutine(selectedTurret.UpgradeTurretCoroutine());
            // ここまで
        }
    }
}

UpgradeTurretメソッドを呼び出していた部分を、UpgradeTurretCoroutineコルーチンの開始に変更します。

アップグレード中は攻撃しない

砲台のアップグレード中は敵を攻撃しないようにします。

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

public class TurretController : MonoBehaviour
{
    // ...省略...
    // ここから
    private bool isUpgrading = false; // アップグレード中フラグ
    // ここまで

    // ...省略...

    /// <summary>
    /// 攻撃間隔管理
    /// </summary>
    public IEnumerator ManageAttacks()
    {
        Debug.Log("攻撃間隔管理");
        // 攻撃状態の間ループ処理を繰り返す
        while (isAttacking)
        {
            // ここから
            //if (targetEnemy) // ターゲットが存在する場合
            if (targetEnemy && !isUpgrading) // ターゲットが存在し、アップグレード中でない場合
            // ここまで
            {
                Attack(); // 攻撃を実行
            }
            // 次の攻撃まで待機
            yield return new WaitForSeconds(attackInterval / 60.0f);
        }
    }

    // ...省略...

    /// <summary>
    /// 砲身を敵の方向に回転させる
    /// </summary>
    private void RotateTurretHeadTowardsEnemy()
    {
        // ここから
        // ターゲットが存在しない場合またはアップグレード中の場合、何もしない
        //if (!targetEnemy) return;
        if (!targetEnemy || isUpgrading) return;
        // ここまで
        // 敵の位置と砲身の位置の差分を計算
        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);
    }

    // ...省略...

    /// <summary>
    /// 砲台をアップグレードするコルーチン
    /// </summary>
    public IEnumerator UpgradeTurretCoroutine()
    {
        // ここから
        isUpgrading = true; // アップグレード開始
        // ここまで
        int newLevel = turretLvData.level + 1; // アップグレード後のレベル
        float upgradeTime = newLevel * 6.0f - 11; // 待機時間
        Debug.Log($"アップグレード開始: レベル {turretLvData.level} → {newLevel} (所要時間 {upgradeTime} 秒)");
        // アップグレード後の砲台情報を取得
        TurretSetting.TurretLvData newTurretLvData = DBManager.instance.turretSetting.GetTurretData(turretData.id, newLevel);
        if (newTurretLvData == null)
        {
            Debug.LogWarning($"砲台 {turretData.id} のレベル {newLevel} のデータが見つかりません");
            // ここから
            isUpgrading = false; // アップグレード終了
            // ここまで
            yield break;
        }
        // ...省略...
        // ここから
        isUpgrading = false; // アップグレード終了
        // ここまで
    }
}

5行目、アップグレード中かどうかをあらわす変数isUpgradingを定義します。

ManageAttacksコルーチン内で、アップグレード中でない場合のみ攻撃を実行するようにします。

RotateTurretHeadTowardsEnemyメソッド内で、アップグレード中の場合、砲身を回転させないようにします。

UpgradeTurretCoroutineコルーチン内で、isUpgradingフラグを切り替えます。

アップグレード中にインジケータを表示する

砲台のアップグレード中に、残り時間をあらわす円形のインジケータを表示します。

STEP

ヒエラルキーでCanvasの配下に「UI > Image」オブジェクトを作成します。名前を「UpgradeIndicatorCanvas」にします。これがインジケータの背景になります。

STEP

UpgradeIndicatorCanvasの子要素として「UI > Image」オブジェクトを作成します。名前を「UpgradeIndicatorFill」にします。これがインジケータの塗りつぶし部分になります。

STEP

それぞれのオブジェクトの「Image > Source Image」に白い円形の画像(circle.png)をアサインします。

「Image > Color」で色を設定します。UpgradeIndicatorCanvasは灰色、UpgradeIndicatorFillは黄色にしました。

「Rect Transform」「Width」, 「Height」でサイズを設定します。両方とも「60」にしました。

STEP

UpgradeIndicatorFill「Image > Image Type」「Filled」に設定します。「Fill Method」「Radial 360」に設定します。「Fill Origin」「Top」に設定します。

タワーディフェンス165

この設定で、円形の画像が時間とともに埋められていくインジケータの準備が整いました。

STEP

UpgradeIndicatorCanvas「Assets > Prefabs」フォルダにドラッグ&ドロップしてプレハブ化します。ヒエラルキーのUpgradeIndicatorCanvasオブジェクトは削除します。

STEP

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

using System.Collections;
using System.Collections.Generic;
using JetBrains.Annotations;
using UnityEditor.PackageManager;
using UnityEngine;
// ここから
using UnityEngine.UI; // UIを使用するための宣言
// ここまで

public class TurretController : MonoBehaviour
{
    // ...省略...
    // ここから
    [SerializeField] private GameObject upgradeIndicatorPrefab; // インジケータのプレハブ
    private GameObject currentIndicator; // 現在のインジケータ
    // ここまで

    // ...省略...

    /// <summary>
    /// 砲台をアップグレードするコルーチン
    /// </summary>
    public IEnumerator UpgradeTurretCoroutine()
    {
        isUpgrading = true; // アップグレード開始
        // ここから
        // インジケータを生成
        currentIndicator = Instantiate(upgradeIndicatorPrefab, transform.position, Quaternion.identity);
        // 親をCanvasに設定
        Canvas canvas = FindObjectOfType<Canvas>();
        currentIndicator.transform.SetParent(canvas.transform, false);
        // ワールド座標をスクリーン座標に変換
        Vector2 screenPosition = Camera.main.WorldToScreenPoint(transform.position);
        currentIndicator.GetComponent<RectTransform>().position = screenPosition;
        // 塗りつぶしImageを取得
        Image fillImage = currentIndicator.transform.Find("UpgradeIndicatorFill").GetComponent<Image>();
        // ここまで
        int newLevel = turretLvData.level + 1; // アップグレード後のレベル
        float upgradeTime = newLevel * 6.0f - 11; // 待機時間
        Debug.Log($"アップグレード開始: レベル {turretLvData.level} → {newLevel} (所要時間 {upgradeTime} 秒)");
        // ここから
        //yield return new WaitForSeconds(upgradeTime); // 指定時間待機
        float elapsedTime = 0f; // 経過時間
        while (elapsedTime < upgradeTime)
        {
            elapsedTime += Time.deltaTime;
            fillImage.fillAmount = elapsedTime / upgradeTime; // 塗りつぶし量を更新
            yield return null;
        }
        // ここまで
        // アップグレード後の砲台情報を取得
        TurretSetting.TurretLvData newTurretLvData = DBManager.instance.turretSetting.GetTurretData(turretData.id, newLevel);
        if (newTurretLvData == null)
        {
            Debug.LogWarning($"砲台 {turretData.id} のレベル {newLevel} のデータが見つかりません");
            isUpgrading = false; // アップグレード終了
            // ここから
            Destroy(currentIndicator); // インジケータを削除
            // ここまで
            yield break;
        }
        turretLvData = newTurretLvData; // データを更新
        InitializeTurret(turretData, newLevel); // 砲台を初期化
        // ...省略...
        isUpgrading = false; // アップグレード終了
        // ここから
        Destroy(currentIndicator); // インジケータを削除
        // ここまで
    }
}

インジケータのプレハブを入れておく変数upgradeIndicatorPrefabと現在のインジケータを入れておく変数currentIndicatorを定義します。

UpgradeTurretCoroutine内に以下の処理を入れます。

  • 28行目: プレハブからインジケータを生成
  • 30-31行目: インジケータのオブジェクトをCanvasの配下に移動
  • 33-34行目: ワールド座標をスクリーン座標に変換
  • 36行目: 塗りつぶし画像を取得
  • 43-49行目: 経過時間に応じて塗りつぶし量を更新

アップグレードが終わったら、currentIndicatorを削除します。

インジケータをCanvasの配下に移動するのは、インジケータがUIオブジェクトで、UIオブジェクトCanvasの配下にないと動かないからです。もうちょい正確にいうと親オブジェクトにCanvasコンポーネントがついている必要があります。なので、Canvasの配下に移動するのではなく、砲台オブジェクトにCanvasコンポーネントを追加でもいけるかも。

STEP

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

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

タワーディフェンス166

以上で、砲台のアップグレード中に円形のインジケータが表示されるようになりました。いえい。

アップグレード中はアップグレードできないようにする

砲台のアップグレード中に同じ砲台をアップグレードできないようにします。強化ボタンを押せないようにすればいいですね。

STEP

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

public class SideBarManager : MonoBehaviour
{
    // ...省略...

    // ここから
    /// <summary>
    /// 強化ボタンのinteractableを切り替え
    /// </summary>
    public void SetUpgradeButtonInteractable(bool interactable)
    {
        upgradeButton.interactable = interactable;
    }
    // ここまで
}

強化ボタンのinteractableを切り替えるメソッドを追加しました。

STEP

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

public class TurretController : MonoBehaviour
{
    // ...省略...

    /// <summary>
    /// 砲台をアップグレードするコルーチン
    /// </summary>
    public IEnumerator UpgradeTurretCoroutine()
    {
        isUpgrading = true; // アップグレード開始
        // インジケータを生成
        currentIndicator = Instantiate(upgradeIndicatorPrefab, transform.position, Quaternion.identity);
        // 親をCanvasに設定
        Canvas canvas = FindObjectOfType<Canvas>();
        currentIndicator.transform.SetParent(canvas.transform, false);
        // ワールド座標をスクリーン座標に変換
        Vector2 screenPosition = Camera.main.WorldToScreenPoint(transform.position);
        currentIndicator.GetComponent<RectTransform>().position = screenPosition;
        // 塗りつぶしImageを取得
        Image fillImage = currentIndicator.transform.Find("UpgradeIndicatorFill").GetComponent<Image>();
        int newLevel = turretLvData.level + 1; // アップグレード後のレベル
        float upgradeTime = newLevel * 6.0f - 11; // 待機時間
        Debug.Log($"アップグレード開始: レベル {turretLvData.level} → {newLevel} (所要時間 {upgradeTime} 秒)");
        float elapsedTime = 0f; // 経過時間
        // ここから
        SideBarManager sideBarManager = FindObjectOfType<SideBarManager>(); // SideBarManagerを取得
        if (sideBarManager != null)
        {
            sideBarManager.SetUpgradeButtonInteractable(false); // 強化ボタンを非活性化
        }
        else
        {
            Debug.LogError("SideBarManager が見つかりません");
        }
        // ここまで
        while (elapsedTime < upgradeTime)
        {
            elapsedTime += Time.deltaTime;
            fillImage.fillAmount = elapsedTime / upgradeTime; // 塗りつぶし量を更新
            yield return null;
        }
        // アップグレード後の砲台情報を取得
        TurretSetting.TurretLvData newTurretLvData = DBManager.instance.turretSetting.GetTurretData(turretData.id, newLevel);
        if (newTurretLvData == null)
        {
            Debug.LogWarning($"砲台 {turretData.id} のレベル {newLevel} のデータが見つかりません");
            isUpgrading = false; // アップグレード終了
            Destroy(currentIndicator); // インジケータを削除
            yield break;
        }
        turretLvData = newTurretLvData; // データを更新
        InitializeTurret(turretData, newLevel); // 砲台を初期化
        // UIの更新
        // ここから
        //SideBarManager sideBarManager = FindObjectOfType<SideBarManager>(); // SideBarManagerを取得
        // ここまで
        if (sideBarManager != null)
        {
            sideBarManager.ShowTurretInfo(turretData, turretLvData, true, this); // 砲台情報パネルを表示
            if (newLevel < 5) // レベルが5未満なら
            {
                sideBarManager.ShowTurretUpgradeInfo(turretData, turretLvData, true); // 強化パネルを表示
            }
            else // レベルが5以上なら
            {
                sideBarManager.HideTurretUpgradeInfo(); // 強化パネルを非表示
            }
            // ここから
            sideBarManager.SetUpgradeButtonInteractable(true); // 強化ボタンを活性化
            // ここまで
        }
        else
        {
            Debug.LogError("SideBarManager が見つかりません");
        }
        isUpgrading = false; // アップグレード終了
        Destroy(currentIndicator); // インジケータを削除
    }
}

UpgradeTurretCoroutine内で、アップグレードを開始したら強化ボタンを非活性化し、アップグレードが終了したら強化ボタンを活性化します。

アップグレード中に攻撃範囲を非表示にする

砲台をアップグレードすると攻撃範囲が変わることがあるので、アップグレード前の攻撃範囲が表示されたままになっているとよろしくない。というわけで、アップグレードが始まったら攻撃範囲を非表示にします。

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

public class TurretController : MonoBehaviour
{
    // ...省略...

    /// <summary>
    /// 砲台をアップグレードするコルーチン
    /// </summary>
    public IEnumerator UpgradeTurretCoroutine()
    {
        isUpgrading = true; // アップグレード開始
        // ここから
        lineRenderer.enabled = false; // 攻撃範囲を非表示
        // ここまで
        // ...省略...
    }
}

たった1行の変更でした。

マップをクリックしたらタイルをハイライト表示する

マップ上のどの砲台が選択されているのかをわかりやすくしたいので、クリックしたタイルをハイライトします。

STEP

ハイライト用の画像(roundedsquare.png)を用意して、「Assets > Images > Icon」にインポートします。

roundedsquare.pngはこんな感じの画像です。背景がグレーに見えますが実際は透明です。

タワーディフェンス167
STEP

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

STEP

TileHighlighterのインスペクターで「Sprite Renderer > Sprite」roundedsquare.pngをドラッグ&ドロップしてアサインします。

タワーディフェンス168
STEP

「Transform > Scale」を調整してタイルのサイズに合うようにします。私はX: 0.4, Y: 0.4にしました。

「Sprite Renderer > Color」で色を設定します。私は半透明の白にしました。

STEP

「Sprite Renderer > Additional Settings > Sorting Layer」「Way」に、「Order in Layer」「1」に設定します。これで、ハイライト画像がマップの道より手前、砲台より奥に表示されます。

タワーディフェンス169
STEP

TileHighlighterのインスペクターで左上のチェックを外してオブジェクトを非表示にしておきます。

タワーディフェンス170
STEP

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

public class GameManager : MonoBehaviour
{
    // ...省略...
    // ここから
    [SerializeField] public GameObject tileHighlighter; // ハイライト用オブジェクト
    private Vector2 tileSize = new Vector2(1, 1); // マス目のサイズ
    // ここまで

    // ...省略...

    /// <summary>
    /// マウスのクリックを検出
    /// </summary>
    void DetectClick()
    {
        // UIがクリックされた場合は何もしない
        if (EventSystem.current.IsPointerOverGameObject())
        {
            return;
        }
        // クリック位置のスクリーン座標をワールド座標に変換
        Vector2 worldPoint = Camera.main.ScreenToWorldPoint(Input.mousePosition);
        // ここから
        // クリック位置をタイルの中央に補正
        Vector2 snappedPosition = new Vector2(
            Mathf.Floor(worldPoint.x / tileSize.x) * tileSize.x + tileSize.x / 2,
            Mathf.Floor(worldPoint.y / tileSize.y) * tileSize.y + tileSize.y / 2
        );
        // ここまで
        // クリック位置から見えるオブジェクトに対してレイキャスト(Ignore Raycastレイヤーを除外)
        RaycastHit2D hit = Physics2D.Raycast(worldPoint, Vector2.zero, Mathf.Infinity, ~LayerMask.GetMask("Ignore Raycast"));
        if (hit.collider != null) // クリックしたオブジェクトにコライダーがあれば
        {
            TurretController turret = hit.collider.GetComponent<TurretController>(); // TurretControllerを取得
            if (turret != null) // クリックしたオブジェクトが砲台ならば
            {
                turret.OnTurretClicked(); // 砲台情報を表示
                // ここから
                //return;
                snappedPosition = turret.transform.position; // 砲台の座標を取得
                // ここまで
            }
        }
        else // 砲台以外をクリックした場合
        {
            sideBarManager.HideTurretInfo(); // 砲台情報パネルを非表示
            sideBarManager.HideTurretUpgradeInfo(); // 砲台強化情報パネルを非表示
        }
        // ここから
        // ハイライトの位置を更新
        if (tileHighlighter != null)
        {
            tileHighlighter.transform.position = snappedPosition;
            tileHighlighter.SetActive(true);
        }
        // ここまで
    }
}

25-28行目、クリックされたワールド座標をゲーム内のグリッドに吸着させるための計算をしています。これによって、クリック位置が常にタイルの中央に補正されます。

39-40行目、クリックしたオブジェクトが砲台だった場合、snappedPositionに砲台の座標を代入します。

51-55行目、tileHighlighterの位置をsnappedPositionに更新し表示します。

STEP

GameManagerオブジェクトのインスペクターで「Game Manager (Script) > TileHighlighter」TileHighlighterオブジェクトをドラッグ&ドロップしてアタッチします。

タワーディフェンス171

以上で、マップをクリックしたときにタイルがハイライトされるようになりました。

STEP

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

public class TurretGenerator : MonoBehaviour
{
    // ...省略...
    // ここから
    [SerializeField] private GameManager gameManager; // GameManagerの参照
    // ここまで

    // ...省略...

    /// <summary>
    /// 砲台アイコンを選択する
    /// </summary>
    public void SelectTurret(int index)
    {
        selectedTurretData = DBManager.instance.turretSetting.turretDataList[index];
        Debug.Log($"{selectedTurretData.name} を選択");
        // アイコンを表示
        selectedTurretIcon.SetActive(true);
        // 砲身のスプライトを設定
        SpriteRenderer iconRenderer = selectedTurretHead.GetComponent<SpriteRenderer>();
        iconRenderer.sprite = selectedTurretData.turretHeadSprite;
        TurretSetting.TurretLvData turretLvData = DBManager.instance.turretSetting.GetTurretData(selectedTurretData.id, 1);
        sideBarManager.ShowTurretInfo(selectedTurretData, turretLvData, false, null); // 砲台情報を表示
        sideBarManager.HideTurretUpgradeInfo(); // 砲台強化情報を非表示
        // ここから
        if (gameManager.tileHighlighter != null)
        {
            gameManager.tileHighlighter.SetActive(false);
        }
        // ここまで
    }
}

サイドバーの砲台アイコンを選択したときには、タイルのハイライト表示を消します。

STEP

TurretGeneratorオブジェクトのインスペクターで「Turret Generator (Script) > Game Manager」GameManagerオブジェクトをドラッグ&ドロップしてアサインします。

タワーディフェンス172

敵のHPバーを表示する

砲台のアップグレード機能とは関係ないのですが、敵のHPバーを実装するのを忘れていたのでここでやっておきましょう。

STEP

Canvasの配下に「UI > Slider」オブジェクトを作成します。名前を「HpBar」ににします。

STEP

HpBarの配下にある「Handle Slide Area」はスライダーの丸いつまみの部分です。不要なので削除します。

タワーディフェンス173
STEP

HpBar「Slider > Value」のつまみを動かすとバーが動くのがわかります。

タワーディフェンス174

しかし、0のときにもバーは少し残るし、1にしても完全には埋まりません。これはつまみの部分を考慮した設計になっているためです。

この問題を解決するには、Fill Area「Rect Transform」「Left」「Right」の値をを0にします。また、 Fill「Rect Transform」「Width」0にします。

STEP

HpBar「Rect Transform」「Width」「Height」を調整してHPバーの大きさを決めます。

また、BackgroundFill「Image > Color」をお好みの色に変更します。定番はBackgroundFillですね。

HPバーの角が丸くなっていますが、これはBackgroundFill「Image > Source Image」に画像がアサインされているからです。「None」に設定すると角が四角くなります。お好みでどうぞ。

STEP

HpBarオブジェクトの「Slider > Max Value」「1」に、「Value」「1」にしておきます。

STEP

C#スクリプトを新規作成し、名前を「HpBar」にします。内容は次のとおりです。

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

public class HpBar : MonoBehaviour
{
    [SerializeField] private Slider slider; // スライダー
    [SerializeField] private Vector3 offset; // 表示位置のオフセット

    /// <summary>
    /// HPバーの初期化
    /// </summary>
    /// <param name="maxHp">最大HP</param>
    public void InitializeHpBar(int maxHp)
    {
        slider.maxValue = maxHp;
        slider.value = maxHp;
    }

    /// <summary>
    /// HPバーの更新
    /// </summary>
    /// <param name="currentHp">現在のHP</param>
    public void UpdateHpBar(int currentHp)
    {
        slider.value = currentHp;
    }

    /// <summary>
    /// HPバーの座標更新
    /// </summary>
    public void UpdatePosition(Vector3 enemyPosition)
    {
        transform.position = Camera.main.WorldToScreenPoint(enemyPosition + offset);
    }
}
STEP

HpBarスクリプトをHpBarオブジェクトにドラッグ&ドロップしてアタッチします。

STEP

HpBarオブジェクトのインスペクターで「Hp Bar (Script) > Slider」HpBarオブジェクトをドラッグ&ドロップしてアサインします。

STEP

HpBarオブジェクトを「Assets > Prefabs」フォルダにドラッグ&ドロップしてプレハブ化します。ヒエラルキーのHpBarオブジェクトは削除します。

STEP

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

public class EnemyController : MonoBehaviour
{
    // ...省略...
    // ここから
    [SerializeField] private HpBar hpBarPrefab; // HPバーのプレハブ
    private HpBar hpBar; // HPバー
    // ここまで

    // ここから
    private void Update()
    {
        hpBar.UpdatePosition(transform.position); // HPバーの位置を更新
    }
    // ここまで

    /// <summary>
    /// 敵データを初期化
    /// </summary>
    public void InitializeEnemy(PathData selectedPath, GameManager gameManager, EnemySetting.EnemyData enemyData)
    {
        // ...省略...
        // ここから
        hpBar = Instantiate(hpBarPrefab, transform); // HPバーを生成
        hpBar.InitializeHpBar(maxHp); // HPバーの初期化
        hpBar.transform.SetParent(GameObject.Find("Canvas").transform, false); // Canvasの子にする
        // ここまで
    }

    // ...省略...

    /// <summary>
    /// ダメージ計算
    /// </summary>
    /// <param name="amount"></param>
    public void CalcDamage(int amount)
    {
        // HPの値を減算した結果値を、最小値と最大値の範囲内に収まるようにして更新
        hp = Mathf.Clamp(hp -= amount, 0, enemyData.maxHp);
        Debug.Log("残りHP : " + hp);
        // ここから
        hpBar.UpdateHpBar(hp); // HPバーの更新
        // ここまで
        if (hp <= 0) // HPが0以下になったら
        {
            DestroyEnemy(); // 敵を破壊
        }
    }

    /// <summary>
    /// 敵の破壊
    /// </summary>
    public void DestroyEnemy()
    {
        tween.Kill(); // tween変数に代入されている処理を終了する
        // ここから
        Destroy(hpBar.gameObject); // HPバーを破壊
        // ここまで
        Destroy(gameObject); // 敵の破壊
    }

    // ...省略...
}

5-6行目、hpBarPrefab変数とhpBar変数を定義します。

10-13行目、毎フレームごとに敵の動きに合わせてHPバーの位置を更新します。

23-25行目、InitializeEnemyメソッド内で、プレハブからHPバーを生成し、初期化し、Canvasの子要素にします。

41行目、CalcDamageメソッド内で、HPバーの表示を更新します。

56行目、敵が破壊されたらHPバーも破壊します。

STEP

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

インスペクターの「Enemy Controller (Script) > Hp Bar Prefab」HpBarプレハブをドラッグ&ドロップしてアサインします。

STEP

ゲームを実行して動作確認をします。

HPバーが敵の真ん中に表示されました。敵の上部に表示したいので位置を調整します。

「Assets > Prefabs > HpBar」をダブルクリックしてプレハブの編集モードに入ります。HpBarのインスペクターで「HpBar (Script) > Offset」を調整します。X: 0, Y: 0.6, Z: 0でいい感じに表示されるようになりました。いえい。

動作確認

最終的な動作確認をしましょう。

  • 砲台設置時に情報パネルが表示されるようになりました。
  • マップ上の砲台にレベルをあらわすマークが表示されるようになりました。
  • 砲台のアップグレード中にインジケータが表示されるようになりました。
  • マップ上の選択中の砲台がハイライト表示されるようになりました。
  • 敵のHPバーが表示されるようになりました。

さいごに

砲台のアップグレードに関連する細かな機能を追加しました。

今回学んだことは、UIオブジェクトは親オブジェクトにCanvasコンポーネントがないと動かないということです。

でわでわ

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

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

シェアしてね

コメント

コメントする

目次