Appearance
第05回 Enemyの探索・追跡・攻撃
前回: 第04回 衝突判定とダメージ / 次回: 第06回 Enemyの状態制御とゲームフィール
今回の授業内容

- 第04回で作成した
CharacterAttackをEnemyからも利用する - Enemyが攻撃対象を探索し、追跡して攻撃する処理を実装する
- PrefabとInstanceの関係を理解する
- 複数のSpawn PointからEnemyを一定間隔で生成する
- 生成数の上限と破棄済みInstanceの管理を実装する
今回の配布コード
第04回までの全スクリプトに、共通攻撃、Enemyの追跡、Enemy生成を加えた第05回終了時点の構成です。第04回までに作成したCharacterAttack.csを変更せず、Enemy側にも接続します。
| ファイル | 内容 |
|---|---|
| PlayerController.cs | 移動、カメラ、共通攻撃の入力 |
| PlayerMove.cs | プレイヤーの移動と振り向き |
| CameraMove.cs | カメラの追従と回転 |
| Health.cs | HPの管理 |
| DamageReceiver.cs | ダメージの受付 |
| AttackContext.cs | 攻撃情報 |
| AttackResolver.cs | 自分自身と同じ陣営を除外する攻撃ルール |
| CharacterAttack.cs | PlayerとEnemyで共有する攻撃処理 |
| AttackTarget.cs | 攻撃対象を示すマーカー |
| EnemyController.cs | 対象の探索、追跡、攻撃 |
| EnemySpawner.cs | Enemy Prefabの生成と生存数管理 |
| Player.cs | PlayerのHPと表示の接続 |
| Enemy.cs | EnemyのHPと死亡処理 |
| AttackEffect.cs | 攻撃演出 |
| SlashEffect.cs | 剣閃エフェクト |
| HealthGauge.cs | HPゲージ |
第04回までの構成へEnemyの追跡と生成を加えます。GameObjectの親子関係と、各GameObjectへ追加するComponentを分けて確認します。
GameObjectのHierarchy
次の図では、インデントがHierarchy上の親子関係を表します。Componentは含めていません。
- Player
- Player Attack
- Enemy Prefab
- Enemy Attack Range
- Enemy Spawner
Playerへ設定するComponent
| GameObject | Component | 役割 |
|---|---|---|
| Player | Rigidbody | 物理判定に参加する |
| Player | Player | HPと表示を接続する |
| Player | PlayerController | 入力を読み取る |
| Player | PlayerMove | 移動と回転を行う |
| Player | DamageReceiver | ダメージを受け付ける |
| Player | AttackTarget | Enemyの攻撃対象になる |
| Player Attack | CharacterAttack | 攻撃時間と命中判定を管理する |
| Player Attack | Trigger Collider | 攻撃範囲を表す |
Enemy Prefabへ設定するComponent
| GameObject | Component | 役割 |
|---|---|---|
| Enemy Prefab | Rigidbody | 物理判定に参加し、敵同士を押し戻す |
| Enemy Prefab | Enemy | HPと死亡処理を接続する |
| Enemy Prefab | EnemyController | 対象の探索、追跡、攻撃判断を行う |
| Enemy Prefab | DamageReceiver | ダメージを受け付ける |
| Enemy Attack Range | CharacterAttack | 攻撃時間と命中判定を管理する |
| Enemy Attack Range | Trigger Collider | 攻撃範囲を表す |
Enemyの生成と攻撃対象の探索
Enemy SpawnerはEnemy Prefabの生成だけを担当します。生成されたEnemyは、自分で攻撃対象を探します。
Enemy Spawner
Instanceを生成する
Enemy Prefab
Enemy Instance
攻撃対象を探す
AttackTarget
既存のCharacterAttackをEnemyでも利用する
第04回では、Playerの攻撃としてCharacterAttackを実装しました。 Playerの攻撃ですが入力とは切り離されており、攻撃時間、当たり判定、命中済みリスト、ダメージ解決の処理はEnemyの攻撃にも利用できます。 今回はこのCharacterAttackをEnemyの攻撃としても利用し、PlayerとEnemyで共通化します。
CharacterAttackの責務を決める
CharacterAttack は、攻撃中だけ攻撃用Colliderを有効にし、Triggerに入った DamageReceiver を検出します。 また、同じ攻撃で同じ相手に何度もダメージを与えないように命中対象を記録します。 ダメージ処理の実際は AttackResolver に任せ、自身は攻撃のタイミングと当たり判定に集中します。
CharacterAttackが扱うこと | 直接扱わないこと |
|---|---|
| 攻撃時間の管理 | HP計算 |
| 攻撃判定ColliderのON/OFF | 死亡処理 |
命中したDamageReceiverの検出 | 追跡AI |
AttackResolverへの受け渡し | Enemyの生成 |
CharacterAttackを実装する
次のコードは第04回で完成したCharacterAttackの抜粋です。 攻撃中だけ攻撃判定Colliderを有効にし、命中した DamageReceiver にダメージ処理を渡します。 細かい処理についてはソースコード内のコメントで確認してください。
cs
using System.Collections.Generic;
using UnityEngine;
public class CharacterAttack : MonoBehaviour
{
[SerializeField] private AttackEffect attackEffect;
[SerializeField] private Collider attackCollider;
[SerializeField] private int damage = 1;
[SerializeField] private float attackDuration = 0.5f;
[SerializeField] private float hitStartTime = 0.15f;
[SerializeField] private float hitEndTime = 0.3f;
// 1回の攻撃で同じ相手に何度も当たらないよう、
// 命中済みの DamageReceiver を覚えておく。
private readonly List<DamageReceiver> _hitReceivers = new();
// 攻撃中かどうか。
// EnemyController など、外側の処理が行動判断に使えるよう公開する。
private bool _isAttacking;
// 攻撃判定Colliderが現在有効かどうか。
// Collider.enabled を直接読まず、攻撃状態としてコード側でも保持する。
private bool _isHitActive;
// 攻撃開始からの経過時間。
// hitStartTime / hitEndTime / attackDuration の判定に使う。
private float _attackElapsedTime;
public bool IsAttacking => _isAttacking;
private void Awake()
{
// 攻撃していない間は、攻撃判定を必ず無効にしておく。
// attackCollider は必須参照なので、設定漏れはエラーとして気づける状態にする。
attackCollider.enabled = false;
}
private void Update()
{
if (!_isAttacking)
return;
_attackElapsedTime += Time.deltaTime;
// 攻撃開始から一定時間後に、攻撃判定を有効にする。
if (!_isHitActive &&
_attackElapsedTime >= hitStartTime)
StartHit();
// 攻撃判定の有効時間が終わったら、判定だけを先に閉じる。
if (_isHitActive &&
_attackElapsedTime >= hitEndTime)
EndHit();
// 攻撃全体の時間が終わったら、攻撃状態を終了する。
if (_attackElapsedTime >= attackDuration)
EndAttack();
}
public void Attack()
{
// 攻撃中に再度 Attack() が呼ばれても、攻撃を重ねない。
if (_isAttacking)
return;
// attackEffect は演出用なので、未設定でも攻撃処理は成立させる。
attackEffect?.Play();
// 新しい攻撃なので、前回の命中履歴を消す。
_hitReceivers.Clear();
_isAttacking = true;
_isHitActive = false;
_attackElapsedTime = 0f;
}
private void OnTriggerEnter(Collider other)
{
// 攻撃判定に入ったColliderの親から、ダメージ受付コンポーネントを探す。
var receiver =
other.GetComponentInParent<DamageReceiver>();
if (receiver == null)
return;
// 同じ攻撃中に同じ相手へ複数回ダメージを与えない。
if (_hitReceivers.Contains(receiver))
return;
_hitReceivers.Add(receiver);
// 攻撃者とダメージ量をまとめ、実際の判定は AttackResolver に任せる。
var attackContext =
new AttackContext(gameObject, damage);
AttackResolver.Resolve(attackContext, receiver);
}
private void EndAttack()
{
_isAttacking = false;
EndHit();
}
private void StartHit()
{
_isHitActive = true;
attackCollider.enabled = true;
}
private void EndHit()
{
_isHitActive = false;
attackCollider.enabled = false;
}
}EnemyからのCharacterAttackの利用
Playerの攻撃は、PlayerController が攻撃ボタン入力を受け取り、都度 CharacterAttack.Attack() を呼ぶ形です。
一方、敵の行動はAIで制御される必要があります。 例えばプレイヤーが攻撃範囲に入ったら自動的に攻撃する、といった挙動を実現しなけばなりません。 この場合は攻撃対象を監視・追跡し、攻撃距離に入ったら CharacterAttack.Attack() を呼ぶ形になります。
PlayerController.cscs
public class PlayerController : MonoBehaviour
{
[SerializeField] private CharacterAttack characterAttack;
private void Update()
{
// 省略: 移動とカメラ操作
if (_inputActions.Player.Attack.WasPressedThisFrame())
characterAttack.Attack();
}
}AttackTargetをマーカーコンポーネントとして実装する
敵の攻撃対象は通常プレイヤーですが、Enemyが Player 型を直接参照すると、EnemyはPlayerに強く依存することになってしまいます。 今回Enemyに必要なのは、相手がPlayerかどうかではなく、相手を攻撃対象として扱ってよいか と 対象がどこにいるか の情報です。 Playerを直接参照しなければ、敵の同士討ちやマルチプレイなど将来的な拡張にも柔軟に対応できます。
今回は、Enemyが攻撃対象にできるObjectに AttackTarget という目印Componentを付けます。 EnemyはScene内の AttackTarget を探し、近い対象を追跡、攻撃します。
AttackTarget は処理をほとんど持たないマーカーComponentです。 PlayerにこのComponentを付けると、Enemyから攻撃対象として見つけられるようになります。 テスト用のCubeに AttackTarget を付ければ、PlayerなしでもEnemyの追跡と攻撃の挙動を確認できます。
cs
using UnityEngine;
public class AttackTarget : MonoBehaviour
{
// 目印以外の処理は特に持ちません
}EnemyControllerで探索・追跡・攻撃を制御する
EnemyController は、攻撃対象の探索、移動、攻撃判断を担当します。 攻撃対象が遠い間は近づき、攻撃距離に入ったら止まって攻撃します。 実際の当たり判定とダメージ処理は、既に作った CharacterAttack に任せます。
Update内の判断順序
Update では、攻撃対象の探索と攻撃判断を行います。 移動すると決まったら、transform.position を少しずつ動かします。
今回のEnemyは、物理挙動ではなく直接座標を操作して移動します。 ただし、そのままでは敵同士がめり込んでしまうため、Enemy本体に Rigidbody を付けて、Collider同士の衝突で押し戻されるようにします。
必要な参照と設定値
characterAttack はEnemy自身の攻撃処理です。searchRadius は攻撃対象を探す範囲です。moveSpeed と rotateSpeed は移動の調整値です。attackDistance と attackInterval は攻撃判断に使います。
cs
using UnityEngine;
public class EnemyController : MonoBehaviour
{
[SerializeField] private CharacterAttack characterAttack;
[SerializeField] private float searchRadius = 30f;
[SerializeField] private float moveSpeed = 3f;
[SerializeField] private float attackDistance = 1.5f;
[SerializeField] private float attackInterval = 1.0f;
[SerializeField] private float rotateSpeed = 10f;
private AttackTarget _target;
private float _lastAttackTime = -999f;攻撃対象を探して行動を判断する
目標が無ければ FindNearestTarget で探します。見つからない時は、そのフレームでは移動しません。攻撃中も移動処理へ進みません。
cs
private void Update()
{
if (_target == null)
_target = FindNearestTarget();
if (_target == null)
return;
if (characterAttack.IsAttacking)
return;距離を見て攻撃する
Y方向は無視し、X-Z平面上で近づきます。攻撃距離に入ったら移動せず、攻撃を試みます。まだ遠い場合は、次のスライドで移動方向を決めます。
cs
var toTarget =
_target.transform.position - transform.position;
toTarget.y = 0f;
if (toTarget.magnitude <= attackDistance)
{
TryAttack();
return;
}Transformで移動する
攻撃対象の方向へ、transform.position を少しずつ動かします。向きも同じ方向へ回します。TryAttack は攻撃間隔から判断し必要に応じて CharacterAttack.Attack() を呼びます。
cs
var direction = toTarget.normalized;
transform.position +=
direction * moveSpeed * Time.deltaTime;
var targetRotation =
Quaternion.LookRotation(direction);
transform.rotation = Quaternion.Slerp(
transform.rotation,
targetRotation,
rotateSpeed * Time.deltaTime);
}
private void TryAttack()
{
if (Time.time < _lastAttackTime + attackInterval)
return;
characterAttack.Attack();
_lastAttackTime = Time.time;
}最も近い対象を探索する
FindNearestTarget はScene内の AttackTarget を調べます。searchRadius より近い対象の中から最も近いものを選びます。やや非効率な処理ですが、ここでは簡単のため FindObjectsByType を使用しています。
cs
private AttackTarget FindNearestTarget()
{
var targets = FindObjectsByType<AttackTarget>(FindObjectsSortMode.None);
AttackTarget nearest = null;
var nearestDistance = searchRadius;
foreach (var target in targets)
{
var distance = Vector3.Distance(
transform.position,
target.transform.position);
if (distance >= nearestDistance)
continue;
nearest = target;
nearestDistance = distance;
}
return nearest;
}
}Prefabのコンポーネントを設定する
Player Prefab

