using UnityEngine; namespace IndianOceanAssets.Engine2_5D { /// /// 敌人AI v2 —— 双重寻敌机制(视觉检测 + 铃铛响应) /// /// 状态流程: /// - Idle:待机。射线检测到玩家 → ChasingPlayer;收到铃铛信号 → ChasingBell /// - ChasingPlayer:追击玩家。持续看到玩家则追击;脱离视线3秒后 → Idle /// - ChasingBell:追击铃铛。追击过程中看到玩家 → ChasingPlayer;到达铃铛位置 → PatrollingBell /// - PatrollingBell:铃铛位置巡逻。巡逻过程中看到玩家 → ChasingPlayer;巡逻时间结束 → Idle /// /// 挂载位置:敌人预制体上 /// 需要:场景中有 EchoSystem(用于摇铃事件)、玩家物体带 "Player" 标签 /// (或手动把玩家拖到 playerTarget 字段) /// public class EnemyAI : MonoBehaviour { [Header("目标")] [Tooltip("玩家目标(留空则自动查找 Player 标签)")] [SerializeField] private Transform playerTarget; [Header("检测与追击")] [Tooltip("靠近主角到此距离时停下")] [SerializeField] private float stopDistance = 1.2f; [Tooltip("追击速度")] [SerializeField] private float chaseSpeed = 3f; [Header("视觉检测")] [Tooltip("射线检测层(哪些物体会阻挡视线)")] [SerializeField] private LayerMask obstacleLayers; [Tooltip("射线起点偏移(避免从地面发射)")] [SerializeField] private float rayOffsetY = 0.5f; [Header("脱离计时")] [Tooltip("玩家脱离视线后,继续追击的时间(秒)")] [SerializeField] private float loseSightTime = 3f; [Header("聆听(摇铃响应)")] [Tooltip("聆听范围:主角在此范围内按E摇铃时,敌人会朝铃铛位置移动")] [SerializeField] private float listenRange = 15f; [Header("铃铛追击")] [Tooltip("到达铃铛位置后的容差距离")] [SerializeField] private float bellArriveDistance = 1f; [Tooltip("追击铃铛的最大时间(超时则放弃)")] [SerializeField] private float bellChaseTimeout = 10f; [Header("铃铛巡逻")] [Tooltip("到达铃铛位置后的巡逻时间")] [SerializeField] private float bellPatrolTime = 5f; [Tooltip("巡逻半径")] [SerializeField] private float bellPatrolRadius = 3f; // 状态枚举 private enum State { Idle, // 待机 ChasingPlayer, // 追击玩家 ChasingBell, // 追击铃铛 PatrollingBell // 铃铛位置巡逻 } private State _state = State.Idle; // 脱离视线计时器 private float _loseSightTimer; // 铃铛相关 private Vector3 _bellTargetPosition; // 铃铛目标位置 private float _bellChaseTimer; // 铃铛追击计时器 // 巡逻相关 private Vector3 _patrolCenter; // 巡逻中心点 private float _patrolTimer; // 巡逻计时器 private Vector3 _patrolTarget; // 当前巡逻目标点 private void Start() { if (playerTarget == null) { var player = GameObject.FindWithTag("Player"); if (player != null) playerTarget = player.transform; } } private void OnEnable() { EchoSystem.OnEchoReleased += OnBell; } private void OnDisable() { EchoSystem.OnEchoReleased -= OnBell; } private void Update() { switch (_state) { case State.Idle: UpdateIdle(); break; case State.ChasingPlayer: UpdateChasingPlayer(); break; case State.ChasingBell: UpdateChasingBell(); break; case State.PatrollingBell: UpdatePatrollingBell(); break; } } // ===== Idle:待机,等待检测 ===== private void UpdateIdle() { // 检测是否看到玩家 if (CanSeePlayer()) { _state = State.ChasingPlayer; _loseSightTimer = 0f; return; } } // ===== ChasingPlayer:追击玩家 ===== private void UpdateChasingPlayer() { // 检测是否看到玩家 if (CanSeePlayer()) { _loseSightTimer = 0f; // 重置计时器 // 追击玩家 float dist = XZDistance(transform.position, playerTarget.position); if (dist > stopDistance) { MoveToward(playerTarget.position, chaseSpeed); } } else { // 玩家脱离视线,开始计时 _loseSightTimer += Time.deltaTime; // 继续朝玩家最后已知位置移动 if (playerTarget != null) { float dist = XZDistance(transform.position, playerTarget.position); if (dist > stopDistance) { MoveToward(playerTarget.position, chaseSpeed); } } // 超过设定时间,停止追击 if (_loseSightTimer >= loseSightTime) { _state = State.Idle; } } } // ===== ChasingBell:追击铃铛 ===== private void UpdateChasingBell() { // 追击过程中检测是否看到玩家 if (CanSeePlayer()) { _state = State.ChasingPlayer; _loseSightTimer = 0f; return; } // 追击计时 _bellChaseTimer += Time.deltaTime; if (_bellChaseTimer >= bellChaseTimeout) { _state = State.Idle; return; } // 检测是否到达铃铛位置 float dist = XZDistance(transform.position, _bellTargetPosition); if (dist <= bellArriveDistance) { // 到达铃铛位置,开始巡逻 _state = State.PatrollingBell; _patrolCenter = _bellTargetPosition; _patrolTimer = 0f; PickNewPatrolPoint(); return; } // 朝铃铛位置移动 MoveToward(_bellTargetPosition, chaseSpeed); } // ===== PatrollingBell:铃铛位置巡逻 ===== private void UpdatePatrollingBell() { // 巡逻过程中检测是否看到玩家 if (CanSeePlayer()) { _state = State.ChasingPlayer; _loseSightTimer = 0f; return; } // 巡逻计时 _patrolTimer += Time.deltaTime; if (_patrolTimer >= bellPatrolTime) { _state = State.Idle; return; } // 朝巡逻点移动 float dist = XZDistance(transform.position, _patrolTarget); if (dist <= 0.5f) { // 到达巡逻点,选择新的巡逻点 PickNewPatrolPoint(); } else { MoveToward(_patrolTarget, chaseSpeed * 0.7f); // 巡逻时速度稍慢 } } // ===== 摇铃事件回调(由 EchoSystem.OnEchoReleased 触发)===== private void OnBell(Vector3 bellPos) { if (playerTarget == null) return; // 只有在聆听范围内才响应 if (XZDistance(transform.position, bellPos) > listenRange) return; // 设置铃铛目标位置 _bellTargetPosition = bellPos; _bellChaseTimer = 0f; // 切换到追击铃铛状态(可以打断任何状态) _state = State.ChasingBell; } /// /// 选择新的巡逻点 /// private void PickNewPatrolPoint() { // 在巡逻中心周围随机选择一个点 Vector2 randomDir = Random.insideUnitCircle * bellPatrolRadius; _patrolTarget = _patrolCenter + new Vector3(randomDir.x, 0f, randomDir.y); } /// /// 检测是否能看到玩家(射线检测) /// private bool CanSeePlayer() { if (playerTarget == null) return false; // 射线起点(稍微抬高,避免从地面发射) Vector3 rayStart = transform.position + Vector3.up * rayOffsetY; Vector3 rayEnd = playerTarget.position + Vector3.up * rayOffsetY; Vector3 dir = rayEnd - rayStart; float dist = dir.magnitude; // 调试:在Scene视图中绘制射线 Debug.DrawLine(rayStart, rayEnd, CanSeePlayerDebugColor()); // 发射射线,只检测障碍物层 if (Physics.Raycast(rayStart, dir.normalized, out RaycastHit hit, dist, obstacleLayers)) { // 击中了物体 Debug.DrawLine(rayStart, hit.point, Color.red); // 如果击中的是玩家,说明可以看到 if (hit.transform == playerTarget) { return true; } // 击中了其他物体(被遮挡) return false; } // 没有击中任何物体 = 没有遮挡 = 可以看到玩家 return true; } /// /// 调试用:根据是否能看到玩家返回不同颜色 /// private Color CanSeePlayerDebugColor() { if (playerTarget == null) return Color.gray; Vector3 rayStart = transform.position + Vector3.up * rayOffsetY; Vector3 rayEnd = playerTarget.position + Vector3.up * rayOffsetY; Vector3 dir = rayEnd - rayStart; float dist = dir.magnitude; if (Physics.Raycast(rayStart, dir.normalized, out RaycastHit hit, dist, obstacleLayers)) { return hit.transform == playerTarget ? Color.green : Color.red; } return Color.green; // 没有遮挡 } // ===== 工具方法 ===== private void MoveToward(Vector3 target, float speed) { Vector3 dir = new Vector3( target.x - transform.position.x, 0f, target.z - transform.position.z ); float dist = dir.magnitude; if (dist < 0.01f) return; dir /= dist; Vector3 move = dir * speed * Time.deltaTime; if (move.magnitude > dist) move = dir * dist; // 不越过目标 transform.position += new Vector3(move.x, 0f, move.z); } private float XZDistance(Vector3 a, Vector3 b) { float dx = a.x - b.x; float dz = a.z - b.z; return Mathf.Sqrt(dx * dx + dz * dz); } // ===== 编辑器可视化 ===== private void OnDrawGizmosSelected() { // 根据状态显示不同颜色 Color stateColor = Color.white; switch (_state) { case State.Idle: stateColor = Color.gray; break; case State.ChasingPlayer: stateColor = Color.red; break; case State.ChasingBell: stateColor = Color.yellow; break; case State.PatrollingBell: stateColor = Color.cyan; break; } // 绘制当前状态指示 Gizmos.color = stateColor; Gizmos.DrawWireSphere(transform.position, 0.5f); // 绘制聆听范围 Gizmos.color = new Color(0f, 0.85f, 1f, 0.3f); Gizmos.DrawWireSphere(transform.position, listenRange); // 绘制停止距离 Gizmos.color = new Color(1f, 0.3f, 0.3f, 0.3f); Gizmos.DrawWireSphere(transform.position, stopDistance); // 绘制铃铛目标(如果在追击铃铛或巡逻) if (_state == State.ChasingBell || _state == State.PatrollingBell) { Gizmos.color = Color.yellow; Gizmos.DrawWireSphere(_bellTargetPosition, bellArriveDistance); // 绘制到铃铛的连线 Gizmos.DrawLine(transform.position, _bellTargetPosition); } // 绘制巡逻范围(如果在巡逻) if (_state == State.PatrollingBell) { Gizmos.color = Color.cyan; Gizmos.DrawWireSphere(_patrolCenter, bellPatrolRadius); // 绘制当前巡逻目标 Gizmos.color = Color.green; Gizmos.DrawWireSphere(_patrolTarget, 0.3f); } } /// 当前状态(调试用) public string CurrentState => _state.ToString(); } }