Files
VRProject/Assets/Scripts/EnemyAI.cs
2026-05-28 16:02:53 +09:00

388 lines
14 KiB
C#
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
using UnityEngine;
using UnityEngine.AI;
using System.Collections;
[RequireComponent(typeof(NavMeshAgent))]
[RequireComponent(typeof(Animator))]
public class EnemyAI : MonoBehaviour
{
/// 敵AIの状態を表すステート
public enum AIState
{
Wander, // 徘徊
Chase, // 追跡
Attack // 攻撃
}
[Header("現在の状態")]
[SerializeField] private AIState currentState = AIState.Wander;
[Header("索敵設定")]
[SerializeField] private float detectionRange = 10f;
[SerializeField] private float loseRange = 15f;
[SerializeField] private string playerTag = "Player";
[Header("攻撃設定")]
[Tooltip("この距離に近づいたら攻撃 (メートル)")]
[SerializeField] private float attackRange = 1.5f;
[Tooltip("パンチの攻撃力")]
[SerializeField] private float attackDamage = 10f;
[Tooltip("一度攻撃が終了してから、次の攻撃を開始するまでの待ち時間 (秒)")]
[SerializeField] private float attackCooldown = 2.0f;
[Tooltip("攻撃の予兆時間 (秒)")]
[SerializeField] private float telegraphDuration = 0.6f;
[Header("ループ攻撃の調整")]
[Tooltip("連続攻撃の間に挟む最低限のインターバル (秒)")]
[SerializeField] private float comboInterval = 0.2f;
[Header("徘徊Wander設定")]
[SerializeField] private float wanderRadius = 8f;
[SerializeField] private float minWaitTime = 2f;
[SerializeField] private float maxWaitTime = 4f;
[SerializeField] private float wanderTimeout = 7f;
private NavMeshAgent agent;
private Animator anim;
private Transform playerTransform;
private bool isCooldown = false;
private bool isAttacking = false;
private bool isWandering = false;
private void Start()
{
agent = GetComponent<NavMeshAgent>();
anim = GetComponent<Animator>();
// AIのメインループコルーチンを開始
StartCoroutine(AILoop());
}
private void Update()
{
// 毎フレームのアニメーションパラメータ更新処理
UpdateAnimation();
// 攻撃ステートにおけるプレイヤーへの方向転換処理
UpdateRotationOnAttack();
}
#region AI ()
private IEnumerator AILoop()
{
while (true)
{
// プレイヤーが未検出の場合はタグから検索
if (playerTransform == null)
{
FindPlayer();
}
// 攻撃アニメーションの再生中でない場合のみ、ステートに応じた行動を実行
if (!isAttacking)
{
switch (currentState)
{
case AIState.Wander:
WanderBehavior();
break;
case AIState.Chase:
ChaseBehavior();
break;
case AIState.Attack:
AttackBehavior();
break;
}
}
// 毎フレーム実行を避け、0.2秒待機することでCPU負荷を軽減
yield return new WaitForSeconds(0.2f);
}
}
private void FindPlayer()
{
GameObject playerObj = GameObject.FindGameObjectWithTag(playerTag);
if (playerObj != null)
{
playerTransform = playerObj.transform;
}
}
#endregion
#region
/// 【徘徊状態】の思考ロジック
private void WanderBehavior()
{
if (playerTransform != null)
{
// プレイヤーとの距離を測定し、索敵範囲内なら追跡ステートへ遷移
float distanceToPlayer = Vector3.Distance(transform.position, playerTransform.position);
if (distanceToPlayer <= detectionRange)
{
currentState = AIState.Chase;
isWandering = false;
agent.ResetPath();
Debug.Log($"👁️ {gameObject.name}: プレイヤーを発見! 追跡します。");
return;
}
}
// すでに徘徊移動中でなければ、新しい目的地への移動コルーチンを開始
if (!isWandering)
{
StartCoroutine(WanderMoveRoutine());
}
}
/// 【追跡状態】の思考ロジック
private void ChaseBehavior()
{
if (playerTransform == null)
{
currentState = AIState.Wander;
return;
}
float distanceToPlayer = Vector3.Distance(transform.position, playerTransform.position);
// 攻撃射程内に入ったら攻撃ステートへ遷移し、その場に停止
if (distanceToPlayer <= attackRange)
{
currentState = AIState.Attack;
agent.ResetPath();
return;
}
// プレイヤーが見失う範囲を超えたら徘徊ステートにリセット
if (distanceToPlayer > loseRange)
{
currentState = AIState.Wander;
agent.ResetPath();
Debug.Log($"❓ {gameObject.name}: プレイヤーを見失った。");
return;
}
// クールダウン中であっても、追跡ステートである限りはプレイヤーの現在位置を追い続ける
agent.SetDestination(playerTransform.position);
}
/// 【攻撃状態】の思考ロジック
private void AttackBehavior()
{
if (playerTransform == null)
{
currentState = AIState.Wander;
return;
}
float distanceToPlayer = Vector3.Distance(transform.position, playerTransform.position);
// 攻撃射程から外れていたら、再び追跡ステートに戻る
if (distanceToPlayer > attackRange)
{
currentState = AIState.Chase;
return;
}
// クールダウン中でなければ攻撃コルーチンを実行クールダウン中の場合は、Update内の処理によりプレイヤーの方向を向きながら待機します
if (!isCooldown)
{
StartCoroutine(PerformPunchAttack());
}
}
#endregion
#region
/// エージェントの移動状態に応じてAnimatorのSpeedパラメータを更新する
private void UpdateAnimation()
{
float targetSpeed = 0f;
// 攻撃中でなく、経路が存在し、かつ実際に一定以上の速度で移動している場合のみSpeedを1にする
if (!isAttacking && agent.hasPath && agent.velocity.magnitude > 0.1f)
{
targetSpeed = 1f;
}
// パラメータをなめらかに変化させて渡す
anim.SetFloat("Speed", targetSpeed, 0.1f, Time.deltaTime);
}
/// 攻撃ステートの際、プレイヤーの方向へ滑らかに旋回させる
private void UpdateRotationOnAttack()
{
if (currentState == AIState.Attack && playerTransform != null)
{
Vector3 direction = (playerTransform.position - transform.position).normalized;
direction.y = 0; // 上下方向の傾き(ピッチ回転)を無視
if (direction != Vector3.zero)
{
// Y軸の回転のみを滑らかに補間して適用
transform.rotation = Quaternion.Slerp(transform.rotation, Quaternion.LookRotation(direction), Time.deltaTime * 10f);
}
}
}
#endregion
#region
/// 徘徊時のランダム移動を制御するコルーチン
private IEnumerator WanderMoveRoutine()
{
isWandering = true;
Vector3 newPos = Vector3.zero;
bool isPathValid = false;
int maxAttempts = 10;
// 有効な経路(壁などで分断されていないルート)が見つかるまで試行
for (int i = 0; i < maxAttempts; i++)
{
newPos = GetRandomNavMeshPoint(transform.position, wanderRadius);
NavMeshPath path = new NavMeshPath();
if (agent.CalculatePath(newPos, path))
{
if (path.status == NavMeshPathStatus.PathComplete)
{
isPathValid = true;
break;
}
}
}
// 有効な目的地が設定できた場合の移動処理
if (isPathValid)
{
agent.SetDestination(newPos);
float elapsedTime = 0f;
// 目的地に到達するか、タイムアウト時間を迎えるまで待機
while (agent.remainingDistance > agent.stoppingDistance)
{
yield return new WaitForSeconds(0.5f);
elapsedTime += 0.5f;
// スタック対策としてのタイムアウト判定
if (elapsedTime >= wanderTimeout)
{
agent.ResetPath();
break;
}
}
}
// 到着または諦めた後、ランダムな時間その場で待機
yield return new WaitForSeconds(Random.Range(minWaitTime, maxWaitTime));
isWandering = false;
}
/// 予兆から攻撃アニメーションのトリガーまでを制御するコルーチン
private IEnumerator PerformPunchAttack()
{
isAttacking = true;
isCooldown = true; // ここで攻撃フラグをロックします
anim.SetTrigger("Telegraph");
Debug.Log($"⚠️ {gameObject.name}: 攻撃の予兆(溜め)開始");
yield return new WaitForSeconds(telegraphDuration);
anim.SetTrigger("Attack");
}
/// 連続攻撃成功時の、クイックなクールダウン解除処理
private IEnumerator ResetCooldownQuickly()
{
yield return new WaitForSeconds(comboInterval);
isCooldown = false;
}
/// コンボが途切れた際の、通常の攻撃クールダウン解除処理
private IEnumerator ResetCooldown()
{
// 指定された秒数待機する
yield return new WaitForSeconds(attackCooldown);
isCooldown = false;
Debug.Log($"✅ {gameObject.name}: 攻撃クールダウンが終了しました。再攻撃可能です。");
}
#endregion
#region
/// 攻撃アニメーションのヒットフレームで実行されるヒット判定Animation Eventから呼び出し
public void OnPunchHit()
{
if (playerTransform == null) return;
float distanceToPlayer = Vector3.Distance(transform.position, playerTransform.position);
// ヒット判定の瞬間、プレイヤーが射程内に残っているか判定猶予として0.3m加算)
if (distanceToPlayer <= attackRange + 0.3f)
{
Debug.Log($"💥 敵のパンチがヒット!");
}
}
/// Recovery硬直アニメーションの終了時に実行される処理Animation Eventから呼び出し
public void OnRecoveryEnd()
{
isAttacking = false;
if (playerTransform == null)
{
currentState = AIState.Wander;
isCooldown = false;
return;
}
float distanceToPlayer = Vector3.Distance(transform.position, playerTransform.position);
// ループ判定:まだプレイヤーが射程内にいる場合はクイックな連続攻撃へ移行
if (distanceToPlayer <= attackRange)
{
StartCoroutine(ResetCooldownQuickly());
Debug.Log($"🔄 {gameObject.name}: プレイヤーがまだ射程内です。連続攻撃(コンボ)を実行します。");
}
else
{
// プレイヤーが離れていた場合はコンボを抜け、追跡状態に戻ると同時に、通常のクールダウン(長めの待機)を開始
currentState = AIState.Chase;
StartCoroutine(ResetCooldown());
Debug.Log($"🏃 {gameObject.name}: プレイヤーが離れたため追跡に戻ります。({attackCooldown}秒の攻撃クールダウンを適用)");
}
}
#endregion
#region
/// 指定された中心座標の範囲内から、NavMesh上の有効なランダム座標を取得する
private Vector3 GetRandomNavMeshPoint(Vector3 center, float radius)
{
Vector3 randomDirection = Random.insideUnitSphere * radius;
randomDirection += center;
NavMeshHit hit;
if (NavMesh.SamplePosition(randomDirection, out hit, radius, NavMesh.AllAreas))
{
return hit.position;
}
return center;
}
/// エディタのSceneビューに索敵・攻撃範囲をギズモとして描画する
private void OnDrawGizmosSelected()
{
// 索敵範囲(赤)
Gizmos.color = Color.red;
Gizmos.DrawWireSphere(transform.position, detectionRange);
// 見失う範囲(黄)
Gizmos.color = Color.yellow;
Gizmos.DrawWireSphere(transform.position, loseRange);
// 攻撃範囲(青)
Gizmos.color = Color.blue;
Gizmos.DrawWireSphere(transform.position, attackRange);
}
#endregion
}