Files
gold_dolphin/unity/.qoder/specs/Enemy双重寻敌机制实现_task-69b.md
T
2026-06-29 02:48:13 +08:00

9.9 KiB

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

新增状态:

private enum State 
{ 
    Idle, 
    ChasingPlayer,      // 追击玩家
    ChasingBell,        // 追击铃铛
    PatrollingBell      // 铃铛位置巡逻
}

新增参数:

[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;

新增变量:

private float _loseSightTimer;           // 脱离视线计时器
private Vector3 _bellTargetPosition;     // 铃铛目标位置
private float _bellChaseTimer;           // 铃铛追击计时器
private Vector3 _patrolCenter;           // 巡逻中心点
private float _patrolTimer;              // 巡逻计时器
private Vector3 _patrolTarget;           // 当前巡逻目标点

步骤2:实现射线检测视线

新增方法:

/// <summary>
/// 检测是否能看到玩家(射线检测)
/// </summary>
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状态:

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);
            }
        }
        
        // 超过3秒,停止追击
        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);  // 巡逻时速度稍慢
    }
}

/// <summary>
/// 选择新的巡逻点
/// </summary>
private void PickNewPatrolPoint()
{
    // 在巡逻中心周围随机选择一个点
    Vector2 randomDir = Random.insideUnitCircle * bellPatrolRadius;
    _patrolTarget = _patrolCenter + new Vector3(randomDir.x, 0f, randomDir.y);
}

步骤4:修改铃铛响应逻辑

OnBell方法:

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方法:

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方法:

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小时