Unity本格入門:いきのこバトルで学ぶ敵スポーンと NavMesh
題材・出典: 技術評論社刊『作って学べる Unity本格入門[Unity 6対応版]』(賀好 昭仁 著)に基づく学習補助の解説です。書籍の代替提供を目的とせず、コード掲載は学習上必要な範囲(必要最小限)にとどめます。利用条件は書籍記載(P.4〜5)および出版社サポート情報に従ってください。本シリーズ目次(書籍・著作の注記)
対象読者:第3回まで読み、プレイヤー周りの流れを把握したあと、敵の生成と上限管理を追いたい方
Spawner.csとSpawnerManager.csを読み、コルーチン・Instantiate・NavMesh・難易度との関係を整理します。
前提:第2回:メインシーンの時間と昼夜 の PassedDay(経過日数)が、スポーン間隔や距離の計算に使われます。
記事の目次
- この記事で扱う範囲
- コードを全部見てみよう
- クラス関係(この記事で登場する範囲)
- UMLで整理する(シーケンス)
- ポイント解説(①〜⑦)
- コードの流れを整理しよう
- 自分でカスタマイズしてみよう!
- まとめ
ポイント一覧
- ①
SpawnerとSpawnerManagerの役割分担 - ②
StartCoroutineとSpawnLoop - ③
PassedDayと出現位置・間隔の難易度 - ④
NavMesh.SamplePositionとInstantiate - ⑤
RefreshEnemies:距離で敵を削除する - ⑥
ListとLINQで null を除去する - ⑦
IsSpawnableで同時存在数を制限する
この記事で扱う範囲
- 対象ファイル:
Spawner.cs、SpawnerManager.cs - 前提知識:シーンに NavMesh がベイクされており、敵プレハブに NavMeshAgent がある想定(3D で経路探索させる一般的な構成です)

