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

【Unity】タワーディフェンス(17) ゴールドとスコア【クソゲー制作】

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

Unity初心者が2Dタワーディフェンスゲームを作っています。

今回はゴールド(お金)とスコアを実装していきます。

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

ゴールドの実装

こゲームではお金をゴールドと呼ぶことにします。そのゴールドを獲得したり支払ったりする処理を実装します。

敵破壊時にゴールドを受け取る

STEP

ゴールドの管理をするためのスクリプトGoldManagerを作成します。

using UnityEngine;
using TMPro; // TextMeshPro を使うために必要
using DG.Tweening; // DOTween を使うために必要

public class GoldManager : MonoBehaviour
{
    // シングルトンインスタンス
    public static GoldManager Instance { get; private set; }

    [SerializeField, Tooltip("現在の所持ゴールド")] private int currentGold = 100;
    [SerializeField, Tooltip("ゴールド表示用UIテキスト")] private TextMeshProUGUI goldText;

    // 現在のゴールド量(読み取り専用)
    public int CurrentGold => currentGold;

    private void Awake()
    {
        // シングルトンパターンの実装
        if (Instance == null)
        {
            Instance = this;
        }
        else if (Instance != this) // 既に別のインスタンスが存在する場合
        {
            Debug.LogWarning("GoldManager のインスタンスが既に存在するため、新しいインスタンスを破棄します。", this);
            Destroy(gameObject);
            return;
        }

        // UIテキストが設定されているか確認
        if (goldText == null)
        {
            Debug.LogError("GoldManager: goldText が Inspector で設定されていません。", this);
        }
    }

    private void Start()
    {
        // ゲーム開始時にUIとイベントを初期化
        UpdateGoldDisplay();
    }

    /// <summary>
    /// ゴールドを追加する
    /// </summary>
    /// <param name="amount">追加するゴールド量(正の数である必要があります)</param>
    public void AddGold(int amount)
    {
        if (amount <= 0)
        {
            Debug.LogWarning($"AddGold: 追加するゴールド量({amount})は正の数である必要があります。", this);
            return;
        }

        ChangeGold(amount);
    }

    /// <summary>
    /// ゴールドを消費する
    /// </summary>
    /// <param name="amount">消費するゴールド量(正の数である必要があります)</param>
    /// <returns>ゴールドが足りて消費できた場合は true、足りない場合は false</returns>
    public bool SpendGold(int amount)
    {
        if (amount <= 0)
        {
            Debug.LogWarning($"SpendGold: 消費するゴールド量({amount})は正の数である必要があります。", this);
            return false;
        }

        return ChangeGold(-amount);
    }

    /// <summary>
    /// ゴールドを増減する
    /// </summary>
    /// <param name="amount">増減するゴールド量(正の数で増加、負の数で減少)</param>
    /// <returns>ゴールドが足りて処理できた場合は true、足りない場合は false</returns>
    private bool ChangeGold(int amount)
    {
        int startValue = currentGold;
        int endValue = currentGold + amount;

        // ゴールドが足りない場合は処理を中断
        if (endValue < 0)
        {
            Debug.LogWarning($"ゴールドが足りません! 必要: {Mathf.Abs(amount)}, 所持: {currentGold}", this);
            return false;
        }

        // DOTween を使用してカウントアップ/カウントダウンアニメーション
        DOTween.To(() => startValue, x =>
        {
            startValue = x;
            if (goldText != null)
            {
                goldText.text = startValue.ToString();
            }
        }, endValue, 0.5f) // 0.5秒でアニメーション
        .SetEase(Ease.OutQuad) // スムーズなアニメーション
        .OnComplete(() =>
        {
            // アニメーション完了後に実際のゴールド値を更新
            SetGold(endValue);
        });

        Debug.Log($"{Mathf.Abs(amount)} ゴールド{(amount > 0 ? "追加" : "消費")}。 現在のゴールド: {endValue}", this);
        return true;
    }

    /// <summary>
    /// ゴールドの値を設定し、UI更新を行う
    /// </summary>
    /// <param name="newGoldValue">新しいゴールドの値</param>
    private void SetGold(int newGoldValue)
    {
        // 念のため負の値にならないようにチェック
        currentGold = Mathf.Max(0, newGoldValue);
        UpdateGoldDisplay();
    }

    /// <summary>
    /// ゴールド表示テキストを更新する
    /// </summary>
    private void UpdateGoldDisplay()
    {
        if (goldText != null)
        {
            goldText.text = currentGold.ToString();
        }
    }
}

Awake()シングルトンパターンを実装しています。ゲーム全体でただ一つだけのインスタンスしかないことを保証し、どこからでも簡単にアクセスできるようにしたい場合に使うヤツですね。

そして、以下のメソッドを定義しました。

  • AddGold(): ゴールドを追加する。処理はChangeGold()に投げています。
  • SpendGold(): ゴールドを消費する。処理はChangeGold()に投げています。
  • ChangeGold(): ゴールドを増減する。DOTweenを使用して、ゴールドの増減の表示をオシャンティにしています。
  • SetGold(): ゴールドの値を設定し、UI更新を行う。
  • UpdateGoldDisplay(): ゴールド表示テキストを更新する。
STEP

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

タワーディフェンス184
STEP

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

タワーディフェンス185
STEP

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

using UnityEngine;
using DG.Tweening;
using System.Linq;

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

    /// <summary>
    /// ダメージを受ける
    /// </summary>
    /// <param name="damageAmount">ダメージ量</param>
    public void TakeDamage(int damageAmount)
    {
        // HPの値を減算した結果値を、最小値と最大値の範囲内に収まるようにして更新
        currentHp = Mathf.Clamp(currentHp - damageAmount, 0, maxHp);
        // HPバーの更新
        hpBar.UpdateHpBar(currentHp);
        if (currentHp <= 0)
        {
            // HPが0以下になったら敵を破壊
            // ここから メソッド名変更
            //DestroyEnemy();
            DefeatEnemy();
            // ここまで
        }
    }

    // ここから メソッド名変更
    /// <summary>
    /// //敵を破壊する
    /// 敵を撃破する
    /// </summary>
    //private void DestroyEnemy()
    private void DefeatEnemy()
    // ここまで
    {
        // ここから
        // ゴールドを追加
        if (GoldManager.Instance != null)
        {
            if (enemyData != null) // enemyDataがnullでないことを確認
            {
                GoldManager.Instance.AddGold(enemyData.Reward);
                Debug.Log($"{enemyData.EnemyName} を倒して {enemyData.Reward} ゴールド獲得!", this);
            }
            else
            {
                Debug.LogError("EnemyData が設定されていません。ゴールドを獲得できませんでした。", this);
            }
        }
        else
        {
            Debug.LogError("GoldManager のインスタンスが見つかりません");
        }

        // tween変数に代入されている処理を終了する
        //moveTween.Kill();
        // HPバーを破壊
        //Destroy(hpBar.gameObject);
        // 敵の破壊
        //Destroy(gameObject);

        // クリーンアップ処理を呼び出す
        CleanupEnemy();
        // ここまで
    }

    //...省略...

    /// <summary>
    /// 敵がゴールに到達した
    /// </summary>
    public void ReachedGoal()
    {
        // ここから ゴール到達時はゴールドを獲得しない
        //DestroyEnemy(); // 敵を破壊する
        // クリーンアップ処理を呼び出す
        CleanupEnemy();
        // ここまで
    }

    // ここから
    /// <summary>
    /// 敵オブジェクトのクリーンアップ処理(移動停止、HPバー削除、GameObject削除)
    /// </summary>
    private void CleanupEnemy()
    {
        // 既に破棄処理中の場合や、オブジェクトがnullの場合は何もしない
        if (this == null || gameObject == null) return;

        // tween変数に代入されている処理を終了する
        moveTween?.Kill();
        moveTween = null; // Killした後は参照をnullにしておくのが安全

        // HPバーを破壊
        if (hpBar != null)
        {
            Destroy(hpBar.gameObject);
            hpBar = null; // 破棄後は参照をnullに
        }

        // 敵本体の破壊
        Destroy(gameObject);
    }
    // ここまで
}

