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("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; 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; 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; if (!_gaitInit || _gaitMode != _mode) { _enemy.PlayLocomotionClip(_mode); _gaitMode = _mode; _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; } } private void TickPatrol() { switch (_patrolStrategy) { case PatrolStrategy.Wander: if (_enemy.Nav != null && !_enemy.Nav.IsMoving) _enemy.Nav.WalkToRandom(); 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; } // 仅首次或"已到达当前路点"时推进并寻路——不依赖 IsMoving // (PathBerserker2d 寻路跨帧异步,IsMoving 在寻路计算期间为 false,会误触发每帧推进)。 if (_wpIndex < 0 || Vector2.Distance(_enemy.transform.position, _waypoints[_wpIndex].position) <= _waypointArriveRadius) { AdvanceWaypoint(); _enemy.MoveTo(_waypoints[_wpIndex].position); } break; } } 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; } } } }