using UnityEngine; using BaseGames.Enemies; namespace BaseGames.Enemies.Navigation { /// /// 地面单位导航:朝目标 X 直接行走,由 的墙/悬崖夹紧决定可达性。 /// /// 不做任何地形扫描:走得过去就是可达,被夹停的地方就是边界。 /// 因此不存在"脱离导航图"这类状态——敌人无论被击退/冲锋到哪里,都能正常继续行走。 /// /// 只驱动水平方向;垂直交给刚体重力。移动经 下发, /// 速度为 0 时退化为移动层配置的步速。 /// [RequireComponent(typeof(EnemyMovement))] public sealed class GroundNavigator : MonoBehaviour, IEnemyNavigator { [Header("到达判定")] [Tooltip("身体中心 X 与目标 X 的距离小于此值即视为到达(m)")] [SerializeField] [Min(0.01f)] private float _stoppingDistance = 0.2f; [Header("游走取点(Wander 用)")] [Tooltip("单次游走的最近距离(m)")] [SerializeField] [Min(0f)] private float _wanderMinRange = 1.5f; [Tooltip("单次游走的最远距离(m)。走不到会被地形夹停,自然结束本次游走。")] [SerializeField] [Min(0f)] private float _wanderMaxRange = 5f; [Header("推进停滞检测")] [Tooltip("窗口时长(s):此时长内净位移不足即判受阻")] [SerializeField] [Min(0.05f)] private float _stallWindowSeconds = 0.4f; [Tooltip("窗口内所需最小净位移(m)")] [SerializeField] [Min(0f)] private float _stallMinProgress = 0.05f; private EnemyBase _enemy; private EnemyMovement _movement; private StallDetector _stall; private Vector2 _target; private bool _hasTarget; private float _speed; // 0 = 用移动层配置步速 private bool _arrived; private bool _blocked; public bool IsMoving => _hasTarget && !_arrived && !_blocked; public bool HasArrived => _arrived; public bool IsBlocked => _blocked; private void Awake() { _enemy = GetComponent(); _movement = GetComponent(); _stall = new StallDetector(_stallWindowSeconds, _stallMinProgress); if (_enemy == null) Debug.LogError($"[GroundNavigator] {name} 找不到 EnemyBase,无法读取身体几何。", this); } // ── IEnemyNavigator ──────────────────────────────────────────── public void MoveTowards(Vector2 target) { // 幂等:同一目标重复调用不重置状态,避免每帧调用时永远清掉到达/受阻标志 if (_hasTarget && (target - _target).sqrMagnitude < 1e-6f) return; _target = target; _hasTarget = true; _arrived = false; _blocked = false; _stall.Reset(BodyX); } public void Stop() { _hasTarget = false; _arrived = false; _blocked = false; if (_enemy != null) _enemy.StopMovement(); } public void SetSpeed(float speed) => _speed = Mathf.Max(0f, speed); public bool TryPickWanderPoint(out Vector2 point) { float sign = Random.value < 0.5f ? -1f : 1f; float x = WanderX(BodyX, _wanderMinRange, _wanderMaxRange, sign, Random.value); point = new Vector2(x, transform.position.y); return true; // 地面单位总能尝试;走不到会被夹停并置 IsBlocked } // ── 驱动 ─────────────────────────────────────────────────────── private float BodyX => _enemy != null ? _enemy.Body.Center.x : transform.position.x; private void FixedUpdate() { if (!_hasTarget || _arrived || _blocked || _movement == null) return; float bodyX = BodyX; if (Arrived(bodyX, _target.x, _stoppingDistance)) { _arrived = true; if (_enemy != null) _enemy.StopMovement(); return; } float dir = Mathf.Sign(_target.x - bodyX); // 地形夹紧即为"不可达边界":着地时前方是墙或悬崖 → 本次移动到此为止 if (_movement.IsGrounded && _movement.WouldBlockAhead(dir)) { _blocked = true; if (_enemy != null) _enemy.StopMovement(); return; } _movement.PendingInput.MoveDir = dir; _movement.PendingInput.MoveSpeed = _speed; // 0 → MoveHorizontal 用配置步速 _movement.PendingInput.WantStop = false; // 兜底:命令了移动却推不动(撞上未纳入检测层的碰撞体等) if (_stall.Tick(bodyX, Time.fixedDeltaTime)) { _blocked = true; if (_enemy != null) _enemy.StopMovement(); } } // ── 纯函数(便于无场景单测)────────────────────────────────────── /// 身体中心 X 是否已进入目标 X 的停止距离内。 public static bool Arrived(float currentX, float targetX, float stopDistance) => Mathf.Abs(targetX - currentX) <= stopDistance; /// /// 游走目标 X:自当前位置朝 方向, /// 在 [minRange, maxRange] 区间内按插值系数 (0..1) 取点。 /// public static float WanderX(float currentX, float minRange, float maxRange, float sign, float t) => currentX + sign * Mathf.Lerp(minRange, Mathf.Max(minRange, maxRange), Mathf.Clamp01(t)); } }