これまで、砲台が敵を撃破したときと敵が城に到達したときのどちらもDestroyEnemy()メソッドで敵を削除していました。が、ゴールドを獲得するのは撃破したときだけなので、処理を分ける必要があります。

というわけで、DestroyEnemy()の名前をDefeatEnemy()に変更して、敵を撃破したときの処理を任せることにしました。そして、DefeatEnemy()にはゴールドを獲得する処理を追加します。敵を削除する処理は、新たにCleanupEnemy()メソッドを定義しました。

敵がゴール(城)に到達したときの処理をするReachedGoal()メソッドはCleanupEnemy()に処理を投げて、敵の削除だけをするようにしました。

STEP

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

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

[CreateAssetMenu(fileName = "EnemySetting", menuName = "ScriptableObject/Enemy Setting")]
public class EnemySetting : ScriptableObject
{
    // 敵のデータを格納するリスト
    [SerializeField] private List<EnemyData> enemyDataList = new List<EnemyData>();
    public List<EnemyData> EnemyDataList => enemyDataList;

    // 敵のデータ構造
    [Serializable]
    public class EnemyData
    {
        // ...省略...

        // ここから
        [SerializeField] private int reward; // 敵を倒したときに獲得する報酬
        public int Reward => reward;
        // ここまで
    }
}

EnemyDataクラスに、敵を倒したときに獲得する報酬(ゴールド、スコア)の変数を追加しました。

STEP

「Assets > Data > EnemySetting」のインスペクターで「Reward」を設定します。

タワーディフェンス186

Rewardは敵を撃破したときに得られる報酬(ゴールド、スコア)のことで、ゴールドはRewardの数字そのまま、スコアはReward * 10を加算しようと思っています。

砲台作成時にゴールドを支払う

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

using System.Collections.Generic;
using UnityEngine;
using UnityEngine.Tilemaps;
// ここから
using UnityEngine.EventSystems;
// ここまで

public class TurretGenerator : MonoBehaviour
{
    [SerializeField] private GameObject turretPrefab; // 砲台のプレハブ
    [SerializeField] private Grid grid; // グリッドコンポーネント
    [SerializeField] private Tilemap unbuildableTilemap; // 建築不可能なタイルマップ
    [SerializeField] private GameObject turretIcon; // 追随する砲台アイコン
    [SerializeField] private GameObject turretHeadIcon; // 追随する砲台アイコンの砲身
    [SerializeField] private GameObject cannotBuildIcon; // 建設不可アイコン
    [SerializeField] private SideBarManager sideBarManager; // サイドバーマネージャー
    [SerializeField] private GameManager gameManager; // ゲームマネージャー
    private Vector3Int currentGridPosition; // 現在のマウス位置のグリッド座標
    private HashSet<Vector3Int> placedTurretCells = new HashSet<Vector3Int>(); // 砲台が配置されているセルのリスト
    private TurretSetting.TurretData selectedTurretData = null; // 選択中の砲台データ

    private void Update()
    {
        // ここから
        // 砲台が選択されていない場合は何もしない
        if (selectedTurretData == null)
        {
            return;
        }
        // ここまで

        // マウスカーソルの位置を取得
        Vector3 mouseWorldPosition = Camera.main.ScreenToWorldPoint(Input.mousePosition);
        // グリッドのセル座標に変換
        currentGridPosition = grid.WorldToCell(mouseWorldPosition);
        // セル座標をワールド座標に変換
        Vector3 snappedWorldPosition = grid.CellToWorld(currentGridPosition);
        // タイルの中心に調整
        turretIcon.transform.position = new Vector3(snappedWorldPosition.x + 0.5f, snappedWorldPosition.y + 0.5f, 0);

        // 設置可能かチェック
        bool canPlaceTurret = IsTurretPlacementValid(currentGridPosition);
        // 建設不可アイコンの表示切り替え
        cannotBuildIcon.SetActive(!canPlaceTurret);

        // マウス左クリックで砲台を設置
        if (Input.GetMouseButtonDown(0))
        {
            // ここから
            // UIをクリックした場合は何もしない
            if (EventSystem.current.IsPointerOverGameObject())
            {
                return;
            }
            // ここまで
            if (canPlaceTurret)
            {
                // ここから
                //PlaceTurret(currentGridPosition);
                // コストを取得して砲台設置を試みる
                TryPlaceTurretWithCost(currentGridPosition);
                // ここまで
            }
            else
            {
                // 設置不可の場合は選択をリセット
                ResetTurretSelection();
                // ここから
                // サイドバーの情報も隠す
                sideBarManager.HideTurretInfo();
                sideBarManager.HideTurretUpgradeInfo();
                // ここまで
            }
        }

        // ここから
        // マウス右クリックでキャンセル
        if (Input.GetMouseButtonDown(1))
        {
            ResetTurretSelection();
            // サイドバーの情報も隠す
            sideBarManager.HideTurretInfo();
            sideBarManager.HideTurretUpgradeInfo();
        }
        // ここまで
    }

    // ここから
    /// <summary>
    /// コストを確認して砲台を配置する
    /// </summary>
    /// <param name="gridPosition">配置するグリッド座標</param>
    private void TryPlaceTurretWithCost(Vector3Int gridPosition)
    {
        // 砲台が選択されていなければ何もしない
        if (selectedTurretData == null)
        {
            return;
        }

        // 配置済みの場合は処理を中断
        if (placedTurretCells.Contains(gridPosition))
        {
            Debug.Log("この場所には既に砲台があります。");
            ResetTurretSelection(); // 選択をリセット
            sideBarManager.HideTurretInfo(); // サイドバーを隠す
            return;
        }

        // レベル1のコストを取得
        TurretSetting.TurretLevelData level1Data = DBManager.Instance.TurretSetting.GetTurretData(selectedTurretData.TurretId, 1);
        if (level1Data == null)
        {
            Debug.LogError($"砲台 {selectedTurretData.TurretId} のレベル1データが見つかりません。");
            ResetTurretSelection();
            sideBarManager.HideTurretInfo();
            return;
        }
        int buildCost = level1Data.Cost;

        // GoldManagerを取得してコストを支払う
        if (GoldManager.Instance != null)
        {
            if (GoldManager.Instance.SpendGold(buildCost))
            {
                // ゴールドが足りて支払えた場合、砲台を設置
                PlaceTurret(gridPosition, level1Data);
            }
            else
            {
                // ゴールドが足りない場合
                Debug.LogWarning($"ゴールドが足りません! 必要: {buildCost}, 所持: {GoldManager.Instance.CurrentGold}");
                ResetTurretSelection(); // 選択をリセット
                sideBarManager.HideTurretInfo(); // サイドバーを隠す
            }
        }
        else
        {
            Debug.LogError("GoldManager のインスタンスが見つかりません。");
            ResetTurretSelection();
            sideBarManager.HideTurretInfo();
        }
    }
    // ここまで

