# Enemy双重寻敌机制实现计划 ## 需求总结 1. **规则一:视觉检测与追击** - 怪物通过射线检测看到玩家(360°全向视野) - 看到玩家后追击 - 玩家脱离视线3秒后停止追击 2. **规则二:铃铛响应** - 玩家摇铃铛时,铃铛声波范围内的怪物获取铃铛位置 - 怪物追击到铃铛位置 - 追击过程中如果看到玩家,切换为追击玩家(执行规则一) - 到达铃铛位置后,如果没有看到玩家,在周围巡逻一段时间 ## 状态机设计 ``` Idle(待机) ├─ 射线检测到玩家 → ChasingPlayer └─ 收到铃铛信号 → ChasingBell ChasingPlayer(追击玩家) ├─ 持续看到玩家 → 继续追击 ├─ 玩家脱离视线 → 开始计时(3秒) │ └─ 3秒内重新看到 → 继续追击 │ └─ 3秒后仍未看到 → Idle └─ 收到铃铛信号 → ChasingBell(打断当前状态) ChasingBell(追击铃铛) ├─ 追击过程中射线检测到玩家 → ChasingPlayer ├─ 到达铃铛位置 → PatrollingBell └─ 超时(10秒未到达)→ Idle PatrollingBell(铃铛位置巡逻) ├─ 巡逻过程中射线检测到玩家 → ChasingPlayer └─ 巡逻时间结束(5秒)→ Idle ``` ## 实现步骤 ### 步骤1:修改EnemyAI.cs - 添加新状态和参数 **文件**: `Assets/enemy/EnemyAI.cs` **新增状态**: ```csharp private enum State { Idle, ChasingPlayer, // 追击玩家 ChasingBell, // 追击铃铛 PatrollingBell // 铃铛位置巡逻 } ``` **新增参数**: ```csharp [Header("视觉检测")] [Tooltip("射线检测层(哪些物体会阻挡视线)")] [SerializeField] private LayerMask obstacleLayers; [Tooltip("射线起点偏移(避免从地面发射)")] [SerializeField] private float rayOffsetY = 0.5f; [Header("脱离计时")] [Tooltip("玩家脱离视线后,继续追击的时间(秒)")] [SerializeField] private float loseSightTime = 3f; [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; ``` **新增变量**: ```csharp private float _loseSightTimer; // 脱离视线计时器 private Vector3 _bellTargetPosition; // 铃铛目标位置 private float _bellChaseTimer; // 铃铛追击计时器 private Vector3 _patrolCenter; // 巡逻中心点 private float _patrolTimer; // 巡逻计时器 private Vector3 _patrolTarget; // 当前巡逻目标点 ``` ### 步骤2:实现射线检测视线 **新增方法**: ```csharp /// /// 检测是否能看到玩家(射线检测) /// 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; // 发射射线,只检测障碍物层 if (Physics.Raycast(rayStart, dir.normalized, out RaycastHit hit, dist, obstacleLayers)) { // 如果击中的是玩家,说明可以看到 return hit.transform == playerTarget; } return false; } ``` ### 步骤3:修改状态机逻辑 **Idle状态**: ```csharp private void UpdateIdle() { // 检测是否看到玩家 if (CanSeePlayer()) { _state = State.ChasingPlayer; _loseSightTimer = 0f; return; } } ``` **ChasingPlayer状态**: ```csharp 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); } } // 超过3秒,停止追击 if (_loseSightTimer >= loseSightTime) { _state = State.Idle; } } } ``` **ChasingBell状态**: ```csharp 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状态**: ```csharp 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); // 巡逻时速度稍慢 } } /// /// 选择新的巡逻点 /// private void PickNewPatrolPoint() { // 在巡逻中心周围随机选择一个点 Vector2 randomDir = Random.insideUnitCircle * bellPatrolRadius; _patrolTarget = _patrolCenter + new Vector3(randomDir.x, 0f, randomDir.y); } ``` ### 步骤4:修改铃铛响应逻辑 **OnBell方法**: ```csharp private void OnBell(Vector3 bellPos) { if (playerTarget == null) return; // 只有在聆听范围内才响应 if (XZDistance(transform.position, bellPos) > listenRange) return; // 设置铃铛目标位置 _bellTargetPosition = bellPos; _bellChaseTimer = 0f; // 切换到追击铃铛状态(可以打断任何状态) _state = State.ChasingBell; } ``` ### 步骤5:更新状态机调用 **Update方法**: ```csharp 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; } } ``` ### 步骤6:更新编辑器可视化 **OnDrawGizmos方法**: ```csharp 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); // 绘制铃铛目标(如果在追击铃铛) 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); } } ``` ## 配置参数建议 | 参数 | 建议值 | 说明 | |-----|--------|------| | `obstacleLayers` | Obstacle | 阻挡视线的物体层 | | `rayOffsetY` | 0.5 | 射线起点高度 | | `loseSightTime` | 3.0 | 脱离视线后继续追击时间 | | `bellArriveDistance` | 1.0 | 到达铃铛位置的容差 | | `bellChaseTimeout` | 10.0 | 追击铃铛最大时间 | | `bellPatrolTime` | 5.0 | 铃铛位置巡逻时间 | | `bellPatrolRadius` | 3.0 | 巡逻半径 | ## 测试要点 1. **视觉检测测试** - 在Enemy和玩家之间放置障碍物,验证射线被阻挡 - 移除障碍物,验证能检测到玩家 2. **脱离视线测试** - 玩家跑出Enemy视线,验证3秒后停止追击 - 3秒内重新出现,验证继续追击 3. **铃铛响应测试** - 玩家摇铃铛,验证Enemy朝铃铛位置移动 - 铃铛追击过程中看到玩家,验证切换为追击玩家 4. **巡逻测试** - 到达铃铛位置后,验证在周围巡逻 - 巡逻过程中看到玩家,验证切换为追击玩家 ## 文件清单 - 修改: `Assets/enemy/EnemyAI.cs` ## 预计工作量 - 实现时间: 2-3小时 - 调试时间: 1-2小时 - 总计: 3-5小时