using UnityEngine;
using BaseGames.AI;
namespace BaseGames.Enemies
{
///
/// 敌人移动执行器:唯一的移动/朝向入口。AI 状态与能力经 IEnemyLocomotion
/// 声明意图,本组件每帧把当前模式翻译为对 EnemyBase/IPathAgent 的调用。
/// 取代散落的 MoveTo/StopMovement/FacePlayer/FaceTarget 直调与三个协程能力。
///
[DisallowMultipleComponent]
public sealed class EnemyLocomotion : MonoBehaviour, IEnemyLocomotion
{
[Header("巡逻策略(Wander/Pace/Waypoints)")]
[SerializeField] private PatrolStrategy _patrolStrategy = PatrolStrategy.Wander;
[Header("Wander 策略参数")]
[Tooltip("到达一个随机游走点后的停顿时长下限(秒)。停顿期间原地待机(播 Idle),再挑下一个点。")]
[SerializeField] [Min(0f)] private float _wanderPauseMin = 0.5f;
[Tooltip("到达一个随机游走点后的停顿时长上限(秒)。与下限相同=固定时长;两者都为 0=不停顿、立即挑下一个点。")]
[SerializeField] [Min(0f)] private float _wanderPauseMax = 1.5f;
[Header("Waypoints 策略参数")]
[SerializeField] private Transform[] _waypoints;
[SerializeField] private bool _pingPong;
[SerializeField] private float _waypointArriveRadius = 0.4f;
private EnemyBase _enemy;
private LocomotionMode _mode = LocomotionMode.Idle;
private Transform _approachTarget;
private Vector2 _facePoint;
private LocomotionMode _gaitMode;
private bool _gaitInit;
private float _paceDir = 1f;
private bool _paceBlockedPrev;
private float _paceWindowStartX;
private int _paceWindowFrames;
private int _wpIndex = -1;
private int _wpDir = 1;
private bool _warnedNoWaypoints;
// 当前路点吸附后的"站得住"目标(宽度由 Nav 层解析,本层不感知身体尺寸)
private Vector2 _wpResolvedGoal;
private bool _wpResolveOk; // 上次路点是否成功吸附到导航段(调试用)
// Wander 到点停顿状态
private bool _wanderPausing;
private float _wanderPauseTimer;
public LocomotionMode CurrentMode => _mode;
public bool IsMoving => _enemy != null && _enemy.Nav != null && _enemy.Nav.IsMoving;
private void Awake()
{
_enemy = GetComponentInParent();
if (_enemy == null)
Debug.LogError("[EnemyLocomotion] 找不到 EnemyBase。", this);
}
// ── IEnemyLocomotion(声明意图,不立即 actuate 之外的副作用)──
public void SetMode(LocomotionMode mode)
{
if (_mode == mode) return;
_mode = mode;
_wanderPausing = false; // 切换模式清 Wander 停顿态
if (mode == LocomotionMode.Idle) _enemy?.StopMovement();
if (mode == LocomotionMode.Patrol && _enemy?.Stats != null)
_enemy.Nav?.SetSpeed(_enemy.Stats.WalkSpeed);
}
public void Approach(Transform target)
{
_mode = LocomotionMode.Approach;
_approachTarget = target;
if (_enemy?.Stats != null) _enemy.Nav?.SetSpeed(_enemy.Stats.RunSpeed);
}
public void MoveTo(Vector2 point)
{
_mode = LocomotionMode.Approach;
_approachTarget = null;
_enemy?.MoveTo(point);
}
public void Face(Vector2 lookAt)
{
_mode = LocomotionMode.Face;
_facePoint = lookAt;
}
public void Stop()
{
_mode = LocomotionMode.Idle;
_enemy?.StopMovement();
}
// ── 每帧把模式翻译成执行 ──
private void Update()
{
if (_enemy == null) return;
// Wander 到点停顿期间用 Idle 步态(原地待机,否则站着播 Walk=原地踏步)。
LocomotionMode gait = (_mode == LocomotionMode.Patrol
&& _patrolStrategy == PatrolStrategy.Wander
&& _wanderPausing)
? LocomotionMode.Idle : _mode;
if (!_gaitInit || _gaitMode != gait)
{
_enemy.PlayLocomotionClip(gait);
// 招式动画锁定期内不算"已应用",解锁后重新同步步态(否则 Skill_End 后卡在收招动画)
if (!_enemy.IsAnimLocked) { _gaitMode = gait; _gaitInit = true; }
}
switch (_mode)
{
case LocomotionMode.Idle:
break; // SetMode(Idle) 已停;保持
case LocomotionMode.Patrol:
TickPatrol();
break;
case LocomotionMode.Face:
_enemy.StopMovement();
_enemy.FaceTarget(_facePoint);
break;
case LocomotionMode.Approach:
if (_approachTarget != null) _enemy.MoveTo(_approachTarget.position);
break;
}
}
///
/// 段内随机游走:随机点限制在当前所站的导航段上(不跨 NavLink,避免崖边夹停/卡 link),
/// 到达一个点后原地停顿 [_wanderPauseMin, _wanderPauseMax] 秒(期间播 Idle),再挑下一个点。
///
private void TickWander()
{
if (_enemy.Nav == null) return;
// 移动中:清停顿态,等到达
if (_enemy.Nav.IsMoving) { _wanderPausing = false; return; }
// 已到达(或尚未出发):先停顿计时,再挑下一个点
if (!_wanderPausing)
{
_wanderPausing = true;
_wanderPauseTimer = Random.Range(_wanderPauseMin, Mathf.Max(_wanderPauseMin, _wanderPauseMax));
}
_wanderPauseTimer -= Time.deltaTime;
if (_wanderPauseTimer <= 0f)
{
_wanderPausing = false;
_enemy.Nav.WalkToRandomOnSegment();
}
}
private void TickPatrol()
{
switch (_patrolStrategy)
{
case PatrolStrategy.Wander:
TickWander();
break;
case PatrolStrategy.Pace: // 撞墙/悬崖翻向的来回踱步(速度驱动,不依赖 nav)
var mv = _enemy.Movement;
// 用与移动层夹紧同一判定(WouldBlockAhead,即时按请求方向+碰撞体前缘),
// 保证"夹停在地形边缘"与"掉头"一致,不会夹停却不翻向。
bool blocked = mv != null && mv.WouldBlockAhead(_paceDir);
// 兜底卡死检测(窗口净位移):命令前进但一段时间净位移过小(撞上未挂检测层的墙/
// 卡角,含贴墙微抖)也视为受阻,使巡逻对场景配置鲁棒。用净位移而非逐帧差,
// 避免"贴墙推进-被弹回"的微抖每帧重置计数。
float px = _enemy.transform.position.x;
if (++_paceWindowFrames >= 12)
{
if (Mathf.Abs(px - _paceWindowStartX) < 0.05f) blocked = true;
_paceWindowStartX = px;
_paceWindowFrames = 0;
}
// 边沿触发:仅"障碍首次出现"翻一次;翻后按新方向即时判定,天然消抖,
// 且两侧都堵(窄台)时翻一次即停住、不来回抖。
if (blocked && !_paceBlockedPrev)
{
_paceDir = -_paceDir;
_paceWindowStartX = px; _paceWindowFrames = 0; // 翻向后重开窗口
}
_paceBlockedPrev = blocked;
_enemy.MoveInDirection(_paceDir);
break;
case PatrolStrategy.Waypoints:
if (_waypoints == null || _waypoints.Length == 0)
{
if (!_warnedNoWaypoints)
{
Debug.LogWarning("[EnemyLocomotion] PatrolStrategy=Waypoints 但未配置 _waypoints,无法巡逻。", this);
_warnedNoWaypoints = true;
}
break;
}
// 到达判定对"吸附后的可达点"做,而非原始路点:原始路点可能摆在空中/崖外,
// 与之的距离永远进不了到达半径 → 卡死;吸附点是身体站得住的可达点。
bool wpArrived = _wpIndex >= 0 &&
Vector2.Distance(_enemy.transform.position, _wpResolvedGoal) <= _waypointArriveRadius;
if (_wpIndex < 0 || wpArrived)
{
// 首次 / 到达当前路点 → 推进到下一路点并寻路
AdvanceWaypoint();
SetWaypointGoal();
}
else if (_enemy.Nav != null && !_enemy.Nav.IsMoving)
{
// 有目标但 nav 空闲且未到达:初始寻路未成(如刚出生、尚未映射到导航图那一帧发的 MoveTo 失败)
// 或路径中途丢失 → 重发同一目标恢复。只重发、不推进索引;RequestMoveTo 自带 0.25s 防抖,
// 寻路跨帧计算期间的重发会被防抖吞掉,不会每帧重启寻路。
_enemy.MoveTo(_wpResolvedGoal);
}
break;
}
}
///
/// 解析当前路点为"身体站得住的可达点"并出发。宽度处理下沉给 Nav 层(),
/// 本层只递原始路点坐标、不感知身体尺寸。吸附失败=路点摆得离导航面太远,显式报错暴露根因,不静默兜底。
///
private void SetWaypointGoal()
{
Vector2 raw = _waypoints[_wpIndex].position;
_wpResolveOk = _enemy.Nav != null && _enemy.Nav.ResolveStandablePoint(raw, out _wpResolvedGoal);
if (!_wpResolveOk)
{
_wpResolvedGoal = raw;
Debug.LogWarning($"[EnemyLocomotion] 路点 '{_waypoints[_wpIndex].name}'(index {_wpIndex}) 无法吸附到任何可行走导航段" +
"(超出搜索半径)。请检查该路点是否摆放在贴近地面/平台的可行走处。", this);
}
_enemy.MoveTo(_wpResolvedGoal);
}
private void AdvanceWaypoint()
{
if (_pingPong)
{
int next = _wpIndex + _wpDir;
if (next < 0 || next >= _waypoints.Length) { _wpDir = -_wpDir; next = _wpIndex + _wpDir; }
_wpIndex = Mathf.Clamp(next, 0, _waypoints.Length - 1);
}
else
{
_wpIndex = (_wpIndex + 1) % _waypoints.Length;
}
}
#if UNITY_EDITOR
// ── 调试只读(供自定义 Inspector / Gizmos,仅编辑器)────────────────────────
public EnemyBase DebugEnemy => _enemy;
public PatrolStrategy DebugStrategy => _patrolStrategy;
public Transform[] DebugWaypoints => _waypoints;
public int DebugWaypointIndex => _wpIndex;
public Vector2 DebugResolvedGoal => _wpResolvedGoal;
public bool DebugResolveOk => _wpResolveOk;
public float DebugArriveRadius => _waypointArriveRadius;
public bool DebugWanderPausing => _wanderPausing;
private void OnDrawGizmosSelected()
{
if (_patrolStrategy != PatrolStrategy.Waypoints || _waypoints == null) return;
// 所有路点 + 连线(编辑期即可见,用于摆点)
Gizmos.color = new Color(0.3f, 0.8f, 1f);
for (int i = 0; i < _waypoints.Length; i++)
{
if (_waypoints[i] == null) continue;
Gizmos.DrawWireSphere(_waypoints[i].position, 0.15f);
var next = _waypoints[(i + 1) % _waypoints.Length];
if (next != null) Gizmos.DrawLine(_waypoints[i].position, next.position);
}
// 运行期:当前吸附目标 + 到达半径 + 连线(绿=吸附成功 红=失败回落原始点)
if (Application.isPlaying && _wpIndex >= 0)
{
Vector3 from = _enemy != null ? _enemy.transform.position : transform.position;
Gizmos.color = _wpResolveOk ? Color.green : Color.red;
Gizmos.DrawWireSphere(_wpResolvedGoal, 0.22f);
Gizmos.DrawLine(from, _wpResolvedGoal);
Gizmos.color = new Color(1f, 0.9f, 0.2f, 0.7f);
Gizmos.DrawWireSphere(_wpResolvedGoal, _waypointArriveRadius);
}
}
#endif
}
}