ユニティーでのタワーディフェンスゲームの作り方のメモです。ゲーム制作の参考にしていただければ嬉しいです。トグルボタンをクリックすると記事が表示されます。小さくて見ずらい画像はクリックで拡大できます。
Enemyの作成
CreateEmptyを作成します。
inspector画面のaddcomponentからSpriterenderer、BoxColider2D、rigidbody2Dを追加します。

rigidbody2DのbodyTypeをkinematicに変更します。

BoxColiderのサイズを敵の大きさに合うように調整します。

敵の歩きモーションを作成します。
歩きモーション用のSprite選択してEnemyにドラッグ&ドロップします。

animが作成されるので保存先を選んで保存します。名前はEnemyWalkにしました。
次に敵に攻撃がヒットした時のモーションを作成します。
歩きのモーションをコピーして複製します。名前をEnemyHitに変更しました。

Enemyのanimaterコントローラーをダブルクリックして開きます。


右クリックからEmptyを作成します。

作成したnewstateの名前をEnemyHitに変更します。

EnemyHitのinspector画面のmotionの◎のところをクリックしてEnemyHitを設定します。

Windowメニューanimation→animationを開きます。

HierarchウィンドウからEnemyを選択してタブからEnemyHitを選択

赤いボタンを押す

ダメージを受けたときに赤くしたいのでキーを選択して、

Enemyのinspector画面のspriteの色を変更します。

再生すると点滅しているような感じになります。
animatorに戻ってanystate→EnemyHitにmaketrangitionで矢印を伸ばします

anystateは設定した条件を満たしたときに即座にそのアニメーションに切り替えることができます。
Hitからwalkにもmaketrangitionで矢印をつなぎます。

次に条件を設定していきます
+をしてtriggerで作成名前はHitにしました

anystate矢印を選択してinspector画面のcoundtionでhitを選びます

Hit→walkの矢印をクリックします。

inspector画面のExittimeを1変更してtransitiondurationを0にします

再生してparamのhitにチェックを入れて動作するか確認します。

enemyの子オブジェクトに空のオブジェクトを作成します。
名前はHPBarPosにしました。
このオブジェクトの場所にHPバーを表示していきます。
場所がわかりにくいのでinspector画面の左上のところをクリックします。

色を選択すると場所が表示されてわかりやすくなります。


色が表示されているので敵の頭上に位置を調整します。
UI→canvasを作成します。

名前はHPcanvasにしました。
HpCanvasのinspector画面のrendermodeをworldspaceに変更します。

するとcanvasの表示場所がゲーム内のポジションに切り替わります。
HpCanvasのrectTranceformをリセットしておきます。
widthは0.8Height0.2にしました。

HPcanvasの子オブジェクトにimageを作成します。
赤ゲージにしたいので名前はRedBarにしました。
HPバー用の画像を用意してsourceimageに設定、
カラーを赤に変更しました。

バーのサイズを調整していきます。
recttranceformの左上のマークをクリックします。

Altを押しながら右下のマークをクリックしました。

RedBarの子オブジェクトにimageを作成します。
名前はGreenBarにしました。

RedBarと同じようにサイズ調整します。
GreenBarのinspector画面のimagetypeをFilledに変更します。

FilleMethotをhorizontalに変更します。

FillAmountのスライダーを動かすと緑のバーが減るようになります。
コードでFillAmountの値を変更することでHPの増減を表示できます。
prefabsフォルダを作成してHPcanvasをprefab化しておきます。

