Unity初心者が2Dタワーディフェンスを作っています。今回は砲台をクリックしたときに砲台の情報をサイドバーに表示する処理を実装します。
- Mac mini (M1, 2020)
- Unity 2022.3.36f1
砲台情報パネルを作る
まずは、砲台情報を表示するためのパネルをサイドバー上に作成します。完成図はこんな感じです。

ヒエラルキーのSideBarオブジェクト上で右クリック「UI > Panel」を選択して、SideBarの配下にPanelオブジェクトを作成します。名前をTurretInfoにします。

TurretInfoオブジェクトの位置と色を適当に調整しましょう。
インスペクターの「Add Component」ボタンをクリックして、「Vertical Layout Group」コンポーネントを追加します。

これは、子オブジェクトを縦方向に整列させるための便利なコンポーネントです。
TurretInfoオブジェクトの配下にTextMeshProオブジェクトを作成します。
- 「TMP Importer」ウインドウが表示されたら「Import TMP Essentials」ボタンをクリックします。
- 「Assets > Fonts」フォルダを作成して、日本語フォントをインポートします。
- メニューの「Window > TextMeshPro > Font Asset Creator」を選択して「Font Asset Creator」ウインドウを開きます。
- 次のように設定して「Generate Font Atlas」ボタンをクリックします。
- 次の画面で「Save」ボタンを押して保存します。
TurretInfoの下には、砲台の名前、攻撃力、攻撃範囲、攻撃速度などの情報を表示するためのTextMeshProオブジェクトを作成します。こんな感じです。

Vertical Layout Groupの設定をして、子オブジェクトがいい感じに並ぶようにします。

インスペクター左上のチェックを外してTurretInfoを非表示にしておきます。

