Unity初心者が2Dタワーディフェンスゲームを制作しています。
制作が進むにつれてコードがスパゲティ化してきたので、今回はすべてのスクリプトをリファクタリングしたいと思います。
環境
- Mac mini (M1, 2020)
- Unity 2022.3.36f1
- Visual Studio Code 1.98.2
目次
Gemini Code Assistを使う
リファクタリングは、もちろん自力でやるのではなくAIの力を借ります。で、Gemini Code Assistを使いたいと思います。VSCodeの機能拡張が提供されているのでインストールして簡単に使い始めることができます。
DBManager.cs のリファクタリング
DBManager.cs
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public class DBManager : MonoBehaviour
{
// DBManagerのインスタンスを保持する静的変数
public static DBManager Instance { get; private set; }
// 敵の設定データを保持する変数
[SerializeField] private EnemySetting enemySetting;
public EnemySetting EnemySetting => enemySetting;
// 砲台の設定データを保持する変数
[SerializeField] private TurretSetting turretSetting;
public TurretSetting TurretSetting => turretSetting;
void Awake()
{
// シングルトンパターンを実装
if (Instance == null)
{
// インスタンスがなければ、自身をインスタンスとして設定
Instance = this;
// シーン遷移時に破棄されないように設定
DontDestroyOnLoad(gameObject);
}
else
{
// すでにインスタンスが存在する場合は、自身を破棄
Destroy(gameObject);
}
}
}シングルトンパターンの改善
instanceをInstanceに変更し、{ get; private set; }を追加しました。これにより、外部からインスタンスを取得することはできますが、設定することはできなくなります。- C#のコーディング規約では、静的プロパティはパスカルケース、動的プロパティはキャメルケースで記述するそうです。へー。なので、
Instanceの先頭は大文字になりました。
変数のカプセル化
enemySettingとturretSettingのアクセス修飾子をprivateにすることで、データのカプセル化を実現しています。=>はラムダ式というものらしいです。よくわかりませんが、これでEnemySettingでenemySettingにアクセスできるようになるらしいです。
EnemySetting.cs のリファクタリング
EnemySetting.cs
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 string enemyId; // 敵のID
public string EnemyId => enemyId;
[SerializeField] private string enemyName; // 敵の名前
public string EnemyName => enemyName;
[SerializeField] private int maxHp; // 敵の最大HP
public int MaxHp => maxHp;
[SerializeField] private float moveSpeed; // 敵の移動速度
public float MoveSpeed => moveSpeed;
[SerializeField] private AnimatorOverrideController overrideController; // 敵の移動アニメーション
public AnimatorOverrideController OverrideController => overrideController;
}
}変数のカプセル化
- アクセス修飾子を
publicからprivateに変更しました。 - 代わりに
publicなプロパティを通じてアクセスするようにしました。 [SerializeField]を追加して、Unityエディタから編集できるようにしました。
変数名の変更
- 変数名を具体的にすることで、コードの可読性を向上させました。
idをenemyIdに変更しました。nameをenemyNameに変更しました。speedをmoveSpeedに変更しました。
プロパティの命名規則の統一
プロパティ名はパスカルケース、フィールド名はキャメルケースに統一しました。
TurretSetting.cs のリファクタリング
TurretSetting.cs
using System;
using System.Collections.Generic;
using UnityEngine;
[CreateAssetMenu(fileName = "TurretSetting", menuName = "ScriptableObject/Turret Setting")]
public class TurretSetting : ScriptableObject
{
// 砲台のデータを格納するリスト
[SerializeField] private List<TurretData> turretDataList = new List<TurretData>();
public List<TurretData> TurretDataList => turretDataList;
// 砲台のデータ構造
[Serializable]
public class TurretData
{
[SerializeField] private string turretId; // 砲台のID
public string TurretId => turretId;
[SerializeField] private string turretName; // 砲台の名前
public string TurretName => turretName;
[SerializeField] private Sprite turretHeadSprite; // 砲身の画像
public Sprite TurretHeadSprite => turretHeadSprite;
[SerializeField] private List<TurretLevelData> turretLevelDataList = new List<TurretLevelData>(); // レベルごとのデータ
public List<TurretLevelData> TurretLevelDataList => turretLevelDataList;
}
// 砲台のレベル別データ構造
[Serializable]
public class TurretLevelData
{
[SerializeField] private int level; // 砲台のレベル
public int Level => level;
[SerializeField] private int attackPower; // 攻撃力
public int AttackPower => attackPower;
[SerializeField] private float attackInterval; // 攻撃間隔
public float AttackInterval => attackInterval;
[SerializeField] private float attackRange; // 攻撃範囲
public float AttackRange => attackRange;
[SerializeField] private int cost; // 建設・強化コスト
public int Cost => cost;
}
/// <summary>
/// 指定した砲台のレベル別データを取得する
/// </summary>
/// <param name="turretId">砲台のID</param>
/// <param name="level">取得したいレベル</param>
/// <returns>指定した砲台のレベル別データ、見つからない場合はnull</returns>
public TurretLevelData GetTurretData(string turretId, int level)
{
// 指定されたIDの砲台データを検索
TurretData turretData = turretDataList.Find(data => data.TurretId == turretId);
// 砲台データが見つかった場合
if (turretData != null)
{
// 指定されたレベルのレベル別データを検索して返す
return turretData.TurretLevelDataList.Find(levelData => levelData.Level == level);
}
// 砲台データが見つからなかった場合はnullを返す
return null;
}
}変数のカプセル化
- アクセス修飾子を
publicからprivateに変更しました。 - 代わりに
publicなプロパティを通じてアクセスするようにしました。 [SerializeField]を追加して、Unityエディタから編集できるようにしました。
変数名、クラス名の変更
- 変数名、クラス名を具体的にすることで、コードの可読性を向上させました。
idをturretIdに変更しました。nameをturretNameに変更しました。turrelLevelsをturretLevelDataListに変更しました。TurretLvDataをTurretLevelDataに変更しました。
プロパティの命名規則の統一
プロパティ名はパスカルケース、フィールド名はキャメルケースに統一しました。
PathData.cs のリファクタリング
PathData.cs
using System.Collections.Generic;
using UnityEngine;
public class PathData : MonoBehaviour
{
[SerializeField] private Transform startPosition; // スタート地点
public Transform StartPosition => startPosition;
[SerializeField] private List<Transform> pathPoints = new List<Transform>(); // 敵の移動経路のリスト
public List<Transform> PathPoints => pathPoints;
}変数名の変更
positionStartをstartPositionに変更しました。英語の語順を普通に怒られた感じです。pathArrayをpathPointsに変更しました。これは、配列ではなくリストを使用することにしたためです。
配列からリストへの変更
pathArrayをList<Transform> pathPointsに変更しました。リストは配列よりも柔軟性が高く、要素の追加や削除が容易です。
変数のカプセル化
- アクセス修飾子を
publicからprivateに変更しました。 - 代わりに
publicなプロパティを通じてアクセスするようにしました。 [SerializeField]を追加して、Unityエディタから編集できるようにしました。
GameManager.cs のリファクタリング
GameManager.cs
using UnityEngine;
using UnityEngine.EventSystems;
public class GameManager : MonoBehaviour
{
[SerializeField] private SideBarManager sideBarManager;
[SerializeField] private GameObject tileHighlighter; // ハイライト用オブジェクト
public GameObject TileHighlighter => tileHighlighter; // tileHighlighterにアクセスするためのプロパティ
private Vector2 tileSize = new Vector2(1, 1); // マス目のサイズ(必要に応じて変更)
void Update()
{
if (Input.GetMouseButtonDown(0)) // マウスの左ボタンがクリックされたら
{
DetectClick();
}
}
/// <summary>
/// マウスのクリックを検出
/// </summary>
private 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(); // 砲台情報を表示
snappedPosition = turret.transform.position; // 砲台の座標を取得
}
}
else // 砲台以外をクリックした場合
{
sideBarManager.HideTurretInfo(); // 砲台情報パネルを非表示
sideBarManager.HideTurretUpgradeInfo(); // 砲台強化情報パネルを非表示
}
// ハイライトの位置を更新
if (tileHighlighter != null)
{
tileHighlighter.transform.position = snappedPosition;
tileHighlighter.SetActive(true);
}
}
}フレームレート固定機能の削除
FixFrameRate()メソッドを削除しました。- 時間をフレーム単位で指定していた部分をすべて秒単位に変更することにしたので、フレームレートの固定機能は不要になりました。この仕様変更は厳密にいうとリファクタリングではないですね。
敵の生成関連のメソッドを削除
- 敵の生成に関連するメソッド(
AddEnemyToList(),CheckSpawnLimit())をEnemySpawner.csに移動するため、削除しました。
プロパティの追加
tileHighlighterに外部からアクセスするためのプロパティTileHighlighterを追加しました。
EnemySpawner.cs のリファクタリング
EnemySpawner.cs
using System.Collections;
using UnityEngine;
public class EnemySpawner : MonoBehaviour
{
[SerializeField] private EnemyController enemyPrefab; // 敵のプレハブ
[SerializeField] private PathData[] pathDataArray; // 移動経路情報の配列
[SerializeField] private int maxSpawnCount; // 敵の最大生成数
[SerializeField] private float spawnInterval; // 敵を生成する間隔(単位は秒)
[SerializeField] private GameManager gameManager; // GameManagerへの参照を追加
private int spawnedEnemyCount = 0; // これまでに生成された敵の数
private bool isSpawning = false; // 敵を生成するかどうかを制御するフラグ
private void Start()
{
isSpawning = true; // 敵を生成可能にする
StartCoroutine(ManageEnemySpawning());
}
/// <summary>
/// 敵の生成管理
/// </summary>
private IEnumerator ManageEnemySpawning()
{
while (isSpawning) // 敵を生成可能ならば
{
yield return new WaitForSeconds(spawnInterval); // 指定した秒数だけ待機
SpawnEnemy(); // 敵生成
AddEnemyToList(); // 敵の情報をListに追加
CheckSpawnLimit(); // 最大生成数を超えたら敵の生成停止
}
}
/// <summary>
/// 敵を生成する
/// </summary>
private void SpawnEnemy()
{
// ランダムな経路を選択
PathData selectedPath = pathDataArray[Random.Range(0, pathDataArray.Length)];
// 経路のスタート地点にプレハブから敵を生成
EnemyController enemyController = Instantiate(enemyPrefab, selectedPath.StartPosition.position, Quaternion.identity);
// ランダムな敵を選択
int enemyId = Random.Range(0, DBManager.Instance.EnemySetting.EnemyDataList.Count);
// 敵データの初期化
enemyController.InitializeEnemy(selectedPath, gameManager, DBManager.Instance.EnemySetting.EnemyDataList[enemyId]);
}
/// <summary>
/// 敵の情報をListに追加
/// </summary>
private void AddEnemyToList()
{
spawnedEnemyCount++; // 生成した敵の数を増やす
}
/// <summary>
/// 敵の生成が上限に達したかを確認
/// </summary>
private void CheckSpawnLimit()
{
if (spawnedEnemyCount >= maxSpawnCount) // 敵の最大生成数を超えたら
{
isSpawning = false; // 敵を生成不可にする
}
}
}メソッドの追加
- GameManager.csから移動してきたメソッド(
AddEnemyToList(),CheckSpawnLimit())を追加しました。
変数の追加
- GameManager.csから移動してきた以下の変数を追加しました。
maxSpawnCountを追加しました。spawnIntervalを追加しました。spawnedEnemyCountを追加しました。isSpawningを追加しました。
メソッド名の変更
ManageSpawning()をManageEnemySpawning()に変更しました。Spawn()をSpawnEnemy()に変更しました。
EnemyController.cs のリファクタリング
EnemyController.cs
using UnityEngine;
using DG.Tweening;
using System.Linq;
public class EnemyController : MonoBehaviour
{
[SerializeField, Header("移動速度")] private float moveSpeed;
[SerializeField, Header("最大HP")] private int maxHp;
private int currentHp;
private Tween moveTween;
private Vector3[] pathPoints;
private Animator animator;
private GameManager gameManager;
private EnemySetting.EnemyData enemyData;
[SerializeField] private HpBar hpBarPrefab;
private HpBar hpBar;
private void Update()
{
// HPバーの位置を更新
hpBar.UpdatePosition(transform.position);
}
/// <summary>
/// 敵を初期化する
/// </summary>
/// <param name="selectedPath">敵の移動経路</param>
/// <param name="gameManager">ゲームマネージャー</param>
/// <param name="enemyData">敵のデータ</param>
public void InitializeEnemy(PathData selectedPath, GameManager gameManager, EnemySetting.EnemyData enemyData)
{
this.enemyData = enemyData;
moveSpeed = this.enemyData.MoveSpeed;
maxHp = this.enemyData.MaxHp;
this.gameManager = gameManager;
currentHp = maxHp;
if (TryGetComponent(out animator))
{
// Animatorコンポーネントが取得できたら、アニメーションの上書きをする
SetUpAnimation();
}
// 経路を取得
pathPoints = selectedPath.PathPoints.Select(point => point.position).ToArray();
// 経路の総距離を計算
float totalDistance = CalculatePathLength(pathPoints);
// 移動時間を計算 (距離 ÷ 速度)
float moveDuration = totalDistance / moveSpeed;
// 経路に沿って移動する処理をmoveTween変数に代入
moveTween = transform.DOPath(pathPoints, moveDuration)
.SetEase(Ease.Linear)
.OnWaypointChange(ChangeWalkingAnimation);
hpBar = Instantiate(hpBarPrefab, transform);
// HPバーの初期化
hpBar.InitializeHpBar(maxHp);
// Canvas の子にする
hpBar.transform.SetParent(GameObject.Find("Canvas").transform, false);
}
/// <summary>
/// 経路の総距離を計算する
/// </summary>
/// <param name="path">経路座標の配列</param>
/// <returns>経路の総距離</returns>
private float CalculatePathLength(Vector3[] path)
{
float length = 0f;
for (int i = 0; i < path.Length - 1; i++)
{
// 各セグメントの距離を計算して合計
length += Vector3.Distance(path[i], path[i + 1]);
}
return length;
}
/// <summary>
/// 敵の進行方向を取得してアニメーションを変更する
/// </summary>
/// <param name="waypointIndex">現在の経由地のインデックス</param>
private void ChangeWalkingAnimation(int waypointIndex)
{
// 次の移動先がない場合は処理を終了
if (waypointIndex >= pathPoints.Length - 1)
{
return;
}
// 移動先の方向を計算
Vector2 direction = (pathPoints[waypointIndex + 1] - pathPoints[waypointIndex]).normalized;
// XとY方向をアニメーターに設定
animator.SetFloat("X", Mathf.Round(direction.x));
animator.SetFloat("Y", Mathf.Round(direction.y));
}
/// <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();
}
}
/// <summary>
/// 敵を破壊する
/// </summary>
private void DestroyEnemy()
{
// tween変数に代入されている処理を終了する
moveTween.Kill();
// HPバーを破壊
Destroy(hpBar.gameObject);
// 敵の破壊
Destroy(gameObject);
}
/// <summary>
/// アニメーションを設定する
/// </summary>
private void SetUpAnimation()
{
// アニメーション用のデータがあればアニメーションを上書きする
if (enemyData.OverrideController != null)
{
animator.runtimeAnimatorController = enemyData.OverrideController;
}
}
}変数名の変更
- 変数名を具体的にすることで、コードの可読性を向上させました。
speedをmoveSpeedに変更しました。hpをcurrentHpに変更しました。tweenをmoveTweenに変更しました。pathをpathPointsに変更しました。amountをdamageAmountに変更しました。
メソッド名の変更
CalcDamageをTakeDamageに変更しました。
Takeは思いつかないなぁ。
HpBar.cs のリファクタリング
HpBar.cs
using UnityEngine;
using UnityEngine.UI;
public class HpBar : MonoBehaviour
{
[SerializeField] private Slider hpSlider; // HPを表示するスライダー
[SerializeField] private Vector3 positionOffset; // HPバーの表示位置オフセット
/// <summary>
/// HPバーを初期化する
/// </summary>
/// <param name="maxHp">最大HP</param>
public void InitializeHpBar(int maxHp)
{
// スライダーの最大値を設定
hpSlider.maxValue = maxHp;
// スライダーの現在値を最大値に設定
hpSlider.value = maxHp;
}
/// <summary>
/// HPバーを更新する
/// </summary>
/// <param name="currentHp">現在のHP</param>
public void UpdateHpBar(int currentHp)
{
// スライダーの値を現在のHPに設定
hpSlider.value = currentHp;
}
/// <summary>
/// HPバーの位置を更新する
/// </summary>
/// <param name="targetPosition">HPバーを表示する対象の位置</param>
public void UpdatePosition(Vector3 targetPosition)
{
// 対象の位置にオフセットを加算して、スクリーン座標に変換し、HPバーの位置を更新
transform.position = Camera.main.WorldToScreenPoint(targetPosition + positionOffset);
}
}変数名の変更
- 変数名を具体的にすることで、コードの可読性を向上させました。
sliderをhpSliderに変更しました。offsetをpositionOffsetに変更しました。enemyPositionをtargetPositionに変更しました。
TurretGenerator.cs のリファクタリング
TurretGenerator.cs
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.Tilemaps;
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()
{
// マウスカーソルの位置を取得
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))
{
if (canPlaceTurret)
{
PlaceTurret(currentGridPosition);
}
else
{
// 設置不可の場合は選択をリセット
ResetTurretSelection();
}
}
}
/// <summary>
/// 砲台を配置する
/// </summary>
/// <param name="gridPosition">配置するグリッド座標</param>
private void PlaceTurret(Vector3Int gridPosition)
{
// 砲台が選択されていなければ何もしない
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);
turretController.OnTurretClicked(); // 砲台がクリックされたときの処理
// 配置されたセルを登録
placedTurretCells.Add(gridPosition);
// 選択をリセット
ResetTurretSelection();
}
/// <summary>
/// 砲台の設置が可能かどうかを判定する
/// </summary>
/// <param name="gridPosition">判定するグリッド座標</param>
/// <returns>設置可能かどうか</returns>
private bool IsTurretPlacementValid(Vector3Int gridPosition)
{
// タイルマップにコライダーがない かつ 配置済みのセルでない場合
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];
// アイコンを表示
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);
}
}変数名の変更
- 変数名を具体的にすることで、コードの可読性を向上させました。
tilemapsをunbuildableTilemapに変更しました。selectedTurretIconをturretIconに変更しました。selectedTurretHeadをturretHeadIconに変更しました。selectedTurretCrossをcannotBuildIconに変更しました。gridPosをcurrentGridPositionに変更しました。occupiedCellsをplacedTurretCellsに変更しました。
メソッド名の変更
GenerateTurret()をPlaceTurret()に変更しました。
メソッドの追加
IsTurretPlacementValid()メソッドを追加しました。ResetTurretSelection()メソッドを追加しました。
TurretController.cs のリファクタリング
TurretController.cs
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.UI;
public class TurretController : MonoBehaviour
{
private int currentAttackPower; // 現在の攻撃力
private float currentAttackInterval; // 現在の攻撃間隔(単位は秒)
[SerializeField] private Transform turretHeadTransform; // 砲身のTransform
[SerializeField] private GameObject shellPrefab; // 砲弾のプレハブ
[SerializeField] private Transform firePointTransform; // 砲弾の発射位置
[SerializeField] private CircleCollider2D attackRangeCollider; //攻撃範囲のコライダー
[SerializeField] private SpriteRenderer turretHeadSpriteRenderer; // 砲身のSpriteRenderer
[SerializeField] private SpriteRenderer levelIconSpriteRenderer; // レベルアイコンのSpriteRenderer
[SerializeField] private Sprite[] levelIcons; // レベルアイコンの配列
private List<EnemyController> enemiesInAttackRange = new List<EnemyController>(); // 攻撃範囲内の敵リスト
private EnemyController currentTargetEnemy = null; // 現在のターゲット
private bool isCurrentlyAttacking = false; // 攻撃中フラグ
private Coroutine currentAttackCoroutine; // 現在の攻撃コルーチン
private TurretSetting.TurretData currentTurretData; // 砲台データ
private TurretSetting.TurretLevelData currentTurretLevelData; // 砲台レベル別データ
[SerializeField] private LineRenderer attackRangeLineRenderer; // 攻撃範囲を描画するLineRenderer
private bool isAttackRangeVisible = false; // 攻撃範囲の表示状態
private Coroutine hideAttackRangeCoroutine; // 攻撃範囲を非表示にするコルーチン
private bool isCurrentlyUpgrading = false; // アップグレード中フラグ
[SerializeField] private GameObject upgradeIndicatorPrefab; // インジケータのプレハブ
private GameObject currentUpgradeIndicator; // 現在のインジケータ
private void Awake()
{
InitializeAttackRangeLineRenderer(); // 攻撃範囲ラインの初期化
}
private void Update()
{
UpdateCurrentTargetEnemy(); // 最も近い敵を探す
if (currentTargetEnemy) // ターゲットが存在する場合
{
RotateTurretHeadTowardsEnemy(); // 砲身をターゲットの方向に向ける
}
}
private void OnTriggerEnter2D(Collider2D collision)
{
if (collision.TryGetComponent(out EnemyController enemy)) // 侵入してきたのが敵ならば
{
// 敵リストに追加
if (!enemiesInAttackRange.Contains(enemy))
{
enemiesInAttackRange.Add(enemy);
}
// 攻撃していなければ開始
if (!isCurrentlyAttacking)
{
isCurrentlyAttacking = true;
currentAttackCoroutine = StartCoroutine(ManageAttacks());
}
}
}
private void OnTriggerExit2D(Collider2D collision)
{
if (collision.TryGetComponent(out EnemyController enemy)) // 出ていったのが敵ならば
{
enemiesInAttackRange.Remove(enemy); // リストから削除
// ターゲットが出て行った場合、別のターゲットを選択
if (currentTargetEnemy == enemy)
{
currentTargetEnemy = null;
UpdateCurrentTargetEnemy();
}
// もし範囲内に敵がいなくなったら攻撃をやめる
if (enemiesInAttackRange.Count == 0)
{
isCurrentlyAttacking = false;
if (currentAttackCoroutine != null)
{
StopCoroutine(currentAttackCoroutine);
currentAttackCoroutine = null;
}
}
}
}
/// <summary>
/// 攻撃範囲ラインの初期化
/// </summary>
private void InitializeAttackRangeLineRenderer()
{
attackRangeLineRenderer.positionCount = 50;
attackRangeLineRenderer.startWidth = 0.08f;
attackRangeLineRenderer.endWidth = 0.08f;
attackRangeLineRenderer.loop = true;
attackRangeLineRenderer.material = new Material(Shader.Find("Sprites/Default"));
attackRangeLineRenderer.startColor = new Color(0, 0, 1, 0.3f);
attackRangeLineRenderer.endColor = new Color(0, 0, 1, 0.3f);
attackRangeLineRenderer.sortingLayerName = "Object";
attackRangeLineRenderer.sortingOrder = 0;
attackRangeLineRenderer.enabled = false; // 最初は非表示
}
/// <summary>
/// 砲台データを初期化
/// </summary>
public void InitializeTurret(TurretSetting.TurretData data, int level)
{
currentTurretData = data;
currentTurretLevelData = DBManager.Instance.TurretSetting.GetTurretData(data.TurretId, level);
currentAttackPower = currentTurretLevelData.AttackPower; // 攻撃力を設定
currentAttackInterval = currentTurretLevelData.AttackInterval; // 攻撃間隔を設定
attackRangeCollider.radius = currentTurretLevelData.AttackRange; // 攻撃範囲を設定
turretHeadSpriteRenderer.sprite = currentTurretData.TurretHeadSprite; // 砲身の画像を設定
UpdateLevelIcon(level); // レベルアイコンを更新
}
/// <summary>
/// レベルアイコンを更新する
/// </summary>
/// <param name="level">現在のレベル</param>
private void UpdateLevelIcon(int level)
{
if (level >= 1 && level <= levelIcons.Length)
{
levelIconSpriteRenderer.sprite = levelIcons[level - 1];
}
else
{
Debug.LogWarning($"レベル {level} のアイコンが設定されていません");
}
}
/// <summary>
/// 砲台に最も近い敵を選択
/// </summary>
private void UpdateCurrentTargetEnemy()
{
if (enemiesInAttackRange.Count == 0) // 攻撃範囲に敵がいなければ
{
currentTargetEnemy = null; // ターゲットなし
return;
}
float closestDistance = float.MaxValue; // 最も近い敵までの距離に最大値を代入
EnemyController closestEnemy = null; // 最も近い敵
// 攻撃範囲内のすべての敵をチェック
foreach (var enemy in enemiesInAttackRange)
{
// 砲台と敵の距離を計算
float distance = Vector2.Distance(transform.position, enemy.transform.position);
// より近い敵を記録
if (distance < closestDistance)
{
closestDistance = distance;
closestEnemy = enemy;
}
}
currentTargetEnemy = closestEnemy; // 最も近い敵をターゲットとして設定
}
/// <summary>
/// 攻撃間隔管理
/// </summary>
public IEnumerator ManageAttacks()
{
// 攻撃状態の間ループ処理を繰り返す
while (isCurrentlyAttacking)
{
if (currentTargetEnemy && !isCurrentlyUpgrading) // ターゲットが存在し、アップグレード中でない場合
{
Attack(); // 攻撃を実行
}
// 次の攻撃まで待機
yield return new WaitForSeconds(currentAttackInterval);
}
}
/// <summary>
/// 攻撃
/// </summary>
private void Attack()
{
if (!currentTargetEnemy || !shellPrefab || !firePointTransform) return;
// 砲弾を生成
GameObject shell = Instantiate(shellPrefab, firePointTransform.position, firePointTransform.rotation);
// ShellControllerに敵情報を渡す
ShellController shellController = shell.GetComponent<ShellController>();
if (shellController)
{
shellController.Initialize(currentTargetEnemy, currentAttackPower);
}
}
/// <summary>
/// 砲身を敵の方向に回転させる
/// </summary>
private void RotateTurretHeadTowardsEnemy()
{
// ターゲットが存在しない場合、何もしない
if (!currentTargetEnemy || isCurrentlyUpgrading) return;
// 敵の位置と砲身の位置の差分を計算
Vector3 direction = currentTargetEnemy.transform.position - turretHeadTransform.position;
// Z軸方向の回転角度を計算
float angle = Mathf.Atan2(direction.y, direction.x) * Mathf.Rad2Deg - 90.0f;
// 砲身を回転させる
turretHeadTransform.rotation = Quaternion.Euler(0, 0, angle);
}
/// <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); // 砲台強化情報パネルを表示
}
// 攻撃範囲の表示切り替え
ToggleAttackRangeVisibility();
}
/// <summary>
/// 攻撃範囲の表示/非表示を切り替える
/// </summary>
private void ToggleAttackRangeVisibility()
{
isAttackRangeVisible = !isAttackRangeVisible;
attackRangeLineRenderer.enabled = isAttackRangeVisible;
if (isAttackRangeVisible)
{
DrawAttackRange();
// すでにコルーチンが動いていたら一度止める
if (hideAttackRangeCoroutine != null)
{
StopCoroutine(hideAttackRangeCoroutine);
}
// 新しくコルーチンを開始
hideAttackRangeCoroutine = StartCoroutine(HideAttackRangeAfterDelay(4f));
}
}
/// <summary>
/// 攻撃範囲を描画
/// </summary>
private void DrawAttackRange()
{
float radius = attackRangeCollider.radius; // 攻撃範囲の半径
Vector3 center = transform.position; // 砲台の位置
int points = attackRangeLineRenderer.positionCount; // 円を描くための点の数
for (int i = 0; i < points; i++)
{
float angle = i * 2 * Mathf.PI / points;
float x = center.x + Mathf.Cos(angle) * radius;
float y = center.y + Mathf.Sin(angle) * radius;
attackRangeLineRenderer.SetPosition(i, new Vector3(x, y, 0));
}
}
/// <summary>
/// 一定時間後に攻撃範囲を非表示にするコルーチン
/// </summary>
private IEnumerator HideAttackRangeAfterDelay(float delay)
{
yield return new WaitForSeconds(delay);
isAttackRangeVisible = false;
attackRangeLineRenderer.enabled = false;
}
/// <summary>
/// 砲台をアップグレードするコルーチン
/// </summary>
public IEnumerator UpgradeTurretCoroutine()
{
// アップグレード開始時の処理
StartUpgrade();
// アップグレード処理
yield return StartCoroutine(PerformUpgrade());
// アップグレード完了後の処理
FinishUpgrade();
}
/// <summary>
/// アップグレード開始時の処理
/// </summary>
private void StartUpgrade()
{
isCurrentlyUpgrading = true; // アップグレード開始
attackRangeLineRenderer.enabled = false; // 攻撃範囲を非表示
// インジケータを生成
currentUpgradeIndicator = Instantiate(upgradeIndicatorPrefab, transform.position, Quaternion.identity);
// 親をCanvasに設定
Canvas canvas = FindObjectOfType<Canvas>();
currentUpgradeIndicator.transform.SetParent(canvas.transform, false);
// ワールド座標をスクリーン座標に変換
Vector2 screenPosition = Camera.main.WorldToScreenPoint(transform.position);
currentUpgradeIndicator.GetComponent<RectTransform>().position = screenPosition;
}
/// <summary>
/// アップグレード処理
/// </summary>
private IEnumerator PerformUpgrade()
{
// 塗りつぶしImageを取得
Image fillImage = currentUpgradeIndicator.transform.Find("UpgradeIndicatorFill").GetComponent<Image>();
int newLevel = currentTurretLevelData.Level + 1; // アップグレード後のレベル
float upgradeTime = newLevel * 6.0f - 11; // 待機時間(最小値を1秒に設定)
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;
}
}
/// <summary>
/// アップグレード完了後の処理
/// </summary>
private void FinishUpgrade()
{
int newLevel = currentTurretLevelData.Level + 1;
// アップグレード後の砲台情報を取得
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); // 強化ボタンを活性化
}
else
{
Debug.LogError("SideBarManager が見つかりません");
}
isCurrentlyUpgrading = false; // アップグレード終了
Destroy(currentUpgradeIndicator); // インジケータを削除
}
}変数名の変更
- 変数名を具体的にすることで、コードの可読性を向上させました。
attackPowerをcurrentAttackPowerに変更しました。attackIntervalをcurrentAttackIntervalに変更しました。turretHeadをturretHeadTransformに変更しました。firePointをfirePointTransformに変更しました。attackRangeをattackRangeColliderに変更しました。enemiesInRangeをenemiesInAttackRangeに変更しました。targetEnemyをcurrentTargetEnemyに変更しました。isAttackingをisCurrentlyAttackingに変更しました。attackCoroutineをcurrentAttackCoroutineに変更しました。turretDataをcurrentTurretDataに変更しました。turretLevelDataをcurrentTurretLevelDataに変更しました。lineRendererをattackRangeLineRendererに変更しました。isRangeVisibleをisAttackRangeVisibleに変更しました。hideRangeCoroutineをhideAttackRangeCoroutineに変更しました。isUpgradingをisCurrentlyUpgradingに変更しました。currentIndicatorをcurrentUpgradeIndicatorに変更しました。
メソッド名の変更
UpdateTargetEnemy()をUpdateCurrentTargetEnemy()に変更しました。HideRangeAfterDelay()をHideAttackRangeAfterDelay()に変更しました。
メソッドの追加
ToggleAttackRangeVisibility()メソッドを追加し、OnTurretClicked()から処理を分離しました。UpdateLevelIcon()メソッドを追加し、InitializeTurret()から処理を分離しました。InitializeAttackRangeLineRenderer()メソッドを追加し、Awake()から処理を分離しました。UpgradeTurretCoroutine()をStartUpgrade(),PerformUpgrade(),FinishUpgrade()に分割しました。
ShellController.cs のリファクタリング
ShellController.cs
using UnityEngine;
public class ShellController : MonoBehaviour
{
[SerializeField, Header("砲弾の速度")] private float shellSpeed = 5.0f; // 砲弾の速度
private EnemyController targetEnemy; // ターゲットとなる敵
private int attackPower; // 砲弾の攻撃力
private void Update()
{
// ターゲットがいない場合、砲弾を破棄する
if (targetEnemy == null)
{
Destroy(gameObject);
return;
}
MoveTowardsTarget(); // 敵に向かって移動する
}
/// <summary>
/// 砲弾を初期化する
/// </summary>
/// <param name="enemy">ターゲットとなる敵</param>
/// <param name="power">砲弾の攻撃力</param>
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.TakeDamage(attackPower);
// 砲弾を破棄する
Destroy(gameObject);
}
}
}
/// <summary>
/// ターゲットに向かって移動する
/// </summary>
private void MoveTowardsTarget()
{
// 敵への方向を計算
Vector3 direction = (targetEnemy.transform.position - transform.position).normalized;
// 砲弾を移動させる
transform.position += direction * shellSpeed * Time.deltaTime;
}
}変数名の変更
- 変数名を具体的にすることで、コードの可読性を向上させました。
speedをshellSpeedに変更しました。
メソッドの追加
MoveTowardsTarget()メソッドを追加し、Update()から処理を分離しました。
SideBarManager.cs のリファクタリング
SideBarManager.cs
using UnityEngine;
using TMPro;
using UnityEngine.UI;
public class SideBarManager : MonoBehaviour
{
[SerializeField, Header("砲台情報パネル")] private GameObject turretInfoPanel;
[SerializeField, Header("砲台名テキスト")] private TextMeshProUGUI turretNameText;
[SerializeField, Header("建設コストテキスト")] private TextMeshProUGUI buildingCostText;
[SerializeField, Header("攻撃力テキスト")] private TextMeshProUGUI attackPowerText;
[SerializeField, Header("攻撃間隔テキスト")] private TextMeshProUGUI attackIntervalText;
[SerializeField, Header("攻撃範囲テキスト")] private TextMeshProUGUI attackRangeText;
[SerializeField, Header("売却ボタン")] private Button sellButton;
[SerializeField, Header("砲台強化情報パネル")] private GameObject turretUpgradeInfoPanel;
[SerializeField, Header("レベルアップテキスト")] private TextMeshProUGUI levelUpText;
[SerializeField, Header("強化コストテキスト")] private TextMeshProUGUI upgradeCostText;
[SerializeField, Header("強化攻撃力テキスト")] private TextMeshProUGUI upgradeAttackPowerText;
[SerializeField, Header("強化攻撃間隔テキスト")] private TextMeshProUGUI upgradeAttackIntervalText;
[SerializeField, Header("強化攻撃範囲テキスト")] private TextMeshProUGUI upgradeAttackRangeText;
[SerializeField, Header("強化ボタン")] private Button upgradeButton;
[SerializeField, Header("強化ボタンテキスト")] private TextMeshProUGUI upgradeButtonText;
private TurretController selectedTurret; // 現在選択されている砲台
private void Start()
{
// 強化ボタンにクリックイベントを追加
upgradeButton.onClick.AddListener(UpgradeSelectedTurret);
}
/// <summary>
/// 砲台情報パネルを表示する
/// </summary>
/// <param name="turretData">砲台データ</param>
/// <param name="turretLevelData">砲台レベル別データ</param>
/// <param name="isSellButtonInteractable">売却ボタンのインタラクト可否</param>
/// <param name="turret">砲台コントローラー</param>
public void ShowTurretInfo(TurretSetting.TurretData turretData, TurretSetting.TurretLevelData turretLevelData, bool isSellButtonInteractable, TurretController turret)
{
selectedTurret = turret; // 現在選択された砲台を保存
turretNameText.text = $"<cspace=-0.2em>{turretData.TurretName}</cspace> Lv.{turretLevelData.Level}";
buildingCostText.text = turretLevelData.Cost.ToString();
attackPowerText.text = turretLevelData.AttackPower.ToString();
attackIntervalText.text = turretLevelData.AttackInterval.ToString();
attackRangeText.text = turretLevelData.AttackRange.ToString();
// 売却ボタンのインタラクト可否を設定
sellButton.interactable = isSellButtonInteractable;
// 砲台情報パネルを表示
turretInfoPanel.SetActive(true);
}
/// <summary>
/// 砲台情報パネルを非表示にする
/// </summary>
public void HideTurretInfo()
{
// 砲台情報パネルを非表示
turretInfoPanel.SetActive(false);
}
/// <summary>
/// 砲台強化情報パネルを表示する
/// </summary>
/// <param name="turretData">砲台データ</param>
/// <param name="turretLevelData">砲台レベル別データ</param>
/// <param name="isUpgradeButtonInteractable">強化ボタンのインタラクト可否</param>
public void ShowTurretUpgradeInfo(TurretSetting.TurretData turretData, TurretSetting.TurretLevelData turretLevelData, bool isUpgradeButtonInteractable)
{
// 次のレベルの砲台データを取得
TurretSetting.TurretLevelData nextLevelTurretData = DBManager.Instance.TurretSetting.GetTurretData(turretData.TurretId, turretLevelData.Level + 1);
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}";
upgradeButtonText.text = $"{nextLevelTurretData.Cost}Gで強化";
// 強化ボタンのインタラクト可否を設定
upgradeButton.interactable = isUpgradeButtonInteractable;
// 砲台強化情報パネルを表示
turretUpgradeInfoPanel.SetActive(true);
}
/// <summary>
/// 砲台強化情報パネルを非表示にする
/// </summary>
public void HideTurretUpgradeInfo()
{
// 砲台強化情報パネルを非表示
turretUpgradeInfoPanel.SetActive(false);
}
/// <summary>
/// 選択された砲台をアップグレードする
/// </summary>
private void UpgradeSelectedTurret()
{
// 選択された砲台が存在する場合
if (selectedTurret != null)
{
// 砲台のアップグレードコルーチンを開始
StartCoroutine(selectedTurret.UpgradeTurretCoroutine());
}
}
/// <summary>
/// 強化ボタンのインタラクト可否を設定する
/// </summary>
/// <param name="isInteractable">インタラクト可否</param>
public void SetUpgradeButtonInteractable(bool isInteractable)
{
// 強化ボタンのインタラクト可否を設定
upgradeButton.interactable = isInteractable;
}
}変数名の変更
- 変数名を具体的にすることで、コードの可読性を向上させました。
turretInfoをturretInfoPanelに変更しました。turretUpgradeInfoをturretUpgradeInfoPanelに変更しました。turretLvDataをturretLevelDataに変更しました。sellButtonInteractableをisSellButtonInteractableに変更しました。upgradeButtonInteractableをisUpgradeButtonInteractableに変更しました。turretNextLvDataをnextLevelTurretDataに変更しました。interactableをisInteractableに変更しました。
変数の値を再設定
リファクタリングによって名前が変わった変数があるので、それらの値を再設定します。
Unityエディタのインスペクターで、ひとつひとつ設定していきます。設定漏れがあれば、ゲームを実行したときにエラーが出ます。
さいごに
今回はコードのリファクタリングをしました。
12個のファイルを一気に修正したので不具合が出ないか心配でしたが、なんとか無事に終わりました。今回のようにまとめてやるのではなくて、こまめにやったほうがリスクは少なそうですね。
でわでわ

コメント