Scriptフォルダの中にEnemyフォルダを作成して3つC#スクリプトを作成します。
名前はEnemy、EnemyHP、EnemyHpBarにしました。
EnemyとEnemyHpをEnemyにアタッチします。
prefabのHpCanvasにEnemyHPをアタッチします。
using UnityEngine;
using UnityEngine.UI;
public class EnemyHPBar : MonoBehaviour
{
public Image hpBarImage;
}
using System;
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.UI;
public class EnemyHP : MonoBehaviour
{
//設定用のHP
[SerializeField] private float hp = 10;
//現在のHP
[NonSerialized] public float currentHp;
//HPを表示するUI
[SerializeField] private GameObject hpBar;
//HPバーのポジション
[SerializeField] private Transform barPoz;
private Image hpBarImage;
// Start is called before the first frame update
void Start()
{
CreateHealthBar();
}
private void CreateHealthBar()
{
//生成して親オブジェクトを設定する
GameObject newBar = Instantiate(hpBar , barPoz.position,Quaternion.identity);
newBar.transform.SetParent(transform);
//newBarから変数にHPバーを格納する
EnemyHPBar healthBar = newBar.GetComponent<EnemyHPBar>();
//画像を変数に格納する
hpBarImage = healthBar.hpBarImage;
//HPを更新する
currentHp = hp;
}
// Update is called once per frame
void Update()
{
//クリックしたらダメージを受ける
if (Input.GetMouseButtonDown(0))
{
ReduceHP(5);
}
//HPバーの表示を更新する
hpBarImage.fillAmount = Mathf.Lerp(hpBarImage.fillAmount, currentHp / hp, Time.deltaTime * 10f);
}
/// <summary>
/// HPを減らす
/// </summary>
/// <param name="damage"></param>
public void ReduceHP(float damage)
{
currentHp -= damage;
//死んでいるかの判定
DeathCheck();
}
/// <summary>
/// HPが0になっているかのチェック
/// </summary>
private void DeathCheck()
{
if(currentHp <= 0)
{
currentHp= 0;
//非表示にする
gameObject.SetActive(false);
}
}
}
エディタに戻りEnemyHPの変数を設定します。

prefabのHpCanvasを開いて変数を設定します。

再生してクリックすると敵がダメージを受けて緑のゲージが減るようになっています。
HPが0になると消滅します。
武器の作成
まず空のオブジェクトを作成します。
名前はFireWeaponにしました。
一定の距離に近づいた敵を攻撃する機能を付けていきます。
攻撃範囲を指定するためにCircleColider2dを追加してisTriggerにチェックを入れます。

FireWeaponの子オブジェクトに2つCreateEmptyを作成します。
名前はplatform、rotatePoint、にしました。
transformをリセットします。

platformには画像を設定したいのでSpriterendererを追加しておきます。

用意した画像をSpriteに設定します

rotatePointの子オブジェクトに2つ空のオブジェクトを作成します。
名前はWeaponImageとBulletposにしました。

WeaponImageもこのオブジェクトにもSpriterendererを追加して用意した画像をSpriteに設定します。

表示する順番を設定したいのでSortingLayerを追加します。
SortingLayerのdefaltのところをクリックしてAddSortingLayerをクリックします。

+ボタンをクリックします。

Node,Enemy、Weapon,Bullet,UIを作成します。

SortingLayerは0が一番下に表示されて数値が大きくなるほど上に表示されます。
platformとWeaponImageのレイヤーをweaponに変更します。

WeaponImageのweaponOrderinLayerを1にします。

BurretPosのinspector画面の左上のとこから位置を表示できるようにして位置を調整します。

Scriptsフォルダに新しくWeaponフォルダを作成してその中にC#スクリプトを作成します。
名前はWeaponにしました。
作成したらFireWeaponにアタッチしておきます。

using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public class Weapon : MonoBehaviour
{
//武器の攻撃範囲
public float attackRange = 3f;
void Start()
{
//プレイ開始時に数値を合わせる
GetComponent<CircleCollider2D>().radius= attackRange;
}
void Update()
{
}
private void OnDrawGizmos()
{
//ゲームプレイ中以外も攻撃範囲を表示させる
Gizmos.DrawWireSphere(transform.position, attackRange);
}
private void OnTriggerEnter2D(Collider2D collision)
{
//攻撃範囲に敵が入った時の処理
if (collision.CompareTag("Enemy"))
{
Debug.Log("攻撃範囲に入りました");
}
}
private void OnTriggerExit2D(Collider2D collision)
{
//攻撃範囲から敵が出たときの処理
if (collision.CompareTag("Enemy"))
{
Debug.Log("攻撃範囲から出ました");
}
}
}
再生してEnemyを手動で動かして攻撃範囲内に入れるとログが出ます。

空のオブジェクトを一つ作成します。
名前はBulletにしました。tranceformをリセットしておきます。

C#スクリプトを作成します。名前はBulletにしました。

作成したスクリプトをBulletオブジェクトにアタッチしておきます。