SideBarManagerスクリプトの作成
サイドバーを管理するためのスクリプトを作ります。
「Assets > Scripts」フォルダの下にC#スクリプトを作成します。名前を「SideBarManager」にします。内容は次のとおりです。
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using TMPro; // TextMeshProを使うために必要な宣言
public class SideBarManager : MonoBehaviour
{
[SerializeField] private GameObject turretInfo; // 砲台情報パネルのオブジェクト
[SerializeField] private TextMeshProUGUI turretNameText; // 砲台の名前のテキスト
[SerializeField] private TextMeshProUGUI buildingCostText; // 建設コストのテキスト
[SerializeField] private TextMeshProUGUI attackPowerText; // 攻撃力のテキスト
[SerializeField] private TextMeshProUGUI attackIntervalText; // 攻撃速度のテキスト
[SerializeField] private TextMeshProUGUI attackRangeText; // 攻撃範囲のテキスト
/// <summary>
/// 砲台情報を表示
/// </summary>
public void ShowTurretInfo(TurretSetting.TurretData turretData)
{
turretNameText.text = turretData.name;
buildingCostText.text = turretData.buildingCost.ToString();
attackPowerText.text = turretData.attackPower.ToString();
attackIntervalText.text = turretData.attackInterval.ToString();
attackRangeText.text = turretData.attackRange.ToString();
turretInfo.SetActive(true); // 砲台情報パネルを表示する
}
/// <summary>
/// 砲台情報を非表示
/// </summary>
public void HideTurretInfo()
{
turretInfo.SetActive(false); // 砲台情報パネルを非表示にする
}
}
ShowTurretInfo()
は砲台情報の各テキストオブジェクトにデータを代入してTurretInfoオブジェクトを表示するメソッドです。
HideTurretInfo()
はTurretInfoオブジェクトを非表示にするメソッドです。とてもシンプルですね。
SideBarManagerスクリプトをSideBarオブジェクトにドラッグ&ドロップしてアタッチします。
SideBarオブジェクトのインスペクターで「Side Bar Manager (Script)」下の各変数にオブジェクトをドラッグ&ドロップしてアサインします。
サイドバーの砲台アイコン選択時
まずは、サイドバーの砲台アイコンをクリックしたときに砲台情報を表示する処理を実装します。
TurretGeneratorスクリプトを修正します。
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.Tilemaps; // Tilemapを使うために必要な宣言
public class TurretGenerator : MonoBehaviour
{
[SerializeField] private GameObject turretPrefab; // 砲台のプレハブ
[SerializeField] private Grid grid; // Grid_BaseのGrid、Tilemapの座標を取得するため
[SerializeField] private Tilemap tilemaps; // Grid_WayのTilemap
[SerializeField] private GameObject selectedTurretIcon; // 追随する砲台アイコン
[SerializeField] private GameObject selectedTurretHead; // 追随する砲台アイコンの砲身
[SerializeField] private GameObject selectedTurretCross; // バツ印のオブジェクト
// ここから 変数追加
[SerializeField] private SideBarManager sideBarManager; // SideBarmanagerスクリプトの参照
// ここまで
private Vector3Int gridPos; // Tilemapのセル座標
private HashSet<Vector3Int> occupiedCells = new HashSet<Vector3Int>(); // 砲台配置済みセルを代入
private TurretSetting.TurretData selectedTurretData = null; // 選択された砲台のデータ
void Update()
{
// ...省略...
}
/// <summary>
/// 砲台生成
/// </summary>
/// <param name="gridPos"></param>
private void GenerateTurret(Vector3Int gridPos)
{
// ...省略...
}
/// <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;
// ここから
sideBarManager.ShowTurretInfo(selectedTurretData); // 砲台情報を表示
// ここまで
}
}
砲台を選択したときにShowTurretInfo()
を呼び出して砲台情報を表示します。引数として選択した砲台のデータ(selectedTurretData
)を渡しています。
TurretGeneratorオブジェクトのインスペクターで「TurretGenerator (Script) > Side Bar Manager」にSideBarオブジェクトをドラッグ&ドロップしてアサインします。
以上で、サイドバーの砲台アイコンをクリックしたときに砲台情報が表示されるようになりました。
マップの砲台選択時
マップ上に設置された砲台をクリックしたときに砲台情報を表示する処理を実装します。こちらはかなりややこしい処理になります。
GameManagerスクリプトを修正します。
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
// ここから
using UnityEngine.EventSystems; // EventSystemを使用するために必要
// ここまで
public class GameManager : MonoBehaviour
{
[SerializeField] private int targetFrameRate = 60; // フレームレートの目標値
[SerializeField] private EnemySpawner enemySpawner;
// ここから
[SerializeField] private SideBarManager sideBarManager; // SideBarmanagerスクリプトの参照
// ここまで
public bool isSpawning; // 敵を生成するかどうかを制御するフラグ
public int spawnInterval; // 敵を生成する間隔(単位はフレーム)
public int spawnedEnemyCount; // これまでに生成された敵の数
public int maxSpawnCount; // 敵の最大生成数
void Awake()
{
// ...省略...
}
void Start()
{
// ...省略...
}
// ここから
void Update()
{
if (Input.GetMouseButtonDown(0)) // マウスの左ボタンがクリックされたら
{
DetectClick();
}
}
// ここまで
/// <summary>
/// フレームレートを固定
/// </summary>
private void FixFrameRate()
{
// ...省略...
}
/// <summary>
/// 敵の情報をListに追加
/// </summary>
public void AddEnemyToList()
{
// ...省略...
}
/// <summary>
/// 敵の生成が上限に達したかを確認
/// </summary>
public void CheckSpawnLimit()
{
// ...省略...
}
// ここから
/// <summary>
/// マウスのクリックを検出
/// </summary>
void DetectClick()
{
// UIがクリックされた場合は何もしない
if (EventSystem.current.IsPointerOverGameObject())
{
return;
}
// クリック位置のスクリーン座標をワールド座標に変換
Vector2 worldPoint = Camera.main.ScreenToWorldPoint(Input.mousePosition);
// クリック位置から見えるオブジェクトに対してレイキャスト(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(turret); // 砲台情報を表示
return;
}
}
// 砲台以外をクリックした場合は砲台情報パネルを非表示
sideBarManager.HideTurretInfo();
}
// ここまで
}
Update()
は毎フレーム呼び出されるメソッドです。この中でマウスの左ボタンクリックが検出されたらDetectClick()
メソッドを呼び出します。
71行目、EventSystem.current.IsPointerOverGameObject()
は、マウスクリックがUIオブジェクト上で行われたかどうかを判定するためのメソッドです。UIがクリックされたときには何もしない(砲台情報パネルを非表示にしない)ようにしています。これは砲台情報パネル上のボタンをクリックできるようにするためです。
78行目、Physics2D.Raycast()
を使ってクリック位置にあるオブジェクトを取得しています。
- 第1引数: レイの開始位置(
worldPoint
はクリック位置のワールド座標) - 第2引数: レイの進む方向(
Vector2.zero
はゼロベクトルなので、その点にあるオブジェクトを指す) - 第3引数: レイの最大距離(
Mathf.Infinity
は無限の距離をあらわすが、方向がゼロベクトルなのでクリック位置の一点のみが対象になる) - 第4引数: レイヤーマスク(
~LayerMask.GetMask("Ignore Raycast")
でレイヤーがIgnore Raycast
のオブジェクトを無視する)
79 – 87行目、クリックしたオブジェクトが砲台ならば、砲台情報を表示します。
最初、私は砲台オブジェクトにButtonコンポーネントを追加してOnClick()
でこの処理を実装しようとしました。でもダメでした。なぜならButtonコンポーネントはUIオブジェクトでしか動作しないからです。マップ上に設置された砲台はUIオブジェクトではありません。残念無念。
次に、砲台オブジェクトにコライダーを追加してOnMouseDown()
でクリックを検知しようとしました。ダメでした。砲台オブジェクトの子オブジェクトにAttackRangeがあって、こいつもコライダーを持っています。そのコライダーにもクリックが反応してしまうのです。残念無念。
というわけで、レイキャストを使うことになりました。
で、レイキャストとは何かというと、ある点から特定の方向に向かって仮想の光線(レイ)を飛ばして衝突するオブジェクトを検出する機能のことです。
ここでは、クリックした点から同じ点にレイを飛ばして、レイヤーが”Ignore Raycast”ではないオブジェクトを検出しています。
GameManagerオブジェクトのインスペクターで「Game Manager (Script) > Side Bar Manager」にSideBarオブジェクトをドラッグ&ドロップしてアサインします。
TurretControllerスクリプトを修正します。
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public class TurretController : MonoBehaviour
{
[SerializeField] private int attackPower = 1; // 攻撃力
[SerializeField] private float attackInterval = 60.0f; // 攻撃間隔(単位はフレーム)
[SerializeField] private Transform turretHead; // 砲身のTransform
[SerializeField] private GameObject shellPrefab; // 砲弾のプレハブ
[SerializeField] private Transform firePoint; // 砲弾の発射位置
[SerializeField] private CircleCollider2D attackRange; //攻撃範囲のコライダー
[SerializeField] private SpriteRenderer turretHeadSpriteRenderer; // 砲身のSpriteRenderer
private List<EnemyController> enemiesInRange = new List<EnemyController>(); // 攻撃範囲内の敵リスト
private EnemyController targetEnemy = null; // 現在のターゲット
private bool isAttacking = false; // 攻撃中フラグ
private Coroutine attackCoroutine; // 現在の攻撃コルーチン
// ここから
private TurretSetting.TurretData turretData; // 砲台データ
// ここまで
private void Update()
{
// ...省略...
}
private void OnTriggerEnter2D(Collider2D collision)
{
// ...省略...
}
private void OnTriggerExit2D(Collider2D collision)
{
// ...省略...
}
/// <summary>
/// 砲台データを初期化
/// </summary>
// ここから
//public void InitializeTurret(TurretSetting.TurretData turretData)
public void InitializeTurret(TurretSetting.TurretData data)
{
turretData = data;
// ここまで
attackPower = turretData.attackPower; // 攻撃力を設定
attackInterval = turretData.attackInterval; // 攻撃間隔を設定
attackRange.radius = turretData.attackRange; // 攻撃範囲を設定
turretHeadSpriteRenderer.sprite = turretData.turretHeadSprite; // 砲身の画像を設定
Debug.Log($"砲台を初期化: {turretData.name}");
}
/// <summary>
/// 砲台に最も近い敵を選択
/// </summary>
private void UpdateTargetEnemy()
{
// ...省略...
}
/// <summary>
/// 攻撃間隔管理
/// </summary>
public IEnumerator ManageAttacks()
{
// ...省略...
}
/// <summary>
/// 攻撃
/// </summary>
private void Attack()
{
// ...省略...
}
/// <summary>
/// 砲身を敵の方向に回転させる
/// </summary>
private void RotateTurretHeadTowardsEnemy()
{
// ...省略...
}
// ここから
/// <summary>
/// 砲台がクリックされたときの処理
/// </summary>
public void OnTurretClicked(TurretController turret)
{
SideBarManager sideBarManager = FindObjectOfType<SideBarManager>();
sideBarManager.ShowTurretInfo(turretData);
Debug.Log($"クリックした砲台: {turretData.name}");
}
// ここまで
}
OnTurretClicked()
を追加しました。マップ上の砲台がクリックされたときに砲台情報を表示するメソッドです。
「Assets > Prefabs > Turret」をダブルクリックしてTurretプレハブの編集モードに入ります。
Box Collider 2Dコンポーネントを追加して、「Is Trigger」のチェックを入れます。