    /// <summary>
    /// 砲台を配置する
    /// </summary>
    /// <param name="gridPosition">配置するグリッド座標</param>
    // ここから
    /// <param name="levelData">配置する砲台のレベルデータ</param>
    //private void PlaceTurret(Vector3Int gridPosition)
    private void PlaceTurret(Vector3Int gridPosition, TurretSetting.TurretLevelData levelData)
    // ここまで
    {
        // ここから 削除
        // 砲台が選択されていなければ何もしない
        //if (selectedTurretData == null)
        //{
        //    return;
        //}

        // 配置済みの場合は処理を中断
        //if (placedTurretCells.Contains(gridPosition))
        //{
        //    return;
        //}
        // ここまで
        // 砲台を生成
        GameObject turret = Instantiate(turretPrefab, gridPosition, Quaternion.identity);
        // 砲台の位置を調整(タイルの中心に配置)
        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);
        // InitializeTurret には選択中の砲台データとレベル1を渡す
        turretController.InitializeTurret(selectedTurretData, levelData.Level);
        // ここまで

        // 設置直後にクリックされた状態にする
        turretController.OnTurretClicked();

        // 配置されたセルを登録
        placedTurretCells.Add(gridPosition);

        // 選択をリセット
        ResetTurretSelection();
    }

    /// <summary>
    /// 砲台の設置が可能かどうかを判定する
    /// </summary>
    /// <param name="gridPosition">判定するグリッド座標</param>
    /// <returns>設置可能かどうか</returns>
    private bool IsTurretPlacementValid(Vector3Int gridPosition)
    {
        // ここから
        // 選択中の砲台がない場合は設置不可
        if (selectedTurretData == null) return false;
        // ここまで
        // タイルマップにコライダーがない かつ 配置済みのセルでない場合
        return (unbuildableTilemap.GetColliderType(gridPosition) == Tile.ColliderType.None) && !placedTurretCells.Contains(gridPosition);
    }

    /// <summary>
    /// 砲台アイコンを選択する
    /// </summary>
    /// <param name="turretIndex">選択する砲台のインデックス</param>
    public void SelectTurret(int turretIndex)
    {
        // 選択された砲台のデータを取得
        selectedTurretData = DBManager.Instance.TurretSetting.TurretDataList[turretIndex];

        // ここから
        // レベル1のデータを取得
        TurretSetting.TurretLevelData turretLevelData = DBManager.Instance.TurretSetting.GetTurretData(selectedTurretData.TurretId, 1);
        if (turretLevelData == null)
        {
            Debug.LogError($"砲台 {selectedTurretData.TurretId} のレベル1データが見つかりません。");
            ResetTurretSelection();
            return;
        }
        // ここまで

        // アイコンを表示
        turretIcon.SetActive(true);
        // 砲身のスプライトを設定
        SpriteRenderer iconRenderer = turretHeadIcon.GetComponent<SpriteRenderer>();
        iconRenderer.sprite = selectedTurretData.TurretHeadSprite;

        // 砲台情報を表示
        // ここから 削除
        //TurretSetting.TurretLevelData turretLevelData = DBManager.Instance.TurretSetting.GetTurretData(selectedTurretData.TurretId, 1);
        // ここまで
        sideBarManager.ShowTurretInfo(selectedTurretData, turretLevelData, false, null);
        sideBarManager.HideTurretUpgradeInfo();

        // タイルハイライトを非表示
        if (gameManager.TileHighlighter != null)
        {
            gameManager.TileHighlighter.SetActive(false);
        }
    }

    /// <summary>
    /// 砲台の選択をリセットする
    /// </summary>
    private void ResetTurretSelection()
    {
        selectedTurretData = null;
        turretIcon.SetActive(false);
        // ここから
        cannotBuildIcon.SetActive(false); // 建設不可アイコンも非表示にする
        // ここまで
    }
}

TryPlaceTurretWithCost()メソッドを追加して、ゴールドが足りていれば支払って砲台を設置、足りなければ警告するようにしました。

ゴールドが足りなければ砲台選択ボタンを非活性化

ゴールドが足りないときに、砲台配置ボタンを選択できないようにします。

STEP

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

using UnityEngine;
using TMPro;
using UnityEngine.UI;

public class SideBarManager : MonoBehaviour
{
    // ...省略...
    // ここから
    [SerializeField, Header("砲台選択ボタンの配列")] private Button[] turretSelectionButton;
    public static SideBarManager Instance { get; private set; } // シングルトンインスタンス
    // ここまで

    //ここから
    // シングルトンパターンを実装
    private void Awake()
    {
        if (Instance == null)
        {
            Instance = this;
        }
        else if (Instance != this)
        {
            Destroy(gameObject);
        }
    }
    // ここまで

    // ...省略...

    // ここから メソッド追加
    /// <summary>
    /// ゴールドに応じて砲台選択ボタンの状態を更新する
    /// </summary>
    public void UpdateTurretSelectionButtons()
    {
        if (GoldManager.Instance == null)
        {
            Debug.LogError("GoldManager のインスタンスが見つかりません。", this);
            return;
        }

        int currentGold = GoldManager.Instance.CurrentGold;

        for (int i = 0; i < turretSelectionButton.Length; i++)
        {
            // 各砲台のレベル1データを取得
            TurretSetting.TurretData turretData = DBManager.Instance.TurretSetting.TurretDataList[i];
            TurretSetting.TurretLevelData level1Data = DBManager.Instance.TurretSetting.GetTurretData(turretData.TurretId, 1);

            if (level1Data != null)
            {
                // ゴールドが足りているか確認し、ボタンのインタラクト可否を設定
                bool isInteractable = currentGold >= level1Data.Cost;
                turretSelectionButton[i].interactable = isInteractable;

                // 子要素の画像をすべて取得
                Image[] turretImages = turretSelectionButton[i].GetComponentsInChildren<Image>();
                foreach (Image turretImage in turretImages)
                {
                    // 画像をグレーアウト
                    turretImage.color = isInteractable ? Color.white : new Color(0.5f, 0.5f, 0.5f, 1f);
                }
            }
            else
            {
                Debug.LogError($"砲台 {turretData.TurretId} のレベル1データが見つかりません。", this);
                turretSelectionButton[i].interactable = false;

                // 子要素の画像をすべてグレーアウト
                Image[] turretImages = turretSelectionButton[i].GetComponentsInChildren<Image>();
                foreach (Image turretImage in turretImages)
                {
                    turretImage.color = new Color(0.5f, 0.5f, 0.5f, 1f);
                }
            }
        }
    }
    // ここまで
}

他のスクリプトからアクセスしやすくするために、シングルトンパターンを使うことにしました。

ゴールドに応じて砲台選択ボタンの状態を更新するメソッドUpdateTurretSelectionButtons()を追加しました。

このメソッドでは、現在の所持金額を確認し、ボタンの活性・非活性を切り替えています。また、ゴールドが不足しているときはボタンの画像をグレーアウトしています。

STEP

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

using UnityEngine;
using TMPro; // TextMeshPro を使うために必要
using DG.Tweening; // DOTween を使うために必要

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

    /// <summary>
    /// ゴールドの値を設定し、UI更新を行う
    /// </summary>
    /// <param name="newGoldValue">新しいゴールドの値</param>
    private void SetGold(int newGoldValue)
    {
        // 念のため負の値にならないようにチェック
        currentGold = Mathf.Max(0, newGoldValue);
        UpdateGoldDisplay();
        // ここから
        // サイドバーマネージャーに通知してボタンを更新
        if (SideBarManager.Instance != null)
        {
            SideBarManager.Instance.UpdateTurretSelectionButtons();
        }
        // ここまで
    }

    // ...省略...
}

SetGold()メソッド内でUpdateTurretSelectionButtons()を呼び出し、ボタンの状態を更新します。

STEP

SideBarオブジェクトのインスペクターで「Side Bar Manager (Script) > Turret Selection Button」に砲台選択ボタンをアサインします。

