Unityを触り始めた頃って、プレイヤーの移動も敵の出現もスコア管理も、全部ひとつの Update() に書いてしまいがちですよね。最初は動くので満足できますが、あとから「敵の出現パターンを変えたい」「ステージごとに出現場所を変えたい」となった瞬間、その巨大なスクリプトを触るのが怖くなります。
そこで今回は「敵を定期的に出現させる」という機能だけを切り出した、小さなコンポーネントを作ってみましょう。
プレイヤーのロジックとは完全に分離して、「召喚だけ」を担当する Summoner(召喚士)コンポーネント です。
【Unity】敵を定期湧きさせて戦場をデザイン!「Summoner」コンポーネント
このコンポーネントは、指定したプレハブ(雑魚敵など)を一定間隔でインスタンス化してシーンに送り出す「召喚士」です。
・どの敵を出すか
・どこに出すか
・どれくらいの間隔で出すか
をインスペクターから調整できるようにして、レベルデザインをしやすくします。
フルコード:Summoner.cs
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
/// <summary>
/// 一定間隔で敵プレハブをインスタンス化する「召喚士」コンポーネント。
/// - Summoner がアタッチされた GameObject の位置を基準にスポーン
/// - 複数種類の敵プレハブからランダムに選択可能
/// - 同時に存在できる最大数を制限可能
/// - ゲーム開始時に自動で召喚開始 or 手動で開始を選択可能
/// </summary>
public class Summoner : MonoBehaviour
{
[Header("召喚対象のプレハブ設定")]
[Tooltip("召喚する敵プレハブ(1つ以上)。複数登録するとランダムに選ばれます。")]
[SerializeField] private List<GameObject> enemyPrefabs = new List<GameObject>();
[Header("召喚位置設定")]
[Tooltip("true の場合は Summoner 自身の位置を基準に召喚します。")]
[SerializeField] private bool useOwnPosition = true;
[Tooltip("false の場合、ここで指定した Transform の位置に召喚します。")]
[SerializeField] private Transform customSpawnPoint;
[Tooltip("召喚位置にランダムな揺らぎを加えます(X,Z平面)。")]
[SerializeField] private Vector2 randomOffsetRange = Vector2.zero;
[Header("召喚タイミング設定")]
[Tooltip("ゲーム開始時に自動で召喚を開始するかどうか。")]
[SerializeField] private bool autoStartOnAwake = true;
[Tooltip("最初の召喚までの待ち時間(秒)。")]
[SerializeField] private float firstSpawnDelay = 1.0f;
[Tooltip("召喚間隔(秒)。一定間隔で召喚します。")]
[SerializeField] private float spawnInterval = 3.0f;
[Tooltip("召喚間隔にランダムな揺らぎを加えます。0 なら完全な固定間隔。")]
[SerializeField] private float spawnIntervalRandomRange = 0.0f;
[Header("召喚数制限")]
[Tooltip("同時に存在できる敵の最大数。0 以下の場合は無制限。")]
[SerializeField] private int maxAliveCount = 10;
[Tooltip("総召喚数の上限。0 以下の場合は無制限。")]
[SerializeField] private int maxTotalSpawnCount = 0;
[Header("デバッグ / 可視化")]
[Tooltip("Scene ビューに召喚範囲をギズモ表示します。")]
[SerializeField] private bool drawGizmos = true;
[Tooltip("ギズモの色。")]
[SerializeField] private Color gizmoColor = new Color(1f, 0.3f, 0.3f, 0.3f);
// 実行時の内部状態
private readonly List<GameObject> _aliveEnemies = new List<GameObject>();
private Coroutine _spawnRoutine;
private int _totalSpawnedCount;
private bool _isSpawning;
// 召喚を外部から制御したいとき用のプロパティ
public bool IsSpawning => _isSpawning;
private void Awake()
{
// Awake のタイミングで自動開始するかどうか
if (autoStartOnAwake)
{
StartSummon();
}
}
/// <summary>
/// 召喚処理を開始します。
/// すでに開始済みの場合は何もしません。
/// </summary>
public void StartSummon()
{
if (_isSpawning) return;
// 敵プレハブが1つも設定されていない場合は警告を出して終了
if (enemyPrefabs == null || enemyPrefabs.Count == 0)
{
Debug.LogWarning($"[Summoner] enemyPrefabs が設定されていません: {name}", this);
return;
}
_isSpawning = true;
_spawnRoutine = StartCoroutine(SpawnLoop());
}
/// <summary>
/// 召喚処理を停止します。
/// すでに停止している場合は何もしません。
/// </summary>
public void StopSummon()
{
if (!_isSpawning) return;
_isSpawning = false;
if (_spawnRoutine != null)
{
StopCoroutine(_spawnRoutine);
_spawnRoutine = null;
}
}
/// <summary>
/// 現在生きている敵の数を返します。
/// </summary>
public int GetAliveCount()
{
CleanupDeadReferences();
return _aliveEnemies.Count;
}
/// <summary>
/// 総召喚数を返します。
/// </summary>
public int GetTotalSpawnedCount()
{
return _totalSpawnedCount;
}
/// <summary>
/// メインの召喚ループ。
/// コルーチンで一定間隔ごとに敵を召喚します。
/// </summary>
private IEnumerator SpawnLoop()
{
// 最初の待ち時間
if (firstSpawnDelay > 0f)
{
yield return new WaitForSeconds(firstSpawnDelay);
}
while (_isSpawning)
{
// 上限チェック(同時存在数)
CleanupDeadReferences();
if (maxAliveCount > 0 && _aliveEnemies.Count >= maxAliveCount)
{
// 上限に達している場合は、次のチェックまで待つだけ
yield return new WaitForSeconds(GetNextInterval());
continue;
}
// 上限チェック(総召喚数)
if (maxTotalSpawnCount > 0 && _totalSpawnedCount >= maxTotalSpawnCount)
{
// これ以上召喚できないのでループ終了
_isSpawning = false;
yield break;
}
// 敵を召喚
SpawnOneEnemy();
// 次の召喚まで待機
yield return new WaitForSeconds(GetNextInterval());
}
}
/// <summary>
/// 1体の敵を召喚する。
/// </summary>
private void SpawnOneEnemy()
{
// プレハブをランダム選択
GameObject prefab = GetRandomEnemyPrefab();
if (prefab == null)
{
Debug.LogWarning($"[Summoner] 有効な enemyPrefabs がありません: {name}", this);
return;
}
// 召喚位置を計算
Vector3 spawnPos = GetSpawnPosition();
Quaternion spawnRot = Quaternion.identity;
// 実際にインスタンス化
GameObject enemy = Instantiate(prefab, spawnPos, spawnRot);
// 管理リストに追加
_aliveEnemies.Add(enemy);
_totalSpawnedCount++;
}
/// <summary>
/// 有効な敵プレハブをランダムに1つ取得する。
/// </summary>
private GameObject GetRandomEnemyPrefab()
{
// null が含まれている可能性もあるので一度フィルタリング
var validList = enemyPrefabs.FindAll(p => p != null);
if (validList.Count == 0) return null;
int index = Random.Range(0, validList.Count);
return validList[index];
}
/// <summary>
/// 実際の召喚位置を計算する。
/// useOwnPosition / customSpawnPoint / randomOffsetRange を考慮。
/// </summary>
private Vector3 GetSpawnPosition()
{
Vector3 basePos;
if (useOwnPosition || customSpawnPoint == null)
{
basePos = transform.position;
}
else
{
basePos = customSpawnPoint.position;
}
// ランダムオフセット(XZ平面)
if (randomOffsetRange != Vector2.zero)
{
float offsetX = Random.Range(-randomOffsetRange.x, randomOffsetRange.x);
float offsetZ = Random.Range(-randomOffsetRange.y, randomOffsetRange.y);
basePos += new Vector3(offsetX, 0f, offsetZ);
}
return basePos;
}
/// <summary>
/// 次の召喚までのインターバルを計算する。
/// spawnIntervalRandomRange を考慮。
/// </summary>
private float GetNextInterval()
{
if (spawnIntervalRandomRange <= 0f)
{
return Mathf.Max(0.01f, spawnInterval);
}
float half = spawnIntervalRandomRange * 0.5f;
float randomOffset = Random.Range(-half, half);
float interval = spawnInterval + randomOffset;
// マイナスにならないように下限を設ける
return Mathf.Max(0.01f, interval);
}
/// <summary>
/// すでに Destroy 済みの敵をリストから取り除く。
/// </summary>
private void CleanupDeadReferences()
{
// null になっている要素を削除
_aliveEnemies.RemoveAll(e => e == null);
}
/// <summary>
/// Scene ビューで召喚位置・範囲を可視化する。
/// </summary>
private void OnDrawGizmosSelected()
{
if (!drawGizmos) return;
// ギズモの色を設定
Gizmos.color = gizmoColor;
// 基準位置
Vector3 basePos;
if (useOwnPosition || customSpawnPoint == null)
{
basePos = transform.position;
}
else
{
basePos = customSpawnPoint.position;
}
// 中心を球で表示
Gizmos.DrawSphere(basePos, 0.2f);
// ランダムオフセット範囲を矩形で表示(XZ平面)
if (randomOffsetRange != Vector2.zero)
{
Vector3 size = new Vector3(
randomOffsetRange.x * 2f,
0.01f,
randomOffsetRange.y * 2f
);
Gizmos.DrawWireCube(basePos, size);
}
}
/// <summary>
/// Summoner が破棄されるときにコルーチンを止める。
/// </summary>
private void OnDestroy()
{
StopSummon();
_aliveEnemies.Clear();
}
}
使い方の手順
ここでは「雑魚敵を定期的に湧かせる敵スポナー」として使う具体例を見ていきましょう。
-
敵プレハブを用意する
- シーン上に「Enemy」オブジェクトを作成し、見た目(Mesh / Sprite)や当たり判定(Collider)、移動用スクリプトなどを設定します。
- 完成したら、そのオブジェクトを
Projectウィンドウにドラッグ&ドロップしてプレハブ化します(例:EnemyWeak)。 - 必要なら、別バリエーション(
EnemyFastやEnemyTankなど)も同様にプレハブ化しておきましょう。
-
Summoner 用の空オブジェクトを作る
- シーン上で
右クリック > Create Emptyを選び、名前をEnemySummonerなどに変更します。 - このオブジェクトの位置が「召喚の基準地点」になります。
たとえばステージの左端に置けば「左から敵が湧いてくる」イメージですね。 EnemySummonerにSummoner.csをアタッチします。
- シーン上で
-
インスペクターでパラメータを設定する
Enemy Prefabs: 用意した敵プレハブを 1 個以上ドラッグ&ドロップします。
複数入れた場合は、毎回ランダムでどれか1つが召喚されます。Use Own Position:EnemySummonerの位置から湧かせたいなら ON のままでOK。Custom Spawn Point: 特定の Empty をスポーン位置にしたい場合だけ指定します(Use Own Positionを OFF にする)。Random Offset Range: 例えば(5, 5)にすると、基準位置の周囲 XZ ±5 の範囲にランダムで出現します。Auto Start On Awake: シーン開始と同時に召喚を始めたいなら ON。First Spawn Delay: 最初の召喚までの待ち時間(例: 2秒)。Spawn Interval: 召喚間隔(例: 3秒ごと)。Spawn Interval Random Range: 例えば 2 にすると「3秒 ±1秒」の間隔で少しランダムに湧きます。Max Alive Count: 同時に存在できる敵の上限(例: 10)。0 なら無制限。Max Total Spawn Count: 合計で何体まで召喚するか(例: 50)。0 なら無制限。
-
実際のシーンで使う具体例
- プレイヤー vs 雑魚ラッシュ
ステージ両端にEnemySummoner_LeftとEnemySummoner_Rightを置いて、左右から雑魚が押し寄せるようにします。
左右でEnemy Prefabsの種類やSpawn Intervalを変えると、難易度調整がしやすいです。 - ボス戦中に雑魚を呼ぶ召喚士
ボスの子オブジェクトとしてBossSummonerを配置し、Use Own Positionを ON にしておけば、ボスの周囲から雑魚が湧き続けます。
ボスがフェーズ2に移行したタイミングでSummoner.StartSummon()を呼ぶ、といった制御も簡単です。 - 動く床の上から敵が落ちてくる
動く床オブジェクトにSummonerをアタッチしておけば、その床の移動に合わせて召喚位置も動いてくれます。
Random Offset Rangeを少し広めにしておくと、「床の上のどこかから敵が落ちてくる」演出が作れます。
- プレイヤー vs 雑魚ラッシュ
メリットと応用
Summoner コンポーネントを使う最大のメリットは、「敵の出現ロジックをプレイヤーやゲーム進行のスクリプトから完全に切り離せる」ことです。
- プレハブ管理がシンプルになる
敵の種類を増やしたいときは、単に新しい敵プレハブを作ってEnemy Prefabsに追加するだけ。
既存のプレイヤーコードや GameManager を触らずに、敵のバリエーションを増やせます。 - レベルデザインが楽になる
シーン上に Summoner をぽんぽん置いて、位置とパラメータを変えるだけで「このエリアは雑魚が多い」「ここは間隔を短くして緊張感アップ」などを視覚的に調整できます。
スクリプトをいじらなくても、レベルデザイナーがインスペクターだけで調整できるのも嬉しいポイントですね。 - 責務が小さいのでテストしやすい
Summoner は「召喚する」ことだけに責務を絞っているので、バグの切り分けがしやすくなります。
「敵が動かない」のか「敵が出ていない」のか、どこに問題があるかが明確になります。
さらに、少しコードを足すだけで応用の幅が広がります。
例えば「プレイヤーが一定距離まで近づいたら召喚を開始する」トリガーを付けるのも簡単です。
/// <summary>
/// 指定したターゲットとの距離が threshold 以下になったら召喚を開始する。
/// (例: プレイヤーが近づいたら敵が湧き始める)
/// </summary>
public void StartSummonWhenNear(Transform target, float threshold)
{
StartCoroutine(StartWhenNearRoutine(target, threshold));
}
private IEnumerator StartWhenNearRoutine(Transform target, float threshold)
{
// ターゲットが存在し、まだ召喚を開始していない間は距離を監視
while (target != null && !IsSpawning)
{
float distance = Vector3.Distance(transform.position, target.position);
if (distance <= threshold)
{
StartSummon();
yield break;
}
yield return null;
}
}
このように、小さなコンポーネントとして Summoner を育てていくと、「敵の湧き方」を自由自在にデザインできるようになります。
巨大な God クラスにロジックを詰め込まず、「召喚は Summoner に任せる」という発想で、スッキリしたプロジェクト構成を目指していきましょう。