AttackRangeオブジェクトのインスペクターで「Layer」を「Ignore Raycast」に設定します。

砲台設置時は売却ボタンを押せないようにする
砲台情報パネルの下部に砲台の売却ボタンがあります。マップ上に設置した砲台を売却するためのボタンなのですが、砲台設置時には使わないので押せないようにしておきましょう。
SideBarManagerスクリプトを修正します。
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using TMPro; // TextMeshProを使うために必要な宣言
// ここから
using UnityEngine.UI; // UIを扱うために必要な宣言
// ここまで
public class SideBarManager : MonoBehaviour
{
[SerializeField] private GameObject turretInfo; // 砲台情報パネルのオブジェクト
[SerializeField] private TextMeshProUGUI turretNameText; // 砲台の名前のテキスト
[SerializeField] private TextMeshProUGUI buildingCostText; // 建設コストのテキスト
[SerializeField] private TextMeshProUGUI attackPowerText; // 攻撃力のテキスト
[SerializeField] private TextMeshProUGUI attackIntervalText; // 攻撃速度のテキスト
[SerializeField] private TextMeshProUGUI attackRangeText; // 攻撃範囲のテキスト
// ここから
[SerializeField] private Button sellButton; // 売却ボタン
// ここまで
/// <summary>
/// 砲台情報を表示
/// </summary>
// ここから
//public void ShowTurretInfo(TurretSetting.TurretData turretData)
public void ShowTurretInfo(TurretSetting.TurretData turretData, bool sellButtonInteractable)
// ここまで
{
turretNameText.text = turretData.name;
buildingCostText.text = turretData.buildingCost.ToString();
attackPowerText.text = turretData.attackPower.ToString();
attackIntervalText.text = turretData.attackInterval.ToString();
attackRangeText.text = turretData.attackRange.ToString();
// ここから
// 売却ボタンのinteractableを切り替え
sellButton.interactable = sellButtonInteractable;
// ここまで
turretInfo.SetActive(true); // 砲台情報パネルを表示する
}
/// <summary>
/// 砲台情報を非表示
/// </summary>
public void HideTurretInfo()
{
turretInfo.SetActive(false); // 砲台情報パネルを非表示にする
}
}
ShowTurretInfo()
メソッドに第2引数を追加して売却ボタンのinteractable
を変更できるようにしました。
TurretGeneratorスクリプトを修正します。
public class TurretGenerator : MonoBehaviour
{
// ...省略...
/// <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;
// ここから
//sideBarManager.ShowTurretInfo(selectedTurretData); // 砲台情報を表示
sideBarManager.ShowTurretInfo(selectedTurretData, false); // 砲台情報を表示、売却ボタンを押せないように
// ここまで
}
}
ShowTurretInfo()
メソッドの第2引数をfalse
にして、砲台設置時は売却ボタンを押せないようにします。
TurretControllerスクリプトを修正します。
public class TurretController : MonoBehaviour
{
// ...省略...
/// <summary>
/// 砲台がクリックされたときの処理
/// </summary>
public void OnTurretClicked(TurretController turret)
{
SideBarManager sideBarManager = FindObjectOfType<SideBarManager>();
// ここから
//sideBarManager.ShowTurretInfo(turretData);
sideBarManager.ShowTurretInfo(turretData, true);
// ここまで
Debug.Log($"クリックした砲台: {turretData.name}");
}
}
ShowTurretInfo()
メソッドの第2引数をtrue
にして、マップの砲台選択時は売却ボタンを押せるようにします。
SideBarオブジェクトのインスペクターで「Side Bar Manager (Script) > Sell Button」にSellButtonオブジェクトをドラッグ&ドロップしてアサインします。