タワーディフェンス187

砲台強化時にゴールドを支払う

砲台を強化するときにゴールドを支払う処理を実装します。強化中とゴールド不足時に強化ボタンを非活性化する処理も同時に実装しちゃいます。

STEP

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

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

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

    /// <summary>
    /// 砲台がクリックされたときの処理
    /// </summary>
    public void OnTurretClicked()
    {
        SideBarManager sideBarManager = FindObjectOfType<SideBarManager>(); // SideBarManagerを取得
        sideBarManager.ShowTurretInfo(currentTurretData, currentTurretLevelData, true, this); // 砲台情報パネルを表示、自分自身を渡す

        // ここから
        //if (currentTurretLevelData.Level < 5) // 砲台のレベルが5未満ならば
        //{
        //    sideBarManager.ShowTurretUpgradeInfo(currentTurretData, currentTurretLevelData, true); // 砲台強化情報パネルを表示
        //}
        // 次のレベルのデータを取得して存在するか確認
        TurretSetting.TurretLevelData nextLevelData = DBManager.Instance.TurretSetting.GetTurretData(currentTurretData.TurretId, currentTurretLevelData.Level + 1);
        if (nextLevelData != null) // 次のレベルが存在する場合
        {
            // 強化情報パネルを表示
            sideBarManager.ShowTurretUpgradeInfo(currentTurretData, currentTurretLevelData, true);
        }
        else // 最大レベルの場合
        {
            // 強化情報パネルを非表示
            sideBarManager.HideTurretUpgradeInfo();
        }
        // ここまで

        // 攻撃範囲の表示切り替え
        ToggleAttackRangeVisibility();
    }

    // ...省略...

    /// <summary>
    /// 砲台をアップグレードするコルーチン
    /// </summary>
    public IEnumerator UpgradeTurretCoroutine()
    {
        // ここから
        // 既にアップグレード中の場合は何もしない
        if (isCurrentlyUpgrading) yield break;

        // 次のレベルのデータを取得
        int nextLevel = currentTurretLevelData.Level + 1;
        TurretSetting.TurretLevelData nextLevelData = DBManager.Instance.TurretSetting.GetTurretData(currentTurretData.TurretId, nextLevel);

        // 次のレベルのデータがない(最大レベル)場合は何もしない
        if (nextLevelData == null)
        {
            Debug.LogWarning($"砲台 {currentTurretData.TurretId} は既に最大レベルです。");
            yield break; // コルーチンを終了
        }

        // アップグレードコストを取得
        int upgradeCost = nextLevelData.Cost;

        // GoldManagerを取得してコストを支払う
        if (GoldManager.Instance != null)
        {
            if (GoldManager.Instance.SpendGold(upgradeCost))
            {
                // ゴールドが足りて支払えた場合、アップグレード処理を開始
                Debug.Log($"砲台 {currentTurretData.TurretName} をレベル {nextLevel} にアップグレードします。コスト: {upgradeCost}G");
            }
            else
            {
                // ゴールドが足りない場合
                Debug.LogWarning($"アップグレードに必要なゴールドが足りません! 必要: {upgradeCost}G, 所持: {GoldManager.Instance.CurrentGold}");
                yield break; // コルーチンを終了
            }
        }
        else
        {
            Debug.LogError("GoldManager のインスタンスが見つかりません。アップグレードできませんでした。");
            yield break; // コルーチンを終了
        }
        // ここまで

        // アップグレード開始時の処理
        StartUpgrade();

        // アップグレード処理
        // ここから 次のレベルデータを渡す
        //yield return StartCoroutine(PerformUpgrade());
        yield return StartCoroutine(PerformUpgrade(nextLevelData));
        // ここまで

        // アップグレード完了後の処理
        // ここから 次のレベルデータを渡す
        //FinishUpgrade();
        FinishUpgrade(nextLevelData);
        // ここまで
    }

    /// <summary>
    /// アップグレード開始時の処理
    /// </summary>
    private void StartUpgrade()
    {
        // ...省略...

        // ここから
        // サイドバーの強化ボタンを非活性化
        SideBarManager sideBarManager = FindObjectOfType<SideBarManager>();
        if (sideBarManager != null)
        {
            sideBarManager.SetUpgradeButtonInteractable(false);
        }
        // ここまで
    }

    /// <summary>
    /// アップグレード処理
    /// </summary>
    // ここから 引数を追加
    /// <param name="nextLevelData">アップグレード後のレベルデータ</param>
    //private IEnumerator PerformUpgrade()
    private IEnumerator PerformUpgrade(TurretSetting.TurretLevelData nextLevelData)
    // ここまで
    {
        // 塗りつぶしImageを取得
        Image fillImage = currentUpgradeIndicator.transform.Find("UpgradeIndicatorFill").GetComponent<Image>();
        // ここから
        // int newLevel = currentTurretLevelData.Level + 1; // アップグレード後のレベル
        int newLevel = nextLevelData.Level; // アップグレード後のレベル
        // ここまで
        float upgradeTime = newLevel * 6.0f - 11; // 待機時間
        float elapsedTime = 0f; // 経過時間

        // ここから ボタンの非活性化はStartUpgradeに移動したので削除
        //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;
        }
    }

    /// <summary>
    /// アップグレード完了後の処理
    /// </summary>
    // ここから 引数を追加
    /// <param name="newTurretLevelData">アップグレード後のレベルデータ</param>
    //private void FinishUpgrade()
    private void FinishUpgrade(TurretSetting.TurretLevelData newTurretLevelData)
    // ここまで
    {
        // ここから
        //int newLevel = currentTurretLevelData.Level + 1;
        int newLevel = newTurretLevelData.Level;
        // ここまで

        // ここから 引数で受け取るので削除
        // アップグレード後の砲台情報を取得
        //TurretSetting.TurretLevelData newTurretLevelData = DBManager.Instance.TurretSetting.GetTurretData(currentTurretData.TurretId, newLevel);
        //if (newTurretLevelData == null)
        //{
        //    Debug.LogWarning($"砲台 {currentTurretData.TurretId} のレベル {newLevel} のデータが見つかりません");
        //    isCurrentlyUpgrading = false; // アップグレード終了
        //    Destroy(currentUpgradeIndicator); // インジケータを削除
        //    return;
        //}
        // ここまで

        currentTurretLevelData = newTurretLevelData; // データを更新
        InitializeTurret(currentTurretData, newLevel); // 砲台を初期化

        // UIの更新
        SideBarManager sideBarManager = FindObjectOfType<SideBarManager>(); // SideBarManagerを取得
        if (sideBarManager != null)
        {
            sideBarManager.ShowTurretInfo(currentTurretData, currentTurretLevelData, true, this); // 砲台情報パネルを表示

            // ここから サイドバーの表示ロジックを変更
            //if (newLevel < 5) // レベルが5未満なら
            //{
            //    sideBarManager.ShowTurretUpgradeInfo(currentTurretData, currentTurretLevelData, true); // 強化パネルを表示
            //}
            //else // レベルが5以上なら
            //{
            //    sideBarManager.HideTurretUpgradeInfo(); // 強化パネルを非表示
            //}
            //sideBarManager.SetUpgradeButtonInteractable(true); // 強化ボタンを活性化
            // 次のレベルがあるか再度チェックして強化パネルの表示/非表示とボタンの状態を決める
            TurretSetting.TurretLevelData nextFurtherLevelData = DBManager.Instance.TurretSetting.GetTurretData(currentTurretData.TurretId, newLevel + 1);
            bool canUpgradeFurther = nextFurtherLevelData != null;

            if (canUpgradeFurther)
            {
                // さらにアップグレード可能なら、コストが足りるかチェックしてボタンの状態を決める
                bool hasEnoughGoldForNext = GoldManager.Instance != null && GoldManager.Instance.CurrentGold >= nextFurtherLevelData.Cost;
                sideBarManager.ShowTurretUpgradeInfo(currentTurretData, currentTurretLevelData, hasEnoughGoldForNext); // 強化パネルを表示し、ボタン状態を設定
            }
            else
            {
                sideBarManager.HideTurretUpgradeInfo(); // 最大レベルなら強化パネルを非表示
            }
            // ここまで
        }
        else
        {
            Debug.LogError("SideBarManager が見つかりません");
        }
        isCurrentlyUpgrading = false; // アップグレード終了
        // ここから
        //Destroy(currentUpgradeIndicator); // インジケータを削除
        if (currentUpgradeIndicator != null) Destroy(currentUpgradeIndicator); // インジケータを削除
        // ここまで
    }
}
STEP

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

