Appearance
第04回 攻撃処理と生成
前回: 第03回 衝突判定とダメージ / 次回: 第05回 敵AIとゲームフィール調整
前回の振り返り
Playerの直接攻撃を作りました

PlayerAttackで攻撃中だけ Collider を有効に して、Trigger に入った相手を検出しましたDamageReceiverで、攻撃を受けた時にダメージを受ける処理を作りましたAttackResolverで攻撃者と被弾者の関係を 判断し、ゲーム仕様に沿ったダメージの解決を集中管理する形としました- 今回はこれらを Enemy も使えるようにし、 Player と戦闘できるようにします
今回のテーマ
攻撃能力のある敵を生成する
攻撃処理を共通化 ついでに追跡も
今回作るもの
攻撃対象を探して攻撃するEnemy
- 前回作った
PlayerAttackを、 Player専用ではない攻撃コンポーネントに 共通化します - Enemyは攻撃対象へ近づき、攻撃できる距離で 停止、一定間隔で攻撃します
- これらの機能を実装したEnemyをPrefab化し、 Spawnerから複数生成します
text
Player
Rigidbody
Player
PlayerController
PlayerMove
DamageReceiver
AttackTarget
Player Attack
CharacterAttack
Enemy Prefab
Rigidbody
Enemy(03回で作成済み)
EnemyController
DamageReceiver
Enemy Attack Range
CharacterAttack
EnemySpawner
Enemy Prefab を生成
追跡対象はEnemy自身が探す今回の到達目標
攻撃処理の共通化と敵の追跡攻撃
PlayerAttackをCharacterAttackとして共通化します- Playerの攻撃入力から
CharacterAttack.Attack()を呼びます - Enemyにも
CharacterAttackを持たせ、同じ攻撃処理を使います - Enemyが攻撃対象を探し、対象へ向かって移動する最低限の追跡処理を作ります
- Enemyは攻撃距離に入ると止まり、一定間隔で攻撃します
- Enemy Prefabを
EnemySpawnerから生成します - Spawnerは生成だけを担当し、追跡対象の決定はEnemy側に任せます
攻撃処理の共通化
あえて Player 専用とする理由がない?
- 前回の
PlayerAttackは、Playerが使う前提で名前を付けました - しかし中身を見ると、処理の多くはPlayer専用ではありません 攻撃時間、当たり判定、命中済みリスト、ダメージ解決は Enemyの攻撃にもそのまま使えます
- Player専用ではない処理なら、名前と参照の持ち方を整理して、 PlayerとEnemyの両方から使える部品にした方が自然かもしれません
- 共通化が常に最適とは限りませんが 今回は
PlayerAttackをCharacterAttackとして共通化します
CharacterAttack の実装
攻撃判定だけを扱う
CharacterAttackは、攻撃中だけ 攻撃用Colliderを有効にし、Triggerに入ったDamageReceiverを検出します- 命中対象を控え、同じ攻撃で同じ相手に 何度もダメージを与えないようにします
- ダメージ処理の実際は
AttackResolverに 任せ、自身は攻撃のタイミングと当たり判定に 集中します
text
CharacterAttack
攻撃時間を管理する
攻撃判定ColliderをON/OFFする
命中したDamageReceiverを探す
AttackResolverへ渡す
CharacterAttackが直接やらないこと
HP計算
死亡処理
追跡AI
生成処理注意: nullチェックについて
必須の依存を防衛的に握りつぶさない
attackColliderはCharacterAttackが動くための必須依存です ここをif (attackCollider != null)で黙って飛ばすと、 Inspector での設定漏れという本質的な問題を隠してしまいます- ここに限らず 必須依存は設定し忘れた時に早く気づける形にし、 null チェックでのスルーは必要な時だけに行うよう心掛けてください
- nullチェック自体が悪いわけではありません 例えば
OnTriggerEnterで触れた相手がDamageReceiverを持っていないことは あり得るため、その場合はreturnして構いません
名前を変える時の注意
ファイル名とクラス名を揃える
- Unityの
MonoBehaviourを扱うクラス管理では、 ファイル名とクラス名を一致させるのが基本です - クラス名
PlayerAttackをCharacterAttackに変更する場合、 ファイル名もCharacterAttack.csに変更します - Player側は、
PlayerControllerの参照先をPlayerAttackからCharacterAttackに差し替えます - すでにInspectorで
PlayerAttackを参照している場合、 変更後に参照が外れていないかを確認してください
CharacterAttack.cs (1/5)
前回のPlayerAttackを共通化する
- Player 専用から Character 共通に変更され ますが、ほとんどの処理は前回と同じです
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;
private readonly List<DamageReceiver> _hitReceivers = new();CharacterAttack.cs (2/5)
攻撃状態を保持する
IsAttackingは、自身が攻撃中かどうかを 表すプロパティです_isHitActiveは、攻撃判定Colliderが 有効になっているかを表します_attackElapsedTimeは、攻撃開始からの 経過時間を数えますAwakeでは、攻撃判定Colliderを最初は 無効にしておきます
cs
private bool _isAttacking;
private bool _isHitActive;
private float _attackElapsedTime;
public bool IsAttacking => _isAttacking;
private void Awake()
{
attackCollider.enabled = false;
}
private void Update()
{
if (!_isAttacking)
return;
_attackElapsedTime += Time.deltaTime;CharacterAttack.cs (3/5)
攻撃時間の更新と攻撃開始
CharacterAttack.Attack()で攻撃を開始します- 攻撃中だけ
_attackElapsedTimeを進めます hitStartTimeで当たり判定を有効にし、hitEndTimeで無効にしますattackDurationを過ぎたら攻撃を終了しますattackEffectは演出用なので、設定されていなくても 攻撃処理自体は成立します
cs
if (!_isHitActive &&
_attackElapsedTime >= hitStartTime)
StartHit();
if (_isHitActive &&
_attackElapsedTime >= hitEndTime)
EndHit();
if (_attackElapsedTime >= attackDuration)
EndAttack();
}
public void Attack()
{
if (_isAttacking)
return;
attackEffect?.Play();
_hitReceivers.Clear();
_isAttacking = true;
_isHitActive = false;
_attackElapsedTime = 0f;
}CharacterAttack.cs (4/5)
ダメージはAttackResolverが解決
- 1回の攻撃で、同じ相手へ何度もダメージを 与えないようにします
- 攻撃者とダメージ量を
AttackContextに まとめ、AttackResolver.Resolve()で ダメージ処理を行います - 自分自身への攻撃除外なども
AttackResolver側で判断します
cs
private void OnTriggerEnter(Collider other)
{
var receiver =
other.GetComponentInParent<DamageReceiver>();
if (receiver == null)
return;
if (_hitReceivers.Contains(receiver))
return;
_hitReceivers.Add(receiver);
var attackContext =
new AttackContext(gameObject, damage);
AttackResolver.Resolve(attackContext, receiver);
}AttackResolverの更新
Enemy同士の相打ちを防ぐ
- 03回の
AttackResolverは、自分自身への ダメージだけを除外していました - 今回はEnemyも攻撃するため、そのままだと Enemyの攻撃が別のEnemyにも当たってしまいます
- 既存の
Playerと、03回で作ったEnemyクラスを使い、 同じ種類の相手にはダメージを与えないようにします Resolve内に味方同士の判定を追加し、IsSameSideはAttackResolverクラス内の別メソッドとして定義します
cs
public static class AttackResolver
{
public static void Resolve(
AttackContext attackContext,
DamageReceiver receiver)
{
if (receiver.transform ==
attackContext.Attacker.transform ||
receiver.transform.IsChildOf(
attackContext.Attacker.transform))
return;
if (IsSameSide(
attackContext.Attacker,
receiver.gameObject))
return;
if (!receiver.CanReceiveDamage)
return;
receiver.ReceiveDamage(attackContext.Damage);
}
private static bool IsSameSide(
GameObject attacker,
GameObject receiver)
{
if (attacker.GetComponentInParent<Player>() != null &&
receiver.GetComponentInParent<Player>() != null)
return true;
if (attacker.GetComponentInParent<Enemy>() != null &&
receiver.GetComponentInParent<Enemy>() != null)
return true;
return false;
}
}CharacterAttack.cs (5/5)
攻撃判定をON/OFFする
StartHitは、攻撃判定を有効にEndHitは、攻撃判定を無効にしますEndAttackでは攻撃状態を解除し 判定も必ず停止します- 個々の変数を直接操作せず、必要な操作を メソッドにまとめることで、状態の不整合を 防ぎましょう
cs
private void EndAttack()
{
_isAttacking = false;
EndHit();
}
private void StartHit()
{
_isHitActive = true;
attackCollider.enabled = true;
}
private void EndHit()
{
_isHitActive = false;
attackCollider.enabled = false;
}
}Player側の変更
PlayerControllerからCharacterAttackを呼ぶ
- Playerの操作入力は、これまで通り
PlayerControllerが受け取ります - 攻撃ボタンが押されたら、
CharacterAttack.Attack()を呼びます - 変更点は、参照する型が
PlayerAttackからCharacterAttackになることだけです - Playerの子Objectにある攻撃判定Colliderの 設定も前回と同じです
cs
public class PlayerController : MonoBehaviour
{
[SerializeField] private CharacterAttack characterAttack;
private void Update()
{
// 省略: 移動とカメラ操作
if (_inputActions.Player.Attack.WasPressedThisFrame())
characterAttack.Attack();
}
}攻撃対象の見つけ方
Playerを直接参照しない
- Enemyが
Player型を直接参照すると、 EnemyはPlayerに強く依存することになってしまいます - 今回Enemyに必要なのは、相手がPlayerかどうかではなく、 相手を攻撃対象として扱ってよいか と 対象がどこにいるか の情報です
- そこで、攻撃対象にできるObjectへ
AttackTargetという目印Componentを付けます - EnemyはScene内の
AttackTargetを探し、 近い対象を追跡、攻撃します
AttackTarget.cs
攻撃対象であることを示す目印
AttackTargetは処理をほとんど持たない マーカーComponentです- PlayerにこのComponentを付けると、 Enemyから攻撃対象として見つけられるように なります
- テスト用のCubeに
AttackTargetを付ければ、 PlayerなしでもEnemyの追跡と攻撃の挙動を 確認できます
cs
using UnityEngine;
public class AttackTarget : MonoBehaviour
{
// 目印以外の処理は特に持ちません
}EnemyControllerの実装方針
探す、近づく、攻撃する
EnemyControllerは、攻撃対象の探索、移動、攻撃判断を担当します- 攻撃対象が遠い間は近づき、 攻撃距離に入ったら止まって攻撃します
- 実際の当たり判定とダメージ処理は、 既に作った
CharacterAttackに任せます
EnemyControllerの処理
Update内でシンプルに動かす
Updateでは、攻撃対象の探索と攻撃判断を行います 移動すると決まったら、transform.positionを少しずつ動かします- 今回のEnemyは、物理挙動ではなく直接座標を操作して移動します 移動処理のために
Rigidbodyは使いません - ただし、そのままでは敵同士がめり込むため、 Enemy本体に
Rigidbodyを付けます Rigidbodyは敵同士を押し戻すために使い、 EnemyControllerから速度を直接操作しません
EnemyController.cs (1/4)
必要な参照と設定値
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;EnemyController.cs (2/4)
Updateで行動を判断する
- 目標が無ければ
FindNearestTargetで探します - 見つからない時は、そのフレームでは移動しません
- 攻撃中も移動処理へ進みません
cs
private void Update()
{
if (_target == null)
_target = FindNearestTarget();
if (_target == null)
return;
if (characterAttack.IsAttacking)
return;EnemyController.cs (3/4)
距離を見て攻撃する
- Y方向は無視し、X-Z平面上で近づきます
- 攻撃距離に入ったら移動せず、攻撃を試みます
- まだ遠い場合は、次のスライドで移動方向を決めます
cs
var toTarget =
_target.transform.position - transform.position;
toTarget.y = 0f;
if (toTarget.magnitude <= attackDistance)
{
TryAttack();
return;
}EnemyController.cs (4/4)
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;
}EnemyController.cs 補足
対象探索
FindNearestTargetはScene内のAttackTargetを調べますsearchRadiusより近い対象の中から 最も近いものを選びます やや非効率な処理ですが、ここでは簡単のためFindObjectsByTypeを使用しています
cs
private AttackTarget FindNearestTarget()
{
var targets =
FindObjectsByType<AttackTarget>();
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;
}
}Player Prefabの Inspector

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

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