マップに攻撃範囲を表示
マップ上の砲台を選択したときに攻撃範囲が表示されたらオシャレじゃない?というわけで、Line Rendererを使って線を描画します。
「Assets > Prefabs > Turret」をダブルクリックしてプレハブの編集画面に入ります。
TurretオブジェクトのインスペクターでLine Rendererコンポーネントを追加します。

Line Rendererの設定はスクリプトでやるので、ここでは何もしません。
TurretControllerスクリプトを修正します。
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public class TurretController : MonoBehaviour
{
// ...省略...
// ここから
[SerializeField] private LineRenderer lineRenderer; // 攻撃範囲を描画するLineRenderer
private bool isRangeVisible = false; // 攻撃範囲の表示状態
private Coroutine hideRangeCoroutine; // 攻撃範囲を非表示にするコルーチン
// ここまで
// ここから
private void Awake()
{
// LineRendererの設定
lineRenderer.positionCount = 50;
lineRenderer.startWidth = 0.08f;
lineRenderer.endWidth = 0.08f;
lineRenderer.loop = true;
lineRenderer.material = new Material(Shader.Find("Sprites/Default"));
lineRenderer.startColor = new Color(0, 0, 1, 0.3f);
lineRenderer.endColor = new Color(0, 0, 1, 0.3f);
lineRenderer.sortingLayerName = "Object";
lineRenderer.sortingOrder = 0;
lineRenderer.enabled = false; // 最初は非表示
}
// ここまで
private void Update()
{
// ...省略...
}
private void OnTriggerEnter2D(Collider2D collision)
{
// ...省略...
}
private void OnTriggerExit2D(Collider2D collision)
{
// ...省略...
}
/// <summary>
/// 砲台データを初期化
/// </summary>
public void InitializeTurret(TurretSetting.TurretData data)
{
// ...省略...
}
/// <summary>
/// 砲台に最も近い敵を選択
/// </summary>
private void UpdateTargetEnemy()
{
// ...省略...
}
/// <summary>
/// 攻撃間隔管理
/// </summary>
public IEnumerator ManageAttacks()
{
// ...省略...
}
/// <summary>
/// 攻撃
/// </summary>
private void Attack()
{
// ...省略...
}
/// <summary>
/// 砲身を敵の方向に回転させる
/// </summary>
private void RotateTurretHeadTowardsEnemy()
{
// ...省略...
}
/// <summary>
/// 砲台がクリックされたときの処理
/// </summary>
public void OnTurretClicked(TurretController turret)
{
SideBarManager sideBarManager = FindObjectOfType<SideBarManager>();
sideBarManager.ShowTurretInfo(turretData, true);
Debug.Log($"クリックした砲台: {turretData.name}");
// ここから
// 攻撃範囲の表示切り替え
isRangeVisible = !isRangeVisible;
lineRenderer.enabled = isRangeVisible;
if (isRangeVisible)
{
DrawAttackRange();
// すでにコルーチンが動いていたら一度止める
if (hideRangeCoroutine != null)
{
StopCoroutine(hideRangeCoroutine);
}
// 新しくコルーチンを開始
hideRangeCoroutine = StartCoroutine(HideRangeAfterDelay(4f));
}
// ここまで
}
// ここから
/// <summary>
/// 攻撃範囲を描画
/// </summary>
private void DrawAttackRange()
{
float radius = attackRange.radius; // 攻撃範囲の半径
Vector3 center = transform.position; // 砲台の位置
int points = lineRenderer.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;
lineRenderer.SetPosition(i, new Vector3(x, y, 0));
}
}
/// <summary>
/// 一定時間後に攻撃範囲を非表示にするコルーチン
/// </summary>
private IEnumerator HideRangeAfterDelay(float delay)
{
yield return new WaitForSeconds(delay);
isRangeVisible = false;
lineRenderer.enabled = false;
}
// ここまで
}
マップ上の砲台をクリックするたびに攻撃範囲の表示をオン/オフできるようにしました。さらに、表示がオンの状態が4秒続くと自動的に表示が消えるようにしました。これはコルーチンで実現しています。
DrawAttackRange()
でLine Rendererを使って円を描画しています。三角関数を使ってますね。「サイン、コサイン、タンジェント」という呪文は今でも覚えていますが、意味は忘れました。
「Assets > Prefabs > Turret」をダブルクリックしてプレハブの編集画面に入ります。Turretオブジェクトのインスペクターで「Turret Controller (Script) > Line Renderer」にTurretオブジェクトをドラッグ&ドロップしてアサインします。

動作確認
さあ、動作確認をしましょう。
以下の機能が実装されました。
- サイドバーの砲台アイコンを選択したら、砲台情報を表示。売却ボタンは非アクティブ。
- マップの砲台を選択したら、砲台情報を表示。売却ボタンはアクティブ。攻撃範囲を青い円で表示。
- 攻撃範囲のオン/オフは砲台のクリックで切り替え。
- 攻撃範囲をオフにしなかったら4秒後に自動オフ。
さいごに
マップ上の砲台を選択したときの処理でかなり試行錯誤しました。結局レイキャストを使ったんだけど、もっと簡単な方法がありそう。
でわでわ
コメント