using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public class Enemy : MonoBehaviour
{
public EnemyHP enemyHP;
void Start()
{
enemyHP= GetComponent<EnemyHP>();
}
void Update()
{
}
}
using System;
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public class Bullet : MonoBehaviour
{
//弾の移動速度
[SerializeField] private float moveSpeed = 10f;
//ダメージ発生距離
[SerializeField] private float damageDidtsnce = 0.1f;
//ターゲットを格納する
private Enemy enemyTarget;
//ダメージ
private float damage;
private void Start()
{
// 動作確認用のコード 不要になったら消去
enemyTarget= FindObjectOfType<Enemy>();
}
void Update()
{
if (enemyTarget != null)
{
//弾を動かす
moveBullet();
}
}
private void moveBullet()
{
//現在地から目的地まで一定の速度で移動する
transform.position = Vector2.MoveTowards
(transform.position,enemyTarget.transform.position,moveSpeed *Time.deltaTime);
//弾と敵の距離を確認する
CheckDistance();
}
/// <summary>
/// 弾と敵の距離を確認して近ければダメージを与える
/// </summary>
private void CheckDistance()
{
//敵との距離
float distanceToTarget = (enemyTarget.transform.position - transform.position).magnitude;
//十分近づいたら
if (distanceToTarget < damageDidtsnce)
{
//ダメージを与える
enemyTarget.enemyHP.ReduceHP(damage);
}
}
}
unityを再生すると弾が敵に向かって飛んでいきます。
確認出来たらstart関数の動作確認用コードは消してしまってOKです。
WeaponとBulletを管理するC#スクリプトを作成します。
名前はweaponControlにしました。
作成したスクリプトをFireWeaponにアタッチしておきます。

using System;
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public class WeaponControl : MonoBehaviour
{
//弾を生成する位置
[SerializeField] private Transform bulletSpawnPos;
//攻撃準備中の攻撃用の弾
private Bullet currentBullet;
//武器を格納
private Weapon weapon;
//設定用の弾ダメージ
[SerializeField] private float damage = 2f;
//アップグレードするときにこの変数を変更する
[NonSerialized] public float bulletDamage;
//生成する弾を格納する
public GameObject fireBullet;
private void Start()
{
//Weapon変数に格納
weapon= GetComponent<Weapon>();
//設定用の変数の数値を格納する
bulletDamage = damage;
//弾をセットする
ReloadBullet();
}
private void Update()
{
if (!currentBullet)
{
ReloadBullet();
}
}
private void ReloadBullet()
{
//弾を生成してポジションと親を設定
GameObject newBullet = Instantiate(fireBullet);
newBullet.transform.localPosition = bulletSpawnPos.position;
newBullet.transform.SetParent(bulletSpawnPos);
//コンポーネントを格納してダメージなどの初期設定をする
currentBullet= newBullet.GetComponent<Bullet>();
}
}
FireWeaponのinspector画面からBurretPosをBulletSpawnPosにアタッチ、
fireBulletにprefab化した弾をアタッチします。

unityを再生して弾が生成されればOKです。
生成した弾が範囲内の敵に向かって飛んでいくようにします。
Weaponスクリプトを開いてコードを追加します。OnTriggerのDebug.logは消してしまってOKです。
//エネミーを格納するリスト
private List<Enemy> enemies;
//攻撃範囲内にいるターゲットを一体格納する
[NonSerialized] public Enemy currentEnemyTarget;
void Start()
{
enemies= new List<Enemy>();
}
void Update()
{
//ターゲットを取得する
GetCurrentTarget();
}
private void GetCurrentTarget()
{
//リストに敵がいないかどうか
if (enemies.Count <= 0)
{
//設定をnullに
currentEnemyTarget=null;
return;
}
//リストから設定
currentEnemyTarget = enemies[0];
}
private void OnTriggerEnter2D(Collider2D collision)
{
//攻撃範囲に敵が入った時の処理
if (collision.CompareTag("Enemy"))
{
//リストに格納する
Enemy enemy = collision.GetComponent<Enemy>();
enemies.Add(enemy);
}
}
private void OnTriggerExit2D(Collider2D collision)
{
//攻撃範囲から敵が出たときの処理
if (collision.CompareTag("Enemy"))
{
Enemy enemy = collision.GetComponent<Enemy>();
//リストの中に引数の要素があるか判定
if (enemies.Contains(enemy))
{
//いるならリストから消去
enemies.Remove(enemy);
}
}
}
次にBulletのスクリプトを開いてコードを追加します。startの動作確認用のコードは削除します。
/// <summary>
/// 攻撃対象を設定する
/// </summary>
/// <param name="enemy"></param>
public void SetTargetEnemy(Enemy enemy)
{
enemyTarget = enemy;
}
WeaponControlのupdate内のif文にelseを追加します。
else
{
currentBullet.SetTargetEnemy(weapon.currentEnemyTarget);
}
エディタに戻って弾が範囲内の敵のところまで移動するか確認します。