using UnityEngine;
using TMPro;
using UnityEngine.UI;

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

    /// <summary>
    /// 砲台強化情報パネルを表示する
    /// </summary>
    /// <param name="turretData">砲台データ</param>
    // ここから 変数名を変更
    /// //<param name="turretLevelData">砲台レベル別データ</param>
    /// //<param name="isUpgradeButtonInteractable">強化ボタンのインタラクト可否</param>
    /// <param name="currentLevelData">現在の砲台レベル別データ</param>
    /// <param name="checkGold">ゴールドが足りているかチェックする</param>
    //public void ShowTurretUpgradeInfo(TurretSetting.TurretData turretData, TurretSetting.TurretLevelData turretLevelData, bool isUpgradeButtonInteractable)
    public void ShowTurretUpgradeInfo(TurretSetting.TurretData turretData, TurretSetting.TurretLevelData currentLevelData, bool checkGold)
    // ここまで
    {
        // 次のレベルの砲台データを取得
        // ここから 変数名が変わったので
        //TurretSetting.TurretLevelData nextLevelTurretData = DBManager.Instance.TurretSetting.GetTurretData(turretData.TurretId, turretLevelData.Level + 1);
        TurretSetting.TurretLevelData nextLevelTurretData = DBManager.Instance.TurretSetting.GetTurretData(turretData.TurretId, currentLevelData.Level + 1);
        // ここまで

        // ここから
        // 次のレベルデータがない場合はパネルを非表示にして終了
        if (nextLevelTurretData == null)
        {
            HideTurretUpgradeInfo();
            return;
        }
        // ここまで

        levelUpText.text = $"Lv.{nextLevelTurretData.Level}へ強化";
        upgradeCostText.text = nextLevelTurretData.Cost.ToString();
        // ここから 変数名が変わったので
        //upgradeAttackPowerText.text = $"{turretLevelData.AttackPower} >>> {nextLevelTurretData.AttackPower}";
        //upgradeAttackIntervalText.text = $"{turretLevelData.AttackInterval} >>> {nextLevelTurretData.AttackInterval}";
        //upgradeAttackRangeText.text = $"{turretLevelData.AttackRange} >>> {nextLevelTurretData.AttackRange}";
        upgradeAttackPowerText.text = $"{currentLevelData.AttackPower} >>> {nextLevelTurretData.AttackPower}";
        upgradeAttackIntervalText.text = $"{currentLevelData.AttackInterval} >>> {nextLevelTurretData.AttackInterval}";
        upgradeAttackRangeText.text = $"{currentLevelData.AttackRange} >>> {nextLevelTurretData.AttackRange}";
        // ここまで
        upgradeButtonText.text = $"{nextLevelTurretData.Cost}Gで強化";

        // 強化ボタンのインタラクト可否を設定
        // ここから
        //upgradeButton.interactable = isUpgradeButtonInteractable;
        bool canAffordUpgrade = true; // デフォルトはtrue
        if (checkGold) // ゴールドチェックが必要な場合
        {
            // GoldManagerが存在し、かつ現在のゴールドがアップグレードコスト以上か確認
            canAffordUpgrade = GoldManager.Instance != null && GoldManager.Instance.CurrentGold >= nextLevelTurretData.Cost;
            if (GoldManager.Instance == null)
            {
                Debug.LogError("GoldManager のインスタンスが見つかりません。コストチェックができません。", this);
                canAffordUpgrade = false; // 安全のため非活性化
            }
        }
        // 強化ボタンのインタラクト可否を設定
        upgradeButton.interactable = canAffordUpgrade;
        // ここまで

        // 砲台強化情報パネルを表示
        turretUpgradeInfoPanel.SetActive(true);
    }

    // ...省略...
}

砲台売却時にゴールドを受け取る

砲台を売却したらゴールドを受け取る処理を実装します。というか、そもそも砲台を売却する処理を実装していなかったので、それもあわせて実装します。

  • 売却ボタンを押したら砲台を削除する。
  • 削除には4秒かかる。その間インジケータを表示。
  • 設置と強化にかかった総コストの半額のゴールドが戻ってくる。
STEP

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

using UnityEngine;
using TMPro;
using UnityEngine.UI;

public class SideBarManager : MonoBehaviour
{
    // ...省略...
    // ここから
    [SerializeField, Header("売却ボタンテキスト")] private TextMeshProUGUI sellButtonText;
    // ここまで

    // ...省略...

    private void Start()
    {
        // 強化ボタンにクリックイベントを追加
        upgradeButton.onClick.AddListener(UpgradeSelectedTurret);

        // ここから
        // 売却ボタンにクリックイベントを追加
        sellButton.onClick.AddListener(SellSelectedTurret);
        // ここまで
    }

    // ...省略...

    // ここから
    /// <summary>
    /// 選択された砲台を売却する
    /// </summary>
    private void SellSelectedTurret()
    {
        // 選択された砲台が存在する場合
        if (selectedTurret != null)
        {
            // 売却コルーチンを開始
            StartCoroutine(selectedTurret.SellTurretCoroutine());
        }
    }

    /// <summary>
    /// 売却ボタンのインタラクト可否を設定
    /// </summary>
    /// <param name="isInteractable">インタラクト可否</param>
    public void SetSellButtonInteractable(bool isInteractable)
    {
        sellButton.interactable = isInteractable;
    }

    /// <summary>
    /// 売却ボタンのテキストを更新
    /// </summary>
    /// <param name="refundAmount">返金額</param>
    public void UpdateSellButtonText(int refundAmount)
    {
        sellButtonText.text = $"{refundAmount}Gで売却";
    }
    // ここまで
}

以下の3つのメソッドを追加しました。

  • SellSelectedTurret(): 砲台の売却コルーチンを開始する。売却ボタンが押されると発動する。
  • SetSellButtonInteractable(): 売却ボタンの活性/非活性を切り替える。
  • UpdateSellButtonText(): 売却ボタンのテキストを更新する。
STEP

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

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

public class TurretController : MonoBehaviour
{
    // ...省略...
    // ここから
    private bool isCurrentlySelling = false; // 売却中フラグ
    // ここまで

    // ...省略...

    /// <summary>
    /// 砲台がクリックされたときの処理
    /// </summary>
    public void OnTurretClicked()
    {
        // ...省略...

        // ここから
        // 売却額を計算してボタンテキストを更新
        int totalCost = 0;
        for (int i = 1; i <= currentTurretLevelData.Level; i++)
        {
            TurretSetting.TurretLevelData levelData = DBManager.Instance.TurretSetting.GetTurretData(currentTurretData.TurretId, i);
            if (levelData != null)
            {
                totalCost += levelData.Cost;
            }
        }
        int refundAmount = Mathf.FloorToInt(totalCost * 0.5f);
        sideBarManager.UpdateSellButtonText(refundAmount);
        // ここまで

        // ...省略...
    }