Player本体には Rigidbody、Player、PlayerController、PlayerMove、DamageReceiver を設定します。今回追加する AttackTarget によって、Enemyから攻撃対象として見つけられるようになります。Player は HP ゲージを持つ他、敵との攻撃の識別に使われます。前回までに作ったCharacterAttackをそのまま利用します。
Enemy Prefab

Enemy本体では、03回で作った Enemy と DamageReceiver をそのまま使います。今回はそこへ EnemyController と Rigidbody を追加し、Colliderも確認します。EnemyController の Character Attack には、Player同様に CharacterAttack を割り当てます。今回の Rigidbody は移動制御ではなく、敵同士を押し戻すために使います。Freeze Position Y と Freeze Rotation XYZ を確認してください。
本体Colliderと攻撃Colliderを分ける
Enemy本体のColliderは、Playerの攻撃を受けたり、物理的な大きさを表したりするために使います。一方で攻撃Colliderは、Enemyが攻撃中だけ有効にするTriggerです。Playerの攻撃判定と同じ考え方で、Enemyにも攻撃用の子Objectを用意します。2つを同じColliderにすると、移動中の接触と攻撃中の命中判定が混ざってしまいます。接触ダメージを与える種類の敵なら一緒でも構いませんが、今回は分割しています。
Rigidbodyで敵同士のめり込みを減らす