説明(学習のヒント):コルーチンで 一定間隔に Instantiate し、NavMesh 上の有効な位置に敵を置くイメージです。経過日数に応じた難易度や、距離による削除も記事で追います。
コードを全部見てみよう
SpawnerManager.cs
using System.Collections.Generic;
using System.Linq;
using UnityEngine;
public class SpawnerManager : MonoBehaviour
{
private const float DistanceLimitFromPlayer = 20f;
[SerializeField] private PlayerController player;
[SerializeField] private int max;
public bool IsSpawnable => _enemies.Count < max;
private List<GameObject> _enemies = new List<GameObject>();
public void RefreshEnemies()
{
var removeTargetEnemies = _enemies.Where(
x => null != x
&& (x.transform.localPosition - player.transform.localPosition).magnitude > DistanceLimitFromPlayer);
foreach (var enemy in removeTargetEnemies)
{
Destroy(enemy.gameObject);
}
_enemies = _enemies.Where(x => x != null).ToList();
}
public void AddEnemy(GameObject enemy)
{
_enemies.Add(enemy);
}
}Spawner.cs
using System.Collections;
using UnityEngine;
using UnityEngine.AI;
[RequireComponent(typeof(SpawnerManager))]
public class Spawner : MonoBehaviour
{
[SerializeField] private PlayerStatus playerStatus;
[SerializeField] private GameObject enemyPrefab;
private SpawnerManager _manager;
private void Start()
{
_manager = GetComponent<SpawnerManager>();
StartCoroutine(SpawnLoop());
}
private IEnumerator SpawnLoop()
{
while (true)
{
var passedDay = MainSceneController.Instance.PassedDay;
_manager.RefreshEnemies();
if (_manager.IsSpawnable)
{
var distanceVector = new Vector3(5, 0);
var spawnPositionFromPlayer = Quaternion.Euler(0, Random.Range(0, 360f), 0) * distanceVector *
Random.Range(2.5f - Mathf.Min(0.5f * passedDay, 1.5f), 4f);
var spawnPosition = playerStatus.transform.position + spawnPositionFromPlayer;
if (NavMesh.SamplePosition(spawnPosition, out var navMeshHit, 10, NavMesh.AllAreas))
{
var enemy = Instantiate(enemyPrefab, navMeshHit.position, Quaternion.identity);
_manager.AddEnemy(enemy);
}
}
yield return new WaitForSeconds(8f - Mathf.Min(2f * passedDay, 6f));
if (playerStatus.Life <= 0)
{
break;
}
}
}
}クラス関係(この記事で登場する範囲)
説明(学習のヒント):Spawner と SpawnerManager は同じオブジェクトに必ず付く関係(点線の RequireComponent)。難易度は MainSceneController の経過日数を見に行きます。
UMLで整理する(シーケンス)
SpawnLoop が MainSceneController の経過日数を参照し、SpawnerManager で敵を管理する関係を表します。
説明(学習のヒント):スポナーがまず経過日数を聞き、敵を足す処理はマネージャーに任せる、という呼び出しの順番です。
ポイント①:Spawner と SpawnerManager の役割分担
Spawner:コルーチンで一定間隔に敵を出したい位置を計算し、InstantiateするSpawnerManager:出現済み敵の List を持ち、遠くに行った敵を消す・数の上限を見る
[RequireComponent(typeof(SpawnerManager))] で、同じ GameObject に両方が付くようになっています。
ポイント②:StartCoroutine と SpawnLoop
SpawnLoop は無限ループ while (true) ですが、毎周 yield return new WaitForSeconds(...) で待つので、フレームを占有し続けません。プレイヤーが倒れたら break でコルーチンを終えます。
ポイント③:PassedDay と出現位置・間隔の難易度
var passedDay = MainSceneController.Instance.PassedDay;経過日数が増えると、
- プレイヤーからのオフセットのランダム範囲が変化(近づきやすくなる)
- 待ち時間
8f - Mathf.Min(2f * passedDay, 6f)が短くなる(最短 2 秒付近まで)
といった形で、時間経過とともに敵が出やすい方向に振れています。
ポイント④:NavMesh.SamplePosition と Instantiate
if (NavMesh.SamplePosition(spawnPosition, out var navMeshHit, 10, NavMesh.AllAreas))
{
var enemy = Instantiate(enemyPrefab, navMeshHit.position, Quaternion.identity);ランダムに決めた座標が NavMesh の外だと、エージェントが動けません。一番近い NavMesh 上の点を SamplePosition で取得し、そこに敵を生成します。
ポイント⑤:RefreshEnemies:距離で敵を削除する
プレイヤーから 20 以上離れた敵は Destroy し、負荷と敵の滞留を抑えます。Destroy は次のフレームで実際に消えるため、その直後に List を掃除する必要があります(次項)。
ポイント⑥:List と LINQ で null を除去する
_enemies = _enemies.Where(x => x != null).ToList();Destroy された GameObject は参照が null になる性質を利用し、死んだ参照を List から除外しています。
ポイント⑦:IsSpawnable で同時存在数を制限する
public bool IsSpawnable => _enemies.Count < max;Inspector で max を設定し、場にいる敵の数が上限未満のときだけ新規生成します。RefreshEnemies で数が減れば、また生成可能になります。
コードの流れを整理しよう
説明(学習のヒント):ひと周の SpawnLoop で何をするかをまとめた図です。ひし形は「分岐」、上から下へ進むと1周期の流れが追いやすいです。
自分でカスタマイズしてみよう!
挑戦①:max と DistanceLimitFromPlayer
同時敵数と「どのくらい離れたら消すか」はゲームの手触りに直結します。数値を変えて挙動を比較してみましょう。
挑戦②:スポーン間隔の式
8f - Mathf.Min(2f * passedDay, 6f) のグラフを意識すると、何日目から頭打ちになるか説明しやすくなります。
まとめ
- スポナーがコルーチンで周期処理し、マネージャがリストと上限・距離削除を担当する分業
MainSceneController.Instance.PassedDayで難易度パラメータを変えるNavMesh.SamplePositionで walkable な位置にInstantiateするDestroy後の null を List から除き、同時存在数maxで負荷と難易度を調整する
続きは 第5回:UI とメニュー です。
最終更新:2026年4月