using UnityEngine; using UnityEngine.AI; using System.Collections; [RequireComponent(typeof(NavMeshAgent))] public class EnemyAI : MonoBehaviour { // 敵の状態を定義する列挙型(FSM) public enum AIState { Wander, Chase } [Header("現在の状態")] [SerializeField] private AIState currentState = AIState.Wander; [Header("索敵設定")] [Tooltip("プレイヤーを検知する距離")] [SerializeField] private float detectionRange = 10f; [Tooltip("プレイヤーを見失う距離(検知距離より広く設定するのがコツ)")] [SerializeField] private float loseRange = 15f; [Tooltip("プレイヤーのタグ名")] [SerializeField] private string playerTag = "Player"; [Header("徘徊(Wander)設定")] [SerializeField] private float wanderRadius = 8f; [SerializeField] private float minWaitTime = 0.1f; [SerializeField] private float maxWaitTime = 1f; [Tooltip("目的地に向けて移動を開始してから、何秒で諦めるか")] [SerializeField] private float wanderTimeout = 7f; private NavMeshAgent agent; private Transform playerTransform; private bool isWandering = false; void Start() { agent = GetComponent(); // 負荷軽減のため、AIの思考ループをコルーチンで開始(毎フレーム Update で処理しない) StartCoroutine(AILoop()); } // AIの意思決定を行うメインループ(0.2秒ごとに実行して負荷を劇的に下げる) private IEnumerator AILoop() { while (true) { // 1. プレイヤーを探す(まだ見つけていない場合のみ) if (playerTransform == null) { FindPlayer(); } // 2. 現在のステート(状態)に応じて処理を分岐 switch (currentState) { case AIState.Wander: WanderBehavior(); break; case AIState.Chase: ChaseBehavior(); break; } // 毎フレームではなく、0.2秒(1秒に5回)だけ実行する(Quest 3向けの最適化) yield return new WaitForSeconds(0.2f); } } // シーン内からプレイヤー(タグ付き)を探すメソッド private void FindPlayer() { GameObject playerObj = GameObject.FindGameObjectWithTag(playerTag); if (playerObj != null) { playerTransform = playerObj.transform; } } // ─── 【徘徊状態】の行動 ─── 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 IEnumerator WanderMoveRoutine() { isWandering = true; // ランダムな目的地を取得して移動開始 Vector3 newPos = GetRandomNavMeshPoint(transform.position, wanderRadius); agent.SetDestination(newPos); float elapsedTime = 0f; // 経過時間を計るカウンター // 目的地に到着するまで待つ(ただしタイムアウト時間を超えたらループを抜ける) while (agent.remainingDistance > agent.stoppingDistance) { // 0.5秒ごとにチェック yield return new WaitForSeconds(0.5f); elapsedTime += 0.5f; // 【スタック対策】設定した制限時間を超えた場合 if (elapsedTime >= wanderTimeout) { Debug.Log($"⚠️ {gameObject.name}: 目的地({newPos})に到達できないため諦めました。"); agent.ResetPath(); // 動けない経路(バグ目的地)を一旦クリアする break; // whileループを強制終了して次の目的地設定へ進める } } // 到着後(または諦めた後)、ランダムな時間だけその場で待機して次の徘徊へ yield return new WaitForSeconds(Random.Range(minWaitTime, maxWaitTime)); isWandering = false; } // ─── 【追跡状態】の行動 ─── private void ChaseBehavior() { if (playerTransform == null) { currentState = AIState.Wander; return; } float distanceToPlayer = Vector3.Distance(transform.position, playerTransform.position); // 見失う距離(loseRange)を超えたら「徘徊状態」に戻る if (distanceToPlayer > loseRange) { currentState = AIState.Wander; agent.ResetPath(); Debug.Log($"❓ {gameObject.name}: プレイヤーを見失った。徘徊に戻ります。"); return; } // プレイヤーの位置を目的地に設定(0.2秒ごとに更新されるため、滑らかに追従します) agent.SetDestination(playerTransform.position); } // 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; } // 【視覚的なデバッグ用】エディタ上で索敵範囲を線で表示する private void OnDrawGizmosSelected() { // 索敵範囲(赤) Gizmos.color = Color.red; Gizmos.DrawWireSphere(transform.position, detectionRange); // 見失う範囲(黄) Gizmos.color = Color.yellow; Gizmos.DrawWireSphere(transform.position, loseRange); } }