388 lines
14 KiB
C#
388 lines
14 KiB
C#
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;
|
||
|
||
[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;
|
||
|
||
// 调试:在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, 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();
|
||
}
|
||
}
|