    // ...省略...

    /// <summary>
    /// 砲台をアップグレードするコルーチン
    /// </summary>
    public IEnumerator UpgradeTurretCoroutine()
    {
        // ここから
        // 既にアップグレード中の場合は何もしない
        //if (isCurrentlyUpgrading) yield break;
        // アップグレード中または売却中の場合は何もしない
        if (isCurrentlyUpgrading || isCurrentlySelling) yield break;
        // ここまで

        // ...省略...
    }

    // ...省略...

    /// <summary>
    /// アップグレード完了後の処理
    /// </summary>
    /// <param name="newTurretLevelData">アップグレード後のレベルデータ</param>
    private void FinishUpgrade(TurretSetting.TurretLevelData newTurretLevelData)
    {
        // ...省略...

        // ここから
        // 売却額を再計算してボタンテキストを更新
        int totalCost = 0;
        for (int i = 1; i <= currentTurretLevelData.Level; i++)
        {
            TurretSetting.TurretLevelData levelData = DBManager.Instance.TurretSetting.GetTurretData(currentTurretData.TurretId, i);
            if (levelData != null)
            {
                totalCost += levelData.Cost;
            }
        }
        int refundAmount = Mathf.FloorToInt(totalCost * 0.5f);

        if (sideBarManager != null)
        {
            sideBarManager.UpdateSellButtonText(refundAmount);
        }
        // ここまで
    }

    // ここから
    /// <summary>
    /// 売却コルーチン
    /// </summary>
    public IEnumerator SellTurretCoroutine()
    {
        // アップグレード中または売却中の場合は何もしない
        if (isCurrentlyUpgrading || isCurrentlySelling) yield break;

        // 売却中フラグを設定
        isCurrentlySelling = true;

        // サイドバーの売却ボタンを非活性化
        SideBarManager sideBarManager = FindObjectOfType<SideBarManager>();
        if (sideBarManager != null)
        {
            sideBarManager.SetSellButtonInteractable(false);
        }

        // 売却額を計算(設置と強化にかかった総コストの半額)
        int totalCost = 0;
        for (int i = 1; i <= currentTurretLevelData.Level; i++)
        {
            TurretSetting.TurretLevelData levelData = DBManager.Instance.TurretSetting.GetTurretData(currentTurretData.TurretId, i);
            if (levelData != null)
            {
                totalCost += levelData.Cost;
            }
        }
        int refundAmount = Mathf.FloorToInt(totalCost * 0.5f);

        // サイドバーの売却ボタンテキストを更新
        if (sideBarManager != null)
        {
            sideBarManager.UpdateSellButtonText(refundAmount);
        }

        // インジケータを生成
        currentUpgradeIndicator = Instantiate(upgradeIndicatorPrefab, transform.position, Quaternion.identity);
        Canvas canvas = FindObjectOfType<Canvas>();
        currentUpgradeIndicator.transform.SetParent(canvas.transform, false);
        Vector2 screenPosition = Camera.main.WorldToScreenPoint(transform.position);
        currentUpgradeIndicator.GetComponent<RectTransform>().position = screenPosition;

        // 塗りつぶしImageを取得
        Image fillImage = currentUpgradeIndicator.transform.Find("UpgradeIndicatorFill").GetComponent<Image>();
        // 塗りつぶしImageの色を設定
        fillImage.color = new Color(0.94f, 0.5f, 0.5f, 0.63f);
        float sellTime = 4.0f; // 売却にかかる時間
        float elapsedTime = 0f;

        while (elapsedTime < sellTime)
        {
            elapsedTime += Time.deltaTime;
            fillImage.fillAmount = elapsedTime / sellTime; // 塗りつぶし量を更新
            yield return null;
        }

        // ゴールドを返金
        if (GoldManager.Instance != null)
        {
            GoldManager.Instance.AddGold(refundAmount);
        }

        // インジケータを削除
        if (currentUpgradeIndicator != null)
        {
            Destroy(currentUpgradeIndicator);
        }

        // TurretGeneratorに通知して砲台の設置済みリストから削除
        TurretGenerator turretGenerator = FindObjectOfType<TurretGenerator>();
        if (turretGenerator != null)
        {
            turretGenerator.ResetTurretPlacement(transform.position);
        }

        // 砲台を削除
        Destroy(gameObject);

        // 売却中フラグを解除
        isCurrentlySelling = false;

        // サイドバーの売却ボタンを再び活性化
        if (sideBarManager != null)
        {
            sideBarManager.SetSellButtonInteractable(true);
        }
    }
    // ここまで
}

OnTurretClicked()内に、売却額を計算してボタンのテキストを更新する処理を追加しました。売却額は設置と強化にかかった総コストの半額なので、そのように計算しています。

UpgradeTurretCoroutine()で、強化中だけでなく売却中にも砲台を強化できないように処理を修正しました。

FinishUpgrade()内に、売却額を計算してボタンのテキストを更新する処理を追加しました。

SellTurretCoroutine()コルーチンを追加しました。ちょっと長いですが以下の処理をしています。

  1. 強化中または売却中の場合は売却できないようにする。
  2. 売却ボタンを非活性化。
  3. 売却額を計算して売却ボタンのテキストを更新。
  4. インジケータを生成しアニメーション。
  5. ゴールドを返金。
  6. 砲台の設置済みリストから削除。
  7. 砲台を削除。
  8. 売却ボタンを活性化。
STEP

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

using System.Collections.Generic;
using UnityEngine;
using UnityEngine.Tilemaps;
using UnityEngine.EventSystems;

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

    // ここから
    /// <summary>
    /// 指定された位置の砲台設置状態をリセット
    /// </summary>
    /// <param name="position">リセットする位置</param>
    public void ResetTurretPlacement(Vector3 position)
    {
        Vector3Int gridPosition = grid.WorldToCell(position);
        placedTurretCells.Remove(gridPosition); // 設置済みリストから削除
    }
    // ここまで
}

ResetTurretPlacement()を追加しました。砲台の設置済みリスト(placedTurretCells)から砲台を削除するメソッドです。

これをしておかないと、砲台を削除した場所に再び砲台を設置することができません。

STEP

SideBarオブジェクトのインスペクターで「Side Bar Manager (Script) > Sell Button Text」SellButtonTextをドラッグ&ドロップしてアサインします。

タワーディフェンス188

売却額の計算をまとめる

お気付きでしょうか。TurretControllerスクリプト内に砲台の売却額を計算する処理が3回出てきて、すべて同じコードなんです。まとめたいですよね。

売却額を計算するメソッドを追加しました。

/// <summary>
/// 売却額を計算する
/// </summary>
/// <returns>売却額(設置と強化にかかった総コストの半額)</returns>
private int CalculateRefundAmount()
{
    int totalCost = 0;

    // 現在のレベルまでのコストを合計
    for (int i = 1; i <= currentTurretLevelData.Level; i++)
    {
        TurretSetting.TurretLevelData levelData = DBManager.Instance.TurretSetting.GetTurretData(currentTurretData.TurretId, i);
        if (levelData != null)
        {
            totalCost += levelData.Cost;
        }
    }

    // 売却額は総コストの50%
    return Mathf.FloorToInt(totalCost * 0.5f);
}

OnTurretClicked()を修正します。

// 売却額を計算してボタンテキストを更新
//int totalCost = 0;
//for (int i = 1; i <= currentTurretLevelData.Level; i++)
//{
//    TurretSetting.TurretLevelData levelData = DBManager.Instance.TurretSetting.GetTurretData(currentTurretData.TurretId, i);
//    if (levelData != null)
//    {
//        totalCost += levelData.Cost;
//    }
//}
//int refundAmount = Mathf.FloorToInt(totalCost * 0.5f);
//sideBarManager.UpdateSellButtonText(refundAmount);
int refundAmount = CalculateRefundAmount();
sideBarManager.UpdateSellButtonText(refundAmount);