弾が敵に当たったらダメージを与えて消えるようにします。
Bulletスクリプトを開いてコードを追加します。
//WeaponControlを管理するコンポーネント
private WeaponControl bulletControl;
//CheckDistanceのif文に追加する
//弾の設定を初期化
bulletControl.ResetBullet();
//当たった弾は消去
Destroy(gameObject);
public void BulletInitialization(WeaponControl weaponControl, float damage)
{
//引数を変数に格納する
bulletControl = weaponControl;
this.damage = damage;
}
}
次にWeaponControlのスクリプトにコードを追加します。
//updateのelseの中に書く
//親の設定を消す
currentBullet.transform.parent = null;
//ReloadBulletの中に書く
//初期設定
currentBullet.BulletInitialization(this, bulletDamage);
/// <summary>
/// 装填中の弾の設定を消す
/// </summary>
/// <exception cref="NotImplementedException"></exception>
internal void ResetBullet()
{
currentBullet = null;
}
エディタに戻り弾が当たってダメージを与えて消滅するのを確認します。
弾の発射スピードが速いので調整できるようにします。WeaponControlにコードを追加します。
//発射する間隔
[SerializeField] private float firingInterval = 2f;
private float nextFireTime;
[NonSerialized] public float delay;
//スタート関数bulletDamageの下に書き足す
delay= firingInterval;
//update関数のelseを消去して書き足す
if (Time.time > nextFireTime)
{
if (Shootable())
{
//親の設定を消す
currentBullet.transform.parent = null;
//弾の攻撃対象を設定
currentBullet.SetTargetEnemy(weapon.currentEnemyTarget);
}
//次の攻撃までの時間を設定
nextFireTime = Time.time + delay;
}
private bool Shootable()
{
return weapon.currentEnemyTarget != null && currentBullet != null
&& weapon.currentEnemyTarget.enemyHP.currentHp > 0f;
}
エディタに戻り一定間隔で弾が発射されるかチェックします。
敵の移動
新しくフォルダを作成して。C#スクリプトを作成します。
フォルダ名はSystem、スクリプト名はMovepointにしました。

CreateEmptyでMovepointオブジェクトを作成します。
作成したMovepointオブジェクトにMovepointスクリプトをアタッチします

using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public class Movepoint : MonoBehaviour
{
//移動ポイントの配列
public Vector3[] points;
}
配列を作成してエディタに戻ります。
inspector画面から4つポイントを設定します。

4つのポイントをエディタ上で確認できるようにします。
//エディタ上で見えるようにする
private void OnDrawGizmos()
{
//for文で配列の数値分処理する
for (int i = 0; i < points.Length; i++)
{
//色を指定
Gizmos.color = Color.yellow;
//ポジション、半径
Gizmos.DrawWireSphere(points[i], 0.5f);
}
}
/// <summary>
/// 引数で渡された数値と同じポイントの位置を返す
/// </summary>
/// <param name="index"></param>
/// <returns></returns>
public Vector3 GetMovePointPosition(int index)
{
//敵の移動に必要
return points[index];
}
エディタに戻りポイントが表示されているか確認します。

このポイントに移動用のハンドルを付けてマウスで移動、CTRL+Zで戻す、番号表示、できるようにしていきます。
Systemフォルダの中に新しくEditorフォルダを作成します

Editorフォルダの中に新しくスクリプトを作成します。
名前はMovePointEditerにしました。

