using UnityEngine; using UnityEngine.AI; using System.Collections; using System; [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 bool isKnockbacking = false; private bool isDead = false; private void Start() { agent = GetComponent(); anim = GetComponent(); // AIのメインループコルーチンを開始 StartCoroutine(AILoop()); } private void Update() { if(isDead) return; // 毎フレームのアニメーションパラメータ更新処理 UpdateAnimation(); // 攻撃ステートにおけるプレイヤーへの方向転換処理 UpdateRotationOnAttack(); } #region AI メインループ (思考ロジック) private IEnumerator AILoop() { while (true) { if(isDead) yield break; // プレイヤーが未検出の場合はタグから検索 if (playerTransform == null) { FindPlayer(); } // 攻撃、のけぞりアニメーションの再生中でない場合のみ、ステートに応じた行動を実行 if (!isAttacking && !isKnockbacking) { 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; } // のけぞり中でない場合、追跡の目的地を更新する if(!isKnockbacking) { 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 && !isKnockbacking && 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 && !isKnockbacking) { 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 // ダメージを受けた際に、EnemyhHealthから呼び出されるのけぞり開始メソッド public void TriggerKnockback(float duration) { if(isDead) return; StopAllCoroutines(); isAttacking = false; isWandering = false; isKnockbacking = false; if(agent.isOnNavMesh) { agent.isStopped = false; } StartCoroutine(KnockbackRoutine(duration)); } public void DisableAIOnDeath() { isDead = true; StopAllCoroutines(); if(agent != null && agent.isOnNavMesh) { agent.isStopped = true; agent.ResetPath(); } } private IEnumerator KnockbackRoutine(float duration) { isKnockbacking = true; if(agent.isOnNavMesh) { agent.isStopped = true; agent.ResetPath(); } anim.SetTrigger("Hit"); yield return new WaitForSeconds(duration); if(agent.isOnNavMesh) { agent.isStopped = false; } isKnockbacking = false; isCooldown = false; StartCoroutine(AILoop()); } #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(UnityEngine.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); if(isKnockbacking) yield break; 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 (isKnockbacking || 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() { if(isKnockbacking) return; 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 = UnityEngine.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 }