FinishUpgrade()を修正します。

// 売却額を再計算してボタンテキストを更新
//int totalCost = 0;
//for (int i = 1; i <= currentTurretLevelData.Level; i++)
//{
//    TurretSetting.TurretLevelData levelData = DBManager.Instance.TurretSetting.GetTurretData(currentTurretData.TurretId, i);
//    if (levelData != null)
//    {
//        totalCost += levelData.Cost;
//    }
//}
//int refundAmount = Mathf.FloorToInt(totalCost * 0.5f);

//if (sideBarManager != null)
//{
//    sideBarManager.UpdateSellButtonText(refundAmount);
//}
int refundAmount = CalculateRefundAmount();
if (sideBarManager != null)
{
    sideBarManager.UpdateSellButtonText(refundAmount);
}

SellTurretCoroutine()を修正します。

// 売却額を計算(設置と強化にかかった総コストの半額)
//int totalCost = 0;
//for (int i = 1; i <= currentTurretLevelData.Level; i++)
//{
//    TurretSetting.TurretLevelData levelData = DBManager.Instance.TurretSetting.GetTurretData(currentTurretData.TurretId, i);
//    if (levelData != null)
//    {
//        totalCost += levelData.Cost;
//    }
//}
//int refundAmount = Mathf.FloorToInt(totalCost * 0.5f);

// サイドバーの売却ボタンテキストを更新
//if (sideBarManager != null)
//{
//    sideBarManager.UpdateSellButtonText(refundAmount);
//}
// 売却額を計算してボタンテキストを更新
int refundAmount = CalculateRefundAmount();
if (sideBarManager != null)
{
    sideBarManager.UpdateSellButtonText(refundAmount);
}

以上でスッキリしたんじゃないでしょうか。

スコアの実装

敵破壊時にスコアを獲得

STEP

スコアを管理するためのScoreManagerスクリプトを新規作成します。

using UnityEngine;
using TMPro; // TextMeshPro を使うために必要
using DG.Tweening; // DOTween を使うために必要

public class ScoreManager : MonoBehaviour
{
    // シングルトンインスタンス
    public static ScoreManager Instance { get; private set; }

    [SerializeField, Tooltip("現在のスコア")] private int currentScore = 0;
    [SerializeField, Tooltip("スコア表示用UIテキスト")] private TextMeshProUGUI scoreText;

    private void Awake()
    {
        // シングルトンパターンの実装
        if (Instance == null)
        {
            Instance = this;
        }
        else if (Instance != this)
        {
            Debug.LogWarning("ScoreManager のインスタンスが既に存在するため、新しいインスタンスを破棄します。", this);
            Destroy(gameObject);
            return;
        }

        // UIテキストが設定されているか確認
        if (scoreText == null)
        {
            Debug.LogError("ScoreManager: scoreText が Inspector で設定されていません。", this);
        }
    }

    private void Start()
    {
        // ゲーム開始時にUIを初期化
        UpdateScoreDisplay();
    }

    /// <summary>
    /// スコアを加算する
    /// </summary>
    /// <param name="amount">加算するスコア量</param>
    public void AddScore(int amount)
    {
        if (amount <= 0)
        {
            Debug.LogWarning($"AddScore: 加算するスコア量({amount})は正の数である必要があります。", this);
            return;
        }

        int startValue = currentScore;
        int endValue = currentScore + amount;

        // DOTween を使用してカウントアップアニメーション
        DOTween.To(() => startValue, x =>
        {
            startValue = x;
            if (scoreText != null)
            {
                scoreText.text = startValue.ToString();
            }
        }, endValue, 0.5f) // 0.5秒でカウントアップ
        .SetEase(Ease.OutQuad) // スムーズなアニメーション
        .OnComplete(() =>
        {
            // アニメーション完了後に実際のスコア値を更新
            currentScore = endValue;
            Debug.Log($"{amount} スコア加算! 現在のスコア: {currentScore}", this);
        });
    }

    /// <summary>
    /// スコア表示テキストを更新する
    /// </summary>
    private void UpdateScoreDisplay()
    {
        if (scoreText != null)
        {
            scoreText.text = currentScore.ToString();
        }
    }
}

シングルトンパターンにしました。

スコアを加算するAddScore()とスコア表示テキストを更新するUpdateScoreDisplay()メソッドを定義しました。

AddScore()内では、DOTweenを使ってスコアのカウントアップがいい感じになるようにしました。

STEP

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

using UnityEngine;
using DG.Tweening;
using System.Linq;

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

    /// <summary>
    /// 敵を撃破する
    /// </summary>
    private void DefeatEnemy()
    {
        // ...省略...

        // ここから
        // スコアを加算
        if (ScoreManager.Instance != null)
        {
            if (enemyData != null)
            {
                int scoreToAdd = enemyData.Reward * 10; // スコアは Reward * 10
                ScoreManager.Instance.AddScore(scoreToAdd);
                Debug.Log($"{enemyData.EnemyName} を倒して {scoreToAdd} スコア獲得!", this);
            }
            else
            {
                Debug.LogError("EnemyData が設定されていません。スコアを加算できませんでした。", this);
            }
        }
        else
        {
            Debug.LogError("ScoreManager のインスタンスが見つかりません");
        }
        // ここまで

        // クリーンアップ処理を呼び出す
        CleanupEnemy();
    }

    // ...省略...
}

DefeatEnemy()メソッドにスコアを加算する処理を追加しました。

STEP

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

タワーディフェンス189
STEP

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

タワーディフェンス190

フローティングテキストの実装

敵を倒したときに獲得したゴールドやスコアをフローティングテキストで表示します。

敵を撃破したとき

STEP

まずは、フローティングテキストのプレハブを作成します。

ヒエラルキー上で右クリック「UI > Text – TextMeshPro」を選択します。

タワーディフェンス191

Canvasの配下に「Text (TMP)」というオブジェクトができるので、名前を「FloatingText」に変更します。

STEP

FloatingTextのインスペクターでオブジェクトのサイズ、テキストのフォントを調整します。

タワーディフェンス192

オブジェクトの位置(Pos X, Pos Y)とフォントの色(Vertex Color)はスクリプトで設定するので、ここでは設定不要です。

STEP

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

ヒエラルキーのFloatingTextオブジェクトは用済みなので削除します(冷たい)。

STEP

FloatingTextManagerスクリプトを新規作成します。

using UnityEngine;
using TMPro;
using DG.Tweening;

public class FloatingTextManager : MonoBehaviour
{
    [SerializeField, Tooltip("フローティングテキストのプレハブ")] private GameObject floatingTextPrefab;
    public static FloatingTextManager Instance { get; private set; } // シングルトンインスタンス

    private void Awake()
    {
        // シングルトンパターンの実装
        if (Instance == null)
        {
            Instance = this;
            DontDestroyOnLoad(gameObject);
        }
        else if (Instance != this)
        {
            Destroy(gameObject);
        }
    }

