Unity初心者が2Dタワーディフェンスを制作しています。今回は敵を自動生成する処理と複数の移動経路を実装していきます。
- Mac mini (M1, 2020)
- Unity 2022.3.36f1
敵の自動生成
敵オブジェクトを自動生成する処理をスクリプトに書いていきます。このとき、スクリプトを管理と生成に分けます。役割分担ですね。
敵を生成するスクリプト
「Assets/Scripts」フォルダで右クリック「Create > C# Script」を選択してC#スクリプトを作成し、名前をEnemySpawnerにします。これが敵を生成するためのスクリプトです。
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public class EnemySpawner : MonoBehaviour
{
[SerializeField]
private EnemyController enemyPrefab; // 敵のプレハブ
[SerializeField]
private PathData pathData; // 移動経路情報
[SerializeField]
private GameManager gameManager;
/// <summary>
/// 敵の生成管理
/// </summary>
/// <returns></returns>
public IEnumerator ManageSpawning()
{
int timer = 0; // タイマーの初期化
while (gameManager.isSpawning) // 敵を生成可能ならば
{
timer++; // タイマー加算
if (timer > gameManager.spawnInterval) // タイマーが敵生成間隔を超えたら
{
timer = 0; // タイマーリセット
Spawn(); // 敵生成
gameManager.AddEnemyToList(); // 敵の情報をListに追加
gameManager.CheckSpawnLimit(); // 最大生成数を超えたら敵の生成停止
}
yield return null;
}
}
/// <summary>
/// 敵の生成
/// </summary>
public void Spawn()
{
// 経路のスタート地点にプレハブから敵を生成
EnemyController enemyController = Instantiate(enemyPrefab, pathData.positionStart.position, Quaternion.identity);
}
}
ManageSpawning()
は敵を一定間隔で生成するコルーチンです。
timer
を毎フレーム増加させる。timer
がspawnInterval
を超えると敵を生成 (Spawn()
を呼び出し)。- 生成後、
GameManager
に敵情報を追加 (AddEnemyToList
)。 - 最大生成数に達したかを確認 (
CheckSpawnLimit
)。 - 毎フレーム
yield return null
で次フレームに処理を進める。
ヒエラルキーにEnemySpawnerオブジェクトを作成してEnemySpawnerスクリプトをアタッチします。
EnemySpawnerオブジェクトのインスペクターで「Enemy Spawner (Script)」の各変数に値を設定します。
- Enemy Prefabに「Assets/Prefabs」のEnemyオブジェクトをドラッグ&ドロップしてアサインする。
- PathDataに「Assets/Prefabs」のPathオブジェクトをドラッグ&ドロップしてアサインする。
- Game ManagerにヒエラルキーのGameManagerオブジェクトをドラッグ&ドロップしてアサインする。
敵を管理するスクリプト
ゲーム全体を管理するためのスクリプトGameManagerはすでに作成されているので、これにコードを追加します。
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public class GameManager : MonoBehaviour
{
[SerializeField]
private int targetFrameRate = 60; // フレームレートの目標値
// ここから 変数を追加
[SerializeField]
private EnemySpawner enemySpawner;
public bool isSpawning; // 敵を生成するかどうかを制御するフラグ
public int spawnInterval; // 敵を生成する間隔(単位はフレーム)
public int spawnedEnemyCount; // これまでに生成された敵の数
public int maxSpawnCount; // 敵の最大生成数
// ここまで
void Awake()
{
FixFrameRate(); // フレームレートを固定
}
// ここから Startメソッドを追加
void Start()
{
isSpawning = true; // 敵を生成可能にする
StartCoroutine(enemySpawner.ManageSpawning());
}
//ここまで
/// <summary>
/// フレームレートを固定
/// </summary>
private void FixFrameRate()
{
QualitySettings.vSyncCount = 0; // V-Sync(垂直同期)を無効化
Application.targetFrameRate = targetFrameRate; // アプリケーションのフレームレートを設定
}
// ここから メソッドを2つ追加
/// <summary>
/// 敵の情報をListに追加
/// </summary>
public void AddEnemyToList()
{
spawnedEnemyCount++; // 生成した敵の数を増やす
}
/// <summary>
/// 敵の生成が上限に達したかを確認
/// </summary>
public void CheckSpawnLimit()
{
if (spawnedEnemyCount >= maxSpawnCount) // 敵の最大生成数を超えたら
{
isSpawning = false; // 敵を生成不可にする
}
}
// ここまで
}
Start()
メソッドはゲーム開始時に実行されます。
isSpawning = true
で敵の生成を許可。ManageSpawning
コルーチンをStartCoroutine
で実行し、敵を一定間隔で生成。
AddEnemyToList()
は生成された敵をListに登録するメソッドです。が、まだListに登録する処理は実装していなくて、生成した敵の数を数える処理だけが実装されています。
CheckSpawnLimit()
は生成された敵の数が上限を超えたかをチェックし、生成を停止します。
GameManagerオブジェクトのインスペクターで「Game Manager (Script)」の各変数に値を設定します。
- Enemy Spawnerに「Assets/Prefabs」のEnemySpawnerオブジェクトをドラッグ&ドロップしてアサインする。
- Is Spawningはスクリプトで制御するので設定不要。
- Spawn Intervalを100に設定。単位はフレーム。
- Spawned Enemy Countはスクリプトで制御するので設定不要。
- Max Spawn Countを5に設定。
動作確認
ゲームを実行して動作確認をします。
100フレーム(約1.3秒)おきに5体の敵が自動生成されました。いえい。
しかしながら!敵の顔が逆方向を向いていますね。進行方向の判定がうまくいっていないようです。EnemyControllerスクリプトのChangeWalkingAnimation()
メソッドを修正します。
/// <summary>
/// 敵の進行方向を取得してアニメを変更
/// </summary>
private void ChangeWalkingAnimation(int index)
{
// 次の移動先がない場合は処理を終了
if (index >= path.Length - 1)
{
return;
}
// 移動先の方向を計算
// ここから 修正
//Vector2 direction = (path[index] - transform.position).normalized;
Vector2 direction = (path[index + 1] - path[index]).normalized;
// ここまで
// XとY方向をアニメーターに設定
animator.SetFloat("X", Mathf.Round(direction.x));
animator.SetFloat("Y", Mathf.Round(direction.y));
}
あたらめて動作確認します。
敵たちが前を向くようになりました。
複数の移動経路
敵がいつも同じ道を通ってくるとは限りません。ちゅーわけで、複数の移動経路を作ります。
新しいPathオブジェクトのプレハブを作成
「Assets/Prefabs」のPathオブジェクトをヒエラルキーにドラッグ&ドロップしてPathオブジェクトを作成します。
ヒエラルキーのPathオブジェクトを右クリックして「Prefab > Unpack Completely」を選択します。これで、このオブジェクトはプレハブとの紐付けを解除されます。
Pathオブジェクトの配下に新しいPosition*オブジェクトを作成したり、インスペクターの「Path Data > Path Array」を修正したりして、新しい経路を作ります。
完成したPathオブジェクトを「Assets/Prefabs」にドラッグ&ドロップしてプレハブ化します。
プレハブには自動的に名前がつきますが、わかりやすい名前に変更しましょう。私は「Path_1-1」「Path_1-2」のようにしました。ステージ1の1つ目、ステージ1の2つ目…みたいな感じです。
ヒエラルキーのPathオブジェクトは用済みなので削除します。
EnemySpawnerスクリプトの修正
EnemySpawnerスクリプトを修正します。
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public class EnemySpawner : MonoBehaviour
{
[SerializeField]
private EnemyController enemyPrefab; // 敵のプレハブ
// ここから 変数を配列に変更
[SerializeField]
//private PathData pathData; // 移動経路情報
private PathData[] pathDataArray; // 移動経路情報の配列
// ここまで
[SerializeField]
private GameManager gameManager;
/// <summary>
/// 敵の生成管理
/// </summary>
/// <returns></returns>
public IEnumerator ManageSpawning()
{
// ...省略...
}
/// <summary>
/// 敵の生成
/// </summary>
public void Spawn()
{
// ここから 修正
// ランダムな経路を選択
PathData selectedPath = pathDataArray[Random.Range(0, pathDataArray.Length)];
// 経路のスタート地点にプレハブから敵を生成
//EnemyController enemyController = Instantiate(enemyPrefab, pathData.positionStart.position, Quaternion.identity);
EnemyController enemyController = Instantiate(enemyPrefab, selectedPath.positionStart.position, Quaternion.identity);
// 経路情報を初期化
enemyController.InitializePath(selectedPath);
// ここまで
}
}
12行目、移動経路を代入する変数を配列にして複数の経路を代入できるようにします。
33行目、複数の経路からランダムに1つ選択してselectedPath
に代入しています。これに合わせて、36行目の敵を生成する処理も修正しています。
38行目、経路情報を初期化します。このスクリプト(EnemySpawner)は敵を生成するためのスクリプトで、敵を経路に沿って移動させるのはEnemyControllerの役割です。なので、EnemyControllerのInitializePath()
に経路情報を渡して、あとは任せたぜということをやっています。
EnemySpawnerオブジェクトのインスペクターで「Enemy Spawner > Path Data Array」にプレハブのPathオブジェクトをドラッグ&ドロップして設定します。
EnemyControllerスクリプトの修正
EnemyControllerスクリプトを修正します。
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using DG.Tweening; // DOTweenを使うために必要な宣言
using System.Linq; // Linqを使うために必要な宣言
public class EnemyController : MonoBehaviour
{
// ここから 変数を削除
//[SerializeField, Header("移動経路の情報")]
//private PathData pathData; // 移動経路情報
// ここまで
[SerializeField, Header("移動速度")]
private float speed;
[SerializeField, Header("最大HP")]
private int maxHp;
[SerializeField, Header("HP")]
private int hp;
private Tween tween; // DOPathメソッドの処理を代入しておく変数
private Vector3[] path; // pathDataから取得した座標を格納するための配列
private Animator animator; // Animatorコンポーネントの取得
void Start()
{
hp = maxHp; // 敵のHPを設定
TryGetComponent(out animator); // Animatorコンポーネントを取得して代入
// ここから 処理を削除
// 経路を取得
// path = pathData.pathArray.Select(x => x.position).ToArray();
// 経路の総距離を計算
// float totalDistance = CalculatePathLength(path);
// 移動時間を計算 (距離 ÷ 速度)
// float moveDuration = totalDistance / speed;
// 経路に沿って移動する処理をtween変数に代入
// tween = transform.DOPath(path, moveDuration)
// .SetEase(Ease.Linear)
// .OnWaypointChange(x => ChangeWalkingAnimation(x));
// ここまで
}
// ここから メソッドを追加
/// <summary>
/// 経路情報を初期化
/// </summary>
public void InitializePath(PathData pathData)
{
// 経路を取得
path = pathData.pathArray.Select(x => x.position).ToArray();
// 経路の総距離を計算
float totalDistance = CalculatePathLength(path);
// 移動時間を計算 (距離 ÷ 速度)
float moveDuration = totalDistance / speed;
// 経路に沿って移動する処理をtween変数に代入
tween = transform.DOPath(path, moveDuration)
.SetEase(Ease.Linear)
.OnWaypointChange(x => ChangeWalkingAnimation(x));
}
// ここまで
// ...省略...
}
9-12行目、移動経路の情報はEnemySpawnerスクリプトに持たせることにしたので削除します。
28-39行目、経路に関する処理をメソッド化するので削除します。
42-59行目、経路情報を初期化するメソッドです。EnemySpawnerスクリプトから呼び出されます。
動作確認
ゲームを実行して動作確認をします。
敵がランダムに複数の経路に沿って移動しました。いえい。
さいごに
今のところ、複数の経路をランダムに選択する仕様になっていますが、将来的にはwaveごとに特定の経路を使うようにしたいです。wave機能を実装するのはいつになるのだろう(遠い目)。
でわでわ
コメント