using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEditor;
[CustomEditor(typeof(Movepoint))]
public class MovePointEditer : Editor
{
//変数に格納する
Movepoint movepoint => target as Movepoint;
private void OnSceneGUI()
{
//色を指定
Handles.color = Color.yellow;
for (int i = 0; i < movepoint.points.Length; i++)
{
//EndChangeCheckとの間でシーン内での変化がないのか確認する
EditorGUI.BeginChangeCheck();
//ポイントの位置を取得
Vector3 currentWaypoint= movepoint.points[i];
//ハンドルを生成して変数に格納する
Vector3 newWaypoint = Handles.FreeMoveHandle
(currentWaypoint, Quaternion.identity, 0.7f, new Vector3(0.3f, 0.3f, 0.3f), Handles.SphereHandleCap);
//ハンドル番号の生成
GUIStyle textStyle= new GUIStyle();
textStyle.fontSize= 18;
textStyle.normal.textColor = Color.white;
Vector3 textPos = Vector3.down * 0.35f + Vector3.right * 0.35f;
//ラベルの発生位置、表示内容、GUIの設定
Handles.Label(movepoint.points[i] + textPos, $"{i + 1}", textStyle);
EditorGUI.EndChangeCheck();
if (EditorGUI.EndChangeCheck())
{
//位置の保存CTRL+Zで戻せるようにする
Undo.RecordObject(target, "Move");
//動かしたハンドル位置に変更する
movepoint.points[i] = newWaypoint;
}
}
}
}
エディタに戻りマウスでポイント移動CTRL+Zで戻すことができるかチェックします。

