Files
gold_dolphin/unity/Assets/enemy/EnemyAI.cs

401 lines
14 KiB
C#
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
using UnityEngine;
namespace IndianOceanAssets.Engine2_5D
{
/// <summary>
/// 敌人AI v2 —— 双重寻敌机制(视觉检测 + 铃铛响应)
///
/// 状态流程:
/// - Idle待机。射线检测到玩家 → ChasingPlayer收到铃铛信号 → ChasingBell
/// - ChasingPlayer追击玩家。持续看到玩家则追击脱离视线3秒后 → Idle
/// - ChasingBell追击铃铛。追击过程中看到玩家 → ChasingPlayer到达铃铛位置 → PatrollingBell
/// - PatrollingBell铃铛位置巡逻。巡逻过程中看到玩家 → ChasingPlayer巡逻时间结束 → Idle
///
/// 挂载位置:敌人预制体上
/// 需要:场景中有 EchoSystem用于摇铃事件、玩家物体带 "Player" 标签
/// (或手动把玩家拖到 playerTarget 字段)
/// </summary>
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;
[Tooltip("最大视野距离(超过此距离看不到玩家)")]
[SerializeField] private float maxSightDistance = 15f;
[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;
}
/// <summary>
/// 选择新的巡逻点
/// </summary>
private void PickNewPatrolPoint()
{
// 在巡逻中心周围随机选择一个点
Vector2 randomDir = Random.insideUnitCircle * bellPatrolRadius;
_patrolTarget = _patrolCenter + new Vector3(randomDir.x, 0f, randomDir.y);
}
/// <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 (dist > maxSightDistance)
{
return false;
}
// 调试在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;
}
/// <summary>
/// 调试用:根据是否能看到玩家返回不同颜色
/// </summary>
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, 1f, 0f, 0.2f);
Gizmos.DrawWireSphere(transform.position, maxSightDistance);
// 绘制聆听范围
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);
}
}
/// <summary>当前状态(调试用)</summary>
public string CurrentState => _state.ToString();
}
}