今回のEnemy移動は transform.position で行うため、移動処理のために Rigidbody は使いません。ただし敵同士が互いにめり込むのを避けるため今回はEnemy本体に Rigidbody を付けます。X/Z方向は固定せず、物理的に押し戻せる余地を残します。
Enemyが滑る場合はDampingを調整する
デフォルトの設定では、衝突で生まれた速度が残り、Enemyが少しずつ滑っていくことがあります。これを防ぐために、Linear Damping を大きめに設定して速度を減衰させるのがおすすめです。Unityのバージョンによっては Drag という名前で表示されます。03回の Rigidbody はTrigger判定を成立させるためでしたが、今回は敵同士を押し戻すために使用しています。
PrefabからEnemyを生成する
PrefabとInstance
Prefab は、GameObjectの構成を保存した設計図です。関連するオブジェクトとComponentの構成をまとめて保存できます。Instance は、Scene上に存在する実体です。Prefabから複製され、位置や状態を個別に持ちます。Enemyのように同じ構成のObjectを何度も使う場合、Prefabを1つ作っておき、必要なタイミングでInstanceを生成します。今回の Spawner は敵の生成しか扱いませんが、何らかの設定値を付与するのに好適な場所です。
Spawn Point
敵を出したい場所に空のGameObjectを置き、Spawn Point として使います。座標をコードに直接書くのではなく、Scene上で位置を設定するのがおすすめです。動作的にはやや非効率ですが、ゲームバランスの調整がしやすくなります。ゲームバランスに関わる位置は、なるべくInspectorやScene上で調整できる形にして、ゲームエンジンUnityの強みを活かす判断は決して間違いではありません。今回は複数のSpawn Pointを用意し、そのうちのランダムな場所から敵を出しています。
Prefabと生成位置を参照する
enemyPrefab は生成するEnemyのPrefabです。spawnPoints は生成位置の候補です。maxAliveCount で生成しすぎを防ぎます。Spawnerは生成だけを担当し、追跡対象の判断はEnemy側に任せます。03回で作った Enemy の型として持つことで、行動AIではなく敵そのものを生成対象として扱います。
cs
using System.Collections.Generic;
using UnityEngine;
public class EnemySpawner : MonoBehaviour
{
[SerializeField] private Enemy enemyPrefab;
[SerializeField] private Transform[] spawnPoints;
[SerializeField] private float spawnInterval = 1.5f;
[SerializeField] private int maxAliveCount = 20;
private readonly List<Enemy> _aliveEnemies = new();
private float _spawnTimer;一定間隔で生成を試みる
Update で経過時間を数え、指定間隔で Spawn を呼びます。破棄済みの敵がListに残っている場合は先に取り除きます。最大数に達している場合、それ以上は生成しません。
cs
private void Update()
{
CleanupDestroyedEnemies();
_spawnTimer += Time.deltaTime;
if (_spawnTimer < spawnInterval)
return;
_spawnTimer = 0f;
Spawn();
}生成したEnemyを管理する
ランダムなSpawn Pointを選んで Enemyを生成します。生成したEnemyをListに登録し、現在数を管理します。
cs
private void Spawn()
{
if (_aliveEnemies.Count >= maxAliveCount)
return;
var spawnPoint =
spawnPoints[Random.Range(0, spawnPoints.Length)];
var enemy = Instantiate(
enemyPrefab,
spawnPoint.position,
spawnPoint.rotation);
_aliveEnemies.Add(enemy);
}破棄済みのEnemyをListから外す
生成した敵を List に控えて生成した敵の数を管理していますが Destroy されたObjectは Listから自動で削除されるわけではありません。そのままでは敵が減っていても _aliveEnemies.Count が減らず、生成上限に達したままになります。RemoveAll を使い、条件に合う要素をまとめて Listから取り除きます。enemy => enemy == null は 「enemy が null なら対象」という条件です。
cs
private void CleanupDestroyedEnemies()
{
_aliveEnemies.RemoveAll(enemy => enemy == null);
}実習1:Player側のCharacterAttackを確認する
- 第04回の
CharacterAttack.csがProjectにあることを確認する - Playerの攻撃判定Colliderが
CharacterAttackに設定されていることを確認する PlayerControllerが同じCharacterAttackを参照していることを確認する- Play ModeでPlayerの攻撃とダメージが前回どおり動くことを確認する
- Playして、Playerの直接攻撃が前回と同じように動くか確認
実習2:Enemyに追跡と攻撃を追加する
Playerに
AttackTargetを追加Enemy本体に
EnemyControllerを追加Enemyに
Rigidbodyを追加し、 敵同士のめり込み対策を設定Is Kinematic: Off、Use Gravity: Off、Linear Damping: 大きめ、 Constraintsを確認Enemyの子Objectとして
Enemy Attack Rangeを作成、CharacterAttackを追加Enemy Attack Rangeに Trigger Collider を設定CharacterAttackのAttack Colliderに 攻撃用Colliderを設定EnemyControllerのCharacter AttackにCharacterAttackを設定
実習3:EnemySpawnerでEnemyを生成する
- EnemyをPrefab化します
Enemy Spawnerを作り、EnemySpawnerを追加しますEnemy Prefabに、ProjectビューのEnemy Prefabを設定しますSpawn Points配列へ、Scene上のSpawn Pointを登録します- 生成されたEnemyが
AttackTargetへ近づき、攻撃するか確認します
動作しない場合のチェックリスト
Enemyが追いかけてこない
- Playerに
AttackTargetが付いているか確認します - Enemyの
Search Radiusが小さすぎないか確認します - Enemyに
EnemyControllerが付いているか確認します moveSpeedが小さすぎないか確認します
Enemyが攻撃してこない
Enemy Attack RangeにCharacterAttackが付いているか確認しますEnemyControllerのCharacter Attackに参照が入っているか確認しますCharacterAttackのAttack Colliderに攻撃用Colliderが設定されているか確認します- 攻撃用Colliderの
Is Triggerが有効になっているか確認します - 攻撃対象に
DamageReceiverが付いているか確認します
生成された敵にPlayerの攻撃が当たらない
- Enemy PrefabにColliderが付いているか確認します
- Enemy本体、または親Objectに
DamageReceiverが付いているか確認します - Player側の
CharacterAttackの攻撃判定Colliderが、 攻撃中に有効になっているか確認します - PlayerとEnemyのLayer同士が接触しない設定になっていないか確認します
- Prefab化する前のScene上のEnemyだけにComponentを追加していて、 Prefab側に反映されていない場合があります
完成条件
- Playerは前回と同じ直接攻撃を使えます
- Enemyは
AttackTargetへ近づき、攻撃距離で止まります - Enemyは一定間隔で攻撃対象へ攻撃できます
- EnemySpawnerがEnemy Prefabを一定間隔で生成します
- Playerの直接攻撃で、生成されたEnemyにもダメージを与えられます
- HPが0になったEnemyはSceneから破棄されます
まとめ
- 第04回で共通名にした
CharacterAttackをPlayerとEnemyの両方から利用した - Playerは入力から、Enemyは行動判断から同じ攻撃処理を呼ぶ
- Enemyは
AttackTargetを探索して追跡と攻撃を行う EnemySpawnerはEnemy Prefabの生成と生存数だけを管理する- 生成されたEnemyも追跡、攻撃、被ダメージ、死亡破棄まで動作する