- 今回のEnemy移動は
transform.positionで 行うため、移動処理のためにRigidbodyは 使いません - ただし敵同士が互いにめり込むのを避けるため 今回はEnemy本体に
Rigidbodyを付けます X/Z方向は固定せず、物理的に押し戻せる余地を残します
EnemyのRigidbody設定の注意
Enemyが少しずつ滑っていく
- デフォルトの設定では、衝突で生まれた速度が残り、 Enemyが少しずつ滑っていくことがあります
- これを防ぐために、
Linear Dampingを大きめに設定して 速度を減衰させるのがおすすめです UnityのバージョンによってはDragという名前で表示されます - 03回の
RigidbodyはTrigger判定を成立させるためでしたが、 今回は敵同士を押し戻すために使用しています
Prefab と Instance
設計図と実体
- Prefab は、GameObjectの構成を保存した設計図です 関連するオブジェクトとComponentの構成をまとめて保存できます
- Instance は、Scene上に存在する実体です Prefabから複製され、位置や状態を個別に持ちます
- Enemyのように同じ構成のObjectを何度も使う場合、 Prefabを1つ作っておき、必要なタイミングでInstanceを生成します
- 今回の Spawner は敵の生成しか扱いませんが、 何らかの設定値を付与するのに好適な場所です
Spawn Point
生成位置を表す空のGameObject
- 敵を出したい場所に空のGameObjectを置き、
Spawn Pointとして使います - 座標をコードに直接書くのではなく、Scene上で位置を設定するのがおすすめです 動作的にはやや非効率ですが、ゲームバランスの調整がしやすくなります
- ゲームバランスに関わる位置は、なるべくInspectorやScene上で 調整できる形にして、ゲームエンジンUnityの強みを活かす判断は 決して間違いではありません
- 今回は複数のSpawn Pointを用意し、 そのうちのランダムな場所から敵を出しています
EnemySpawner.cs (1/3)
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;EnemySpawner.cs (2/3)
一定間隔で生成を試みる
Updateで経過時間を数え、指定間隔でSpawnを呼びます- 破棄済みの敵がListに残っている場合は 先に取り除きます
- 最大数に達している場合、それ以上は生成 しません
cs
private void Update()
{
CleanupDestroyedEnemies();
_spawnTimer += Time.deltaTime;
if (_spawnTimer < spawnInterval)
return;
_spawnTimer = 0f;
Spawn();
}EnemySpawner.cs (3/3)
生成した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);
}破棄済みの敵をListから外す
Destroyされた参照を取り除く
- 生成した敵を List に控えて生成した敵の数を 管理していますが
DestroyされたObjectは Listから 自動で削除されるわけではありません - そのままでは敵が減っていても
_aliveEnemies.Countが減らず、生成上限に 達したままになります RemoveAllを使い、条件に合う要素をまとめて Listから取り除きますenemy => enemy == nullは 「enemy が null なら対象」という条件です
cs
private void CleanupDestroyedEnemies()
{
_aliveEnemies.RemoveAll(enemy => enemy == null);
}実習1
PlayerAttackをCharacterAttackへ変更する
PlayerAttack.csをCharacterAttack.csに変更 クラス名もCharacterAttackに変更- Playerの攻撃判定Colliderを
CharacterAttackに設定 PlayerControllerの参照をCharacterAttackに変更- 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をPrefab化します
Enemy Spawnerを作り、EnemySpawnerを追加しますEnemy Prefabに、ProjectビューのEnemy Prefabを設定しますSpawn Points配列へ、Scene上のSpawn Pointを登録します- 生成されたEnemyが
AttackTargetへ近づき、攻撃するか確認します
よくあるエラー (1/2)
Enemyが追いかけてこない
- Playerに
AttackTargetが付いているか確認します - Enemyの
Search Radiusが小さすぎないか確認します - Enemyに
EnemyControllerが付いているか確認します moveSpeedが小さすぎないか確認します
よくあるエラー (2/2)
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で共有できる
- Player専用だった
PlayerAttackをCharacterAttackに共通化しました - Playerは入力から
CharacterAttack.Attack()を呼びます - Enemyは
EnemyControllerからCharacterAttack.Attack()を呼びます - Enemyは
AttackTargetを探して、攻撃対象を自己判断します - EnemySpawnerはPrefabの生成だけを担当します
- 生成されたEnemyも、追跡、攻撃、被ダメージ、死亡破棄まで動きます
今日の完成状態
攻撃する敵がゲーム中に増える
- Playerは前回と同じ直接攻撃を使えます
- Enemyは
AttackTargetへ近づき、攻撃距離で止まります - Enemyは一定間隔で攻撃対象へ攻撃できます
- EnemySpawnerがEnemy Prefabを一定間隔で生成します
- Playerの直接攻撃で、生成されたEnemyにもダメージを与えられます
- HPが0になったEnemyはSceneから破棄されます
AttackTarget.cs
cs
using UnityEngine;
// 攻撃対象であることを示す目印
public class AttackTarget : MonoBehaviour
{
// 目印以外の処理は特に持ちません
}EnemyController.cs (1/2)
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;cs
private void Update()
{
if (_target == null)
_target = FindNearestTarget();
if (_target == null)
return;
if (characterAttack.IsAttacking)
return;
var toTarget =
_target.transform.position - transform.position;
toTarget.y = 0f;
if (toTarget.magnitude <= attackDistance)
{
TryAttack();
return;
}EnemyController.cs (2/2)
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;
}cs
private AttackTarget FindNearestTarget()
{
var targets = FindObjectsByType<AttackTarget>();
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;
}
}EnemySpawner.cs
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;
private void Update()
{
CleanupDestroyedEnemies();
_spawnTimer += Time.deltaTime;
if (_spawnTimer < spawnInterval)
return;
_spawnTimer = 0f;
Spawn();
}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);
}
private void CleanupDestroyedEnemies()
{
_aliveEnemies.RemoveAll(enemy => enemy == null);
}
}余裕があれば
自由に改善してみましょう
- 敵の攻撃とプレイヤーの攻撃を差別化してみましょう 色を変える、速度を変える、範囲を変えるなどから始めるのがおすすめです
- プレイヤーの攻撃中の動作を止めてみましょう 現在は攻撃中に移動できてしまい、的が定めづらいかもしれません 完全に停止させるのではなく、減速するのも良いでしょう
- プレイヤーの攻撃受け付けに先行入力を入れてみましょう 攻撃の終端に入力がされた場合はすかさず次の攻撃に移ると プレイヤーの操作感が良くなります
次回へ向けて
敵AIとゲームフィールを調整する
- 今回は、Enemyが
AttackTargetを探して攻撃する最低限の Bot の動作を作りました - 次回は、攻撃距離、攻撃間隔、移動速度、HP、ダメージ量に加えて、 被弾時ののけぞりと攻撃中断を入れます
- 敵AIの調整とゲームフィールの改善を行い、 プレイヤーが戦っていて手応えを感じられるようにします