Enemyスクリプトを開いてコードを書き足します。
using System;
//インスペクターに表示する必要が無くなったのでNonSerializedにしておく
[NonSerialized]public EnemyHP enemyHP;
//移動速度設定用
[SerializeField] private float setMoveSpeed = 3f;
//移動速度
private float moveSpeed;
[NonSerialized] public Movepoint movepoint;
//向かうポイントの番号
private int currentMovepointIndex;
//CurrentPointPositionが行われるたびに後半の処理が行われる
public Vector3 CurrentPointPosition => movepoint.GetMovePointPosition(currentMovepointIndex);
void Start()
{
//向かうポイントを設定
currentMovepointIndex= 0;
//移動速度の設定
SetMoveSpeed();
//指定のコンポーネントがついているオブジェクトを探して格納する
movepoint = FindObjectOfType<Movepoint>().GetComponent<Movepoint>();
}
private void SetMoveSpeed()
{
moveSpeed = setMoveSpeed;
}
void Update()
{
Move();
}
private void Move()
{
//ポジションを移動
transform.position = Vector3.MoveTowards(transform.position,CurrentPointPosition,moveSpeed * Time.deltaTime);
}
Unityを再生してポイント1に向かって敵が動き出せばOKです
Enemyスクリプトにコードを追加します。
void Update()
{
//現在の目的のポイントに到達しているかの判定
if (NextPointReached())
{
//次の目的ポイントに更新する
UpdatePointIndex();
}
}
private void UpdatePointIndex()
{
//最終地点でなければ目標ポイントを更新する
if (currentMovepointIndex < movepoint.points.Length - 1)
{
currentMovepointIndex++;
}
}
private bool NextPointReached()
{
//次のポイントまでの残りの距離を変数に格納
float distance = (transform.position - CurrentPointPosition).magnitude;
//判定
if (distance < 0.1)
{
return true;
}
return false;
}
Unityを再生して最終ポイントまで敵が動き出せばOKです。
敵のアニメージョンの切り替え
敵が攻撃を受けたときのアニメーションの実装です。
Enemyフォルダに新しくC#スクリプトを作成します。
名前はEnemyAnimationsにしました。
作成したらEnemyオブジェクトにアタッチしておきます。
EnemyAnimationsにコードを書く前にEnemyスクリプトとEnemyHPにコードを追加します。
//アニメーションを再生した時に動きを止めたい
/// <summary>
/// 移動速度を0にする
/// </summary>
public void StopMovement()
{
moveSpeed= 0f;
}
//被弾した時
public static Action<Enemy> OnEnemyHit;
//Enemyを格納する
private Enemy enemy;
//Start関数に書く
//コンポーネント格納
enemy= GetComponent<Enemy>();
//ReduceHPのDeathCheckの前に書く
//被弾アニメーション
OnEnemyHit?.Invoke(enemy);
using System;
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public class EnemyAnimations : MonoBehaviour
{
private Animator animator;
private Enemy enemy;
// Start is called before the first frame update
void Start()
{
animator = GetComponent<Animator>();
enemy = GetComponent<Enemy>();
}
// Update is called once per frame
void Update()
{
}
private void PlayHitAnimation()
{
animator.SetTrigger("hit");
}
/// <summary>
/// 移動を止めてアニメーションを再生
/// </summary>
/// <param name="enemy"></param>
private void EnemyHit(Enemy enemy)
{
if (this.enemy == enemy)
{
StartCoroutine(PlayHurt());
}
}
private IEnumerator PlayHurt()
{
//移動スピードを0にする
enemy.StopMovement();
//アニメーションの再生
PlayHitAnimation();
//再生待機中
yield return new WaitForSeconds(StopTime());
//移動再開
enemy.SetMoveSpeed();
}
private float StopTime()
{
//アニメーションの長さ+調整数値を返す
return animator.GetCurrentAnimatorStateInfo(0).length + 0.3f;
}
//イベントに関数を登録&解除
private void OnEnable()
{
EnemyHP.OnEnemyHit += EnemyHit;
}
private void OnDisable()
{
EnemyHP.OnEnemyHit -= EnemyHit;
}
}
//関数がprivateになっていてエラーになっているものはpublicにする
再生してアニメーション動作するかチェックします。
攻撃間隔が長い場合WaeponControlのFiringIntervalを短くします。
敵のHPが0になった瞬間に敵が消滅してしまうのでアニメーションを再生してから消滅するように修正します。
//animationsを扱うための変数
private EnemyAnimations enemyAnimations;
//スタート関連に書く
enemyAnimations= GetComponent<EnemyAnimations>();
//DeathCheckのif文の中に書く
//非表示にするは消す
//StopTimeがprivateだったらpublicにする
//被弾アニメーションを再生してから消滅させる
Invoke("Die", enemyAnimations.StopTime());
private void Die()
{
ResetHealth();
}
private void ResetHealth()
{
currentHp = hp;
hpBarImage.fillAmount = 1f;
gameObject.SetActive(false);
}
マップの作成
オブジェクトプールの作成
作成したスクリプトをMovepointにアタッチしておきます。
Movepointの名前をWaypoint_and_Spawnerに変更しました。
using System;
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public class ObjectPooler : MonoBehaviour
{
//生成するアイテム
[SerializeField] private GameObject poolObject;
//生成する数
[SerializeField] private int poolSize = 10;
//生成したオブジェクトを管理するリスト
private List<GameObject> pool;
//親オブジェクトを格納しておく
private GameObject poolContainer;
private void Awake()
{
//インスタンス化
pool= new List<GameObject>();
//オブジェクトを生成して名前を付けて変数に格納
poolContainer = new GameObject($"Pool - {poolObject.name}");
//プールオブジェクトの作成
CreatePooler();
}
/// <summary>
/// poolSizeの数poolObjectを生成する
/// </summary>
private void CreatePooler()
{
//poolsizeの分ループ
for (int i = 0; i < poolSize; i++)
{
//生成したオブジェクトをリストに格納
pool.Add(CreateObject());
}
}
/// <summary>
/// poolobjectを生成して呼んだ場所に返す
/// </summary>
/// <returns></returns>
private GameObject CreateObject()
{
//オブジェクトを作成して変数に格納する
GameObject newInstance = Instantiate(poolObject);
//親の設定
newInstance.transform.SetParent(poolContainer.transform);
//非表示
newInstance.SetActive(false);
//返す
return newInstance;
}
/// <summary>
/// プールからオブジェクトを引き出す
/// </summary>
/// <returns></returns>
public GameObject GetObjectFromPool()
{
//リストに格納されている分ループする
for (int i = 0; i < pool.Count; i++)
{
//もしプール内の非表示オブジェクトだったら
if (!pool[i].activeInHierarchy)
{
//呼び出した場所に返す
return pool[i];
}
}
//足りなければ生成して返す
return CreateObject();
}
/// <summary>
/// プールにオブジェクトを返却
/// </summary>
/// <param name="instance"></param>
public static void ReturnToPool(GameObject instance)
{
instance.SetActive(false);
}
}
Enemyをprefab化します。本体は一旦非表示にしておきます。
Waypoint_and_Spawnerのinspector画面のpoolObjectにprefab化したenemyをアタッチします。

再生してPoolEnemyの子オブジェクトにEnemyが生成されているかチェックします。

FireWeaponにObjectPoolerスクリプトをアタッチします。
PoolObjectにBulletを設定します。

WeaponControlスクリプトとBulletスクリプトにコードを追加します。
//ObjectPoolerを格納するための変数
private ObjectPooler pooler;
//スタート関数に書く
pooler= GetComponent<ObjectPooler>();
//ReloadBulletに書く
GameObject newBullet = pooler.GetObjectFromPool();//GameObject newBullet = Instantiate(fireBullet);を消して書き直す
//表示
newBullet.SetActive(true);
//CheckDistanceのif文に書く
//プールに戻す
ObjectPooler.ReturnToPool(gameObject);//destroyは消す
//BulletInitializationに書く
ResetBullet();
private void ResetBullet()
{
enemyTarget = null;
transform.localRotation= Quaternion.identity;
}
Bulletが機能するかチェックします。
敵をスポーンさせる
SystemフォルダにC#スクリプトを作成します。名前はSpawnerにしました。
作成したらWaypoint_and_Spawnerにアタッチしておきます。
using System;
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
//スポーンモード
public enum Spawnmodes
{
constant,//一定間隔でスポーンする
Random,//ランダムにスポーンする
}
public class Spawner : MonoBehaviour
{
//スポーンモードの選択
[SerializeField] private Spawnmodes spawnModes = Spawnmodes.constant;
//最短のスポーン間隔
[SerializeField] private float minRandomDelay;
//最長のスポーン間隔
[SerializeField]private float maxRandomDelay;
//一定モードのスポーン時間
[SerializeField] private float constantSpawnTime;
//スポーンさせる数を設定する
[SerializeField] private int enemyCount = 10;
//タイマー変数
private float spawnTimer;
//スポーンさせた数(数を追加していく)
private float spawned;
//enemyのオブジェクトプール用
private ObjectPooler pooler;
//MovePoint格納用 Enemyに渡すためにここに格納する
private Movepoint movePoint;
private void Start()
{
//変数にコンポーネントを格納する
pooler = GetComponent<ObjectPooler>();
movePoint= GetComponent<Movepoint>();
}
private void Update()
{
//spawnタイマーの時間を減らす
spawnTimer -= Time.deltaTime;
//確認
if (spawnTimer < 0)
{
spawnTimer = GetSpawnDelay();
//スポーン上限の確認
if (spawned < enemyCount)
{
//生成済みの数を追加
spawned++;
//敵を生成
SpawnEnemy();
}
}
}
/// <summary>
/// 敵を生成する
/// </summary>
/// <exception cref="NotImplementedException"></exception>
private void SpawnEnemy()
{
//プールから取得して変数に格納
GameObject newInstance = pooler.GetObjectFromPool();
//エネミーの初期設定
SetEnemy(newInstance);
//表示する
newInstance.SetActive(true);
}
private void SetEnemy(GameObject newInstance)
{
//enemyでMovepointを使えるように格納している
Enemy enemy = newInstance.GetComponent<Enemy>();
enemy.movepoint = movePoint;
//生成位置をこのオブジェクトの位置に設定
enemy.transform.position = transform.position;
//スピードの設定
enemy.SetMoveSpeed();
}
/// <summary>
/// 一定もしくはランダムの数値を返す
/// </summary>
/// <returns></returns>
private float GetSpawnDelay()
{
if (spawnModes == Spawnmodes.constant)
{
return constantSpawnTime;
}
else
{
//引数の間からランダムな数値を選んで返す
return UnityEngine.Random.Range(minRandomDelay, maxRandomDelay);
}
}
}
こんな感じでスポーンするようになります。
最後のポイントに到着したEnemyを非表示にしていきます。
Enemyスクリプトを開きます。
//後々使う
//ゴールに到着した時のイベント
public static Action OnReachedGoal;
//UpdatePointIndexのif文の後に追加する
else
{
//最後ならプールに戻す
ReachedGoal();
}
/// <summary>
/// プールにこのオブジェクトを返す
/// </summary>
/// <exception cref="NotImplementedException"></exception>
private void ReachedGoal()
{
//アクションを呼ぶ
OnReachedGoal?.Invoke();
//体力の初期化関数を呼ぶ
enemyHP.ResetHealth();
//プールにオブジェクトを戻す
ObjectPooler.ReturnToPool(gameObject);
}
public void ResetMovePoint()
{
currentMovepointIndex= 0;
}
EnemyHPスクリプトを開きます。
//死んだときの処理
public static Action OnEnemyDead;
//Die関数に追加する
//アクションを呼ぶ
OnEnemyDead?.Invoke();
//プールに返す
ObjectPooler.ReturnToPool(gameObject);
一体目のEnemy最終地点にたどり着くと残りのエネミーが最終地点に直線移動してしまうのでSpawnerにコードを追加します。
//SetEnemyのEnemy enemy = newInstance.GetComponent<Enemy>();enemy.movepoint = movePoint;のしたに書く
{
//リセット
enemy.ResetMovePoint();
ウェーブ機能
Systemフォルダに新しくC#スクリプトを作成します。
名前はLevelManagerにしました。

LevelManagerというオブジェクトを作成してLevelManagerスクリプトをアタッチします。
Transformの値をリセットしておきます。

using System;
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public class LevelManager : MonoBehaviour
{
[NonSerialized] public int currentWave;
//シングルトンにする
public static LevelManager instance;
private void Awake()
{
if (instance == null)
{
instance= this;
}
else if (instance != this)
{
Destroy(gameObject);
}
}
// Start is called before the first frame update
void Start()
{
//現在のウェーブを設定する
currentWave= 1;
}
private void WaveCompleted()
{
currentWave++;
}
private void OnEnable()
{
Spawner.OnWaveCompleted += WaveCompleted;
}
private void OnDisable()
{
Spawner.OnWaveCompleted -= WaveCompleted;
}
}
Spawnerスクリプトを開いてコードを追加します。
//生成できる敵の数
private int enemiesRemaining;
//ウェーブの遅延
[SerializeField] private float waveDelayTime = 1f;
//ウェーブ達成時に呼ばれるアクション
public static Action OnWaveCompleted;
//Start関数に書く
//生成できる数を設定する
enemiesRemaining = enemyCount;
/// <summary>
/// ゴールやデスで消えたエネミーを記録する
/// </summary>
private void RecordEnemy()
{
//このウェーブで生存している敵の数
enemiesRemaining--;
//このウェーブの完了条件を確認する
CurrentWeaveCheck();
}
/// <summary>
/// ウェーブの完了条件を確認して、イベントとコルーチンを呼ぶ
/// </summary>
/// <exception cref="NotImplementedException"></exception>
private void CurrentWeaveCheck()
{
if (enemiesRemaining <= 0)
{
//ウェーブ完了時の処理を呼び出す
OnWaveCompleted?.Invoke();
//次のウェーブの準備
StartCoroutine(NextWave());
}
}
/// <summary>
/// 次のウェーブに向けて数値関係をリセット
/// </summary>
/// <returns></returns>
private IEnumerator NextWave()
{
//数値の分待機する
yield return new WaitForSeconds(waveDelayTime);
//生成する敵の数をセット
enemiesRemaining = enemyCount;
//次のスポーンまでの時間
spawnTimer = 0f;
//生成した後
spawned = 0;
}
private void OnEnable()
{
//イベントに関数を登録
Enemy.OnReachedGoal += RecordEnemy;
EnemyHP.OnEnemyDead += RecordEnemy;
}
private void OnDisable()
{
//イベントから関数を消去
Enemy.OnReachedGoal -= RecordEnemy;
EnemyHP.OnEnemyDead -= RecordEnemy;
}
Waypoint_and_Spawnerのinspector画面からWaveDeleyTime次のウェーブまでの時間を設定します。
拠点の体力
拠点の体力を実装して敵が10体ゴールにたどり着くとゲームオーバーになるようにしていきます。
//実際に値を変更させる体力変数
[NonSerialized] public int currentLife;
//体力の初期設定だけに使う数値
[SerializeField] private int life = 10;
//Start関数に書く
//体力を設定
currentLife = life;
//onEnebleに書く
Enemy.OnReachedGoal += ReduceLifes;
//onDisableに書く
Enemy.OnReachedGoal += ReduceLifes;
/// <summary>
/// ライフを減らして体力を確認する
/// </summary>
private void ReduceLifes()
{
currentLife--;
//体力が0になっていないか
if (currentLife <= 0)
{
currentLife = 0;
//ゲームオーバー
Debug.Log("ゲームオーバー");
}
}
体力が0になるとデバックログでゲームオーバーと表示されます。
おすすめのアセット
Unityでタワーディフェンスゲームを作りたいの方におすすめのアセットを紹介します。「Tower Defense Toolkit 4 (TDTK-4)」は、基本的なゲームメカニズムやエディターを提供し、初心者でも簡単に自由にカスタマイズ可能なタワーディフェンスゲームを作成できます。このツールキットには、敵、ウェーブなどの管理システムが含まれており、直感的な操作でゲーム開発を楽しむことができます。Unity Asset Storeで入手可能です。