    /// <summary>
    /// フローティングテキストを表示する
    /// </summary>
    /// <param name="text">表示するテキスト</param>
    /// <param name="color">テキストの色</param>
    /// <param name="position">ワールド座標での表示位置</param>
    /// <param name="offset">表示位置のオフセット</param>
    public void ShowFloatingText(string text, Color color, Vector3 position, Vector3 offset)
    {
        if (floatingTextPrefab == null)
        {
            Debug.LogError("FloatingTextPrefab が設定されていません。", this);
            return;
        }

        // フローティングテキストを生成
        GameObject floatingText = Instantiate(floatingTextPrefab, position + offset, Quaternion.identity);

        // テキストの設定
        TextMeshProUGUI textMesh = floatingText.GetComponent<TextMeshProUGUI>();
        if (textMesh != null)
        {
            textMesh.text = text;
            textMesh.color = color;

            // RectTransform の設定と Canvas 配下への配置
            RectTransform rectTransform = floatingText.GetComponent<RectTransform>();
            Canvas canvas = FindObjectOfType<Canvas>();
            if (canvas != null)
            {
                floatingText.transform.SetParent(canvas.transform, false);
                rectTransform.position = Camera.main.WorldToScreenPoint(position + offset);
            }

            // DOTween を使用してアニメーションを設定
            rectTransform.DOMoveY(rectTransform.position.y + 50f, 1.5f).SetEase(Ease.OutQuad); // 上方向に移動
            textMesh.DOFade(0f, 1.5f).SetEase(Ease.InQuad); // フェードアウト
        }
        else
        {
            Debug.LogError("フローティングテキストの TextMeshProUGUI コンポーネントが見つかりません。", this);
        }

        // 2秒後に削除
        Destroy(floatingText, 2.0f);
    }
}

シングルトンパターンを実装してShowFloatingText()メソッドを定義しています。

ShowFloatingText()では以下の処理をしています。

  • floatingTextPrefabの設定チェック。
  • プレハブからfloatingTextオブジェクトを生成。
  • 表示するテキストと色を設定。
  • オブジェクトをCanvasの配下へ設置し、表示位置を設定。
  • 上方向へ移動しながらフェードアウトするアニメーションを設定。
  • 2秒後にオブジェクトを削除。
STEP

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

GameManagerのインスペクターの「Floating Text Manager (Script) > Floating Text Prefab」FloatingTextプレハブをドラッグ&ドロップしてアサインします。

STEP

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

using UnityEngine;
using DG.Tweening;
using System.Linq;

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

    /// <summary>
    /// 敵を撃破する
    /// </summary>
    private void DefeatEnemy()
    {
        // ゴールドを追加
        if (GoldManager.Instance != null)
        {
            if (enemyData != null) // enemyDataがnullでないことを確認
            {
                // ...省略...

                // ここから
                // ゴールドのフローティングテキストを表示
                FloatingTextManager.Instance.ShowFloatingText(
                    $"+{enemyData.Reward}",
                    new Color(1.0f, 0.843f, 0.0f),
                    transform.position,
                    new Vector3(0, 1.35f, 0)
                );
                // ここまで
            }
            else
            {
                Debug.LogError("EnemyData が設定されていません。ゴールドを獲得できませんでした。", this);
            }
        }
        else
        {
            Debug.LogError("GoldManager のインスタンスが見つかりません");
        }

        // スコアを加算
        if (ScoreManager.Instance != null)
        {
            if (enemyData != null)
            {
                // ...省略...

                // ここから
                // スコアのフローティングテキストを表示
                FloatingTextManager.Instance.ShowFloatingText(
                    $"+{scoreToAdd}",
                    new Color(0.867f, 0.867f, 0.867f),
                    transform.position,
                    new Vector3(0, 1.0f, 0)
                );
                // ここまで
            }
            else
            {
                Debug.LogError("EnemyData が設定されていません。スコアを加算できませんでした。", this);
            }
        }
        else
        {
            Debug.LogError("ScoreManager のインスタンスが見つかりません");
        }

        // クリーンアップ処理を呼び出す
        CleanupEnemy();
    }

    // ...省略...
}

DefeatEnemy()内にゴールドとスコアのフローティングテキストを表示する処理を追加しました。

それぞれに引数として、テキストの内容、色、敵の位置、位置のオフセットを指定しています。色はゴールドが黄色、スコアは白にしました。オフセットを調整して2つのテキストが重ならないようにします。

砲台を設置、強化、売却したとき

STEP

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

using System.Collections.Generic;
using UnityEngine;
using UnityEngine.Tilemaps;
using UnityEngine.EventSystems;

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

    /// <summary>
    /// コストを確認して砲台を配置する
    /// </summary>
    /// <param name="gridPosition">配置するグリッド座標</param>
    private void TryPlaceTurretWithCost(Vector3Int gridPosition)
    {
        // ...省略...

        // GoldManagerを取得してコストを支払う
        if (GoldManager.Instance != null)
        {
            if (GoldManager.Instance.SpendGold(buildCost))
            {
                // ゴールドが足りて支払えた場合、砲台を設置
                PlaceTurret(gridPosition, level1Data);

                // ここから
                // ゴールド消費時のフローティングテキストを表示
                FloatingTextManager.Instance.ShowFloatingText(
                    $"-{buildCost}",
                    new Color(1.0f, 0.843f, 0.0f),
                    gridPosition,
                    new Vector3(0.5f, 1.5f, 0)
                );
                // ここまで
            }
            else
            {
                // ...省略...
            }
        }
        else
        {
            // ...省略...
        }
    }

    // ...省略...
}

TryPlaceTurretWithCost()内にゴールドを消費するフローティングテキストを表示する処理を追加しました。

STEP

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

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

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

    /// <summary>
    /// 砲台をアップグレードするコルーチン
    /// </summary>
    public IEnumerator UpgradeTurretCoroutine()
    {
        // ...省略...

        // GoldManagerを取得してコストを支払う
        if (GoldManager.Instance != null)
        {
            if (GoldManager.Instance.SpendGold(upgradeCost))
            {
                // ゴールドが足りて支払えた場合、アップグレード処理を開始
                Debug.Log($"砲台 {currentTurretData.TurretName} をレベル {nextLevel} にアップグレードします。コスト: {upgradeCost}G");
                // ここから
                // ゴールド消費時のフローティングテキストを表示
                FloatingTextManager.Instance.ShowFloatingText(
                    $"-{upgradeCost}",
                    new Color(1.0f, 0.843f, 0.0f),
                    transform.position,
                    new Vector3(0, 1.0f, 0)
                );
                // ここまで
            }
            else
            {
                // ...省略...
            }
        }
        else
        {
            // ...省略...
        }

        // ...省略...
    }

    // ...省略...

    /// <summary>
    /// 売却コルーチン
    /// </summary>
    public IEnumerator SellTurretCoroutine()
    {
        // ...省略...

        // ゴールドを返金
        if (GoldManager.Instance != null)
        {
            GoldManager.Instance.AddGold(refundAmount);

            // ここから
            // ゴールド返金時のフローティングテキストを表示
            FloatingTextManager.Instance.ShowFloatingText(
                $"+{refundAmount}",
                new Color(1.0f, 0.843f, 0.0f),
                transform.position,
                new Vector3(0, 1.0f, 0)
            );
            // ここまで
        }

        // ...省略...
    }

    // ...省略...
}

UpgradeTurretCoroutine()内にゴールド消費時のフローティングテキスト表示処理、SellTurretCoroutine()内にゴールド返金時のフローティングテキスト表示処理を追加しました。

動作確認

敵を撃破したときや砲台を設置、強化、売却したときにゴールドとスコアが増減するようになりました。数字の増え方もオシャレにアニメーションしています。私は満足です。

さいごに

今回はゴールドとスコアを実装しました。

ゲーム性を高めるには今回の実装だけでは不十分で、もっと別のタイミングでもゴールドやスコアを獲得するチャンスが必要だと思っています。今後、ウェーブ機能を実装したらそのへんも考えていきたいですね。

でわでわ

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

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

シェアしてね

コメント

コメントする

目次