using UnityEngine; using PathBerserker2d; using BaseGames.Enemies; using System; using System.Collections.Generic; namespace BaseGames.Enemies.Navigation { /// /// PathBerserker2d 导航代理包装器(架构 07_EnemyModule §5 能力驱动版)。 /// /// 设计原则: /// /// 连接段穿越(跳跃/攀爬/传送等)委托给 能力组件执行 /// 能力自行驱动动画 + 物理,本组件只做"调度 → 等回调 → 通知 PB2d 继续" /// 无对应能力时自动回退到 TransformBasedMovement(兜底行为) /// Awake 自动发现 GameObject 上的全部 INavLinkHandler,并禁用 TBM 对应 FeatureFlag /// /// [RequireComponent(typeof(NavAgent))] [RequireComponent(typeof(TransformBasedMovement))] public class EnemyNavAgent : MonoBehaviour, IPathAgent { // ── 序列化 ────────────────────────────────────────────────────── [Tooltip("边缘检测前向距离(m)")] [SerializeField] private float _edgeCheckFwdOffset = 0.3f; [Tooltip("边缘检测向下射线长度(m)")] [SerializeField] private float _edgeCheckDownLen = 0.6f; [Tooltip("边缘检测 LayerMask(留空 = 所有层)")] [SerializeField] private LayerMask _groundMask = ~0; [Header("重寻路防抖(PathBerserker2d 不可每帧 UpdatePath,否则路径永远算不完)")] [Tooltip("目标位移阈值(m):目标移动超过此值才重算路径")] [SerializeField] private float _repathTargetThreshold = 0.5f; [Tooltip("两次重算最小间隔(s):留出跨帧路径计算完成的时间")] [SerializeField] private float _repathMinInterval = 0.25f; [Header("目标点吸附")] [Tooltip("把巡逻/移动目标吸附到最近可行走段时,允许目标离最近段的最大真实距离(m)。语义同 PB2d 的 " + "closestToSegmentMaxDistance——想多大就多大;只要目标在此距离内一定吸附得到。仅当目标离所有" + "烘焙段都超过此值(真正脱离导航面)才判失败。")] [SerializeField] private float _maxSnapDistance = 10f; [Header("受阻检测(寻路移动无进展即判被挡)")] [Tooltip("卡死检测窗口时长(s):段上移动时,此时长内朝子目标推进小于阈值即判受阻")] [SerializeField] private float _stuckWindowSeconds = 0.5f; [Tooltip("卡死推进阈值(m):窗口内朝子目标的距离缩小不足此值即判卡死")] [SerializeField] private float _stuckMinProgress = 0.05f; // 重寻路防抖状态 private Vector2 _lastPathGoal; private bool _hasPathGoal; private float _lastPathTime; // ── IPathAgent 公开状态 ──────────────────────────────────────── public bool IsMoving => _navAgent != null && _navAgent.IsFollowingAPath; public bool IsOnLink => _navAgent != null && _navAgent.IsOnLink; public NavLinkType CurrentLinkType => _currentLinkType; public Vector2 CurrentLinkStart => _currentLinkStart; public Vector2 CurrentLinkEnd => _currentLinkEnd; /// 上次寻路移动是否因不可达/被挡而受阻(走到最近点或原地停,而非到达原目标)。新目标 RequestMoveTo 后复位。 public bool LastMoveObstructed => _lastMoveObstructed; /// 取上次寻路失败时算出的最近可达世界点。无则返回 false。 public bool TryGetClosestReachable(out Vector2 worldPos) { if (_navAgent != null) return _navAgent.TryGetClosestReachablePosition(out worldPos); worldPos = Vector2.zero; return false; } // ── IPathAgent 事件 ──────────────────────────────────────────── public event Action OnLinkStarted; public event Action OnLinkCompleted; public event Action OnNavPathFailed; public event Action OnGoalReached; // ── 私有 ──────────────────────────────────────────────────────── private NavAgent _navAgent; private TransformBasedMovement _movement; private EnemyMovement _enemyMovement; // 能力 handler 注册表(NavLinkType → INavLinkHandler) private readonly Dictionary _handlers = new Dictionary(); private INavLinkHandler _activeHandler; // 受阻状态(供意图层结算) private bool _lastMoveObstructed; private bool _redirectedToClosest; // 本轮已改道到最近点,避免每帧重复下发 private bool _obstructionSignal; // 受阻信号(寻路失败/卡死),延后到 FixedUpdate 统一处理 // 卡死检测窗口 private float _stuckWinTimer; private float _stuckWinStartDist; private bool _stuckWinInit; // 连接段状态缓存 private NavLinkType _currentLinkType = NavLinkType.None; private bool _wasNavOnSegment; private Vector2 _currentLinkStart; private Vector2 _currentLinkEnd; // ── 初始化 ───────────────────────────────────────────────────── private void Awake() { _navAgent = GetComponent(); _movement = GetComponent(); _enemyMovement = GetComponent(); // 物理原生移动:禁用 TBM 段移动——不让它直接写 transform.position(会与 Dynamic 刚体物理冲突)。 // 段上的水平移动改由 EnemyMovement 用 velocity 执行、刚体重力保持贴地(见 FixedUpdate)。 if (_movement != null) _movement.enabledFeatures &= ~TransformBasedMovement.FeatureFlags.SegmentMovement; // 自动发现 INavLinkHandler 组件并注册(包含子对象) foreach (var handler in GetComponentsInChildren(true)) RegisterLinkHandler(handler); _navAgent.OnStartLinkTraversal += HandleLinkStart; _navAgent.OnSegmentTraversal += HandleSegmentTraversal; _navAgent.OnFailedToFindPath += HandlePathFailed; _navAgent.OnReachedGoal += HandleGoalReached; } private void OnDestroy() { if (_navAgent == null) return; _navAgent.OnStartLinkTraversal -= HandleLinkStart; _navAgent.OnSegmentTraversal -= HandleSegmentTraversal; _navAgent.OnFailedToFindPath -= HandlePathFailed; _navAgent.OnReachedGoal -= HandleGoalReached; } // ── Handler 注册 ─────────────────────────────────────────────── /// /// 注册能力为某连接段类型的处理器。 /// 注册后 TransformBasedMovement 中对应 FeatureFlag 将被禁用,由能力全权负责该类型穿越。 /// public void RegisterLinkHandler(INavLinkHandler handler) { if (handler == null) return; foreach (var type in handler.HandledLinkTypes) { _handlers[type] = handler; // 禁用 TBM 对该类型的处理,完全交给能力 var flag = TypeToFeatureFlag(type); if (flag != 0 && _movement != null) _movement.enabledFeatures &= ~flag; } } /// 移除某处理器(例如能力被永久禁用时)。TBM FeatureFlag 自动恢复。 public void UnregisterLinkHandler(INavLinkHandler handler) { if (handler == null) return; foreach (var type in handler.HandledLinkTypes) { if (_handlers.TryGetValue(type, out var registered) && ReferenceEquals(registered, handler)) { _handlers.Remove(type); // 恢复 TBM 对该类型的处理 var flag = TypeToFeatureFlag(type); if (flag != 0 && _movement != null) _movement.enabledFeatures |= flag; } } } // ── IPathAgent ───────────────────────────────────────────────── public void RequestMoveTo(Vector2 target) { if (_navAgent == null) return; // PathBerserker2d 明确警告不可每帧 UpdatePath(路径计算跨帧,每帧重启则永不完成、agent 永不移动)。 // 防抖:目标未明显移动 且 距上次重算不足间隔 → 跳过,让当前路径算完并跟随。 if (_hasPathGoal && Vector2.Distance(target, _lastPathGoal) < _repathTargetThreshold && Time.time - _lastPathTime < _repathMinInterval) return; _lastPathGoal = target; _hasPathGoal = true; _lastPathTime = Time.time; _lastMoveObstructed = false; // 新目标 → 复位受阻态 _redirectedToClosest = false; ResetStuckWindow(); _navAgent.UpdatePath(target); } public void StopNavigation() { _activeHandler?.AbortLinkTraversal(); _activeHandler = null; _hasPathGoal = false; // 复位防抖,下次请求立即重新寻路 _navAgent?.Stop(); } public bool IsAtDestination() => _navAgent == null || _navAgent.IsIdle; public void SetSpeed(float speed) { if (_movement != null) _movement.movementSpeed = speed; } public bool CanReach(Vector2 target) => _navAgent?.CanReach(target) ?? false; public bool WalkToRandom() => _navAgent?.SetRandomDestination() ?? false; // 段内随机游走:按身体前缘安全余量内缩段两端,避免随机点落在角色宽度站不住的崖沿(WouldFallAhead 夹停)。 public bool WalkToRandomOnSegment() => _navAgent?.SetRandomDestinationOnCurrentSegment( _enemyMovement != null ? _enemyMovement.EdgeSafeMargin : 0f) ?? false; // 目标点吸附:把任意世界目标映射到最近可行走段、并按身体宽度(EdgeSafeMargin)从段两端内缩, // 得到"身体真正站得住"的点。身体宽度是本执行层独占的知识——意图层(AI/Locomotion)只递原始目标。 // 用 PB2d 映射任意点到导航面的机制(_maxSnapDistance 内一定命中最近段);返回 false 仅表示目标真正脱离导航面。 public bool ResolveStandablePoint(Vector2 target, out Vector2 standable) { if (_navAgent == null) { standable = target; return false; } float margin = _enemyMovement != null ? _enemyMovement.EdgeSafeMargin : 0f; return _navAgent.TryGetStandablePointNear(target, _maxSnapDistance, margin, out standable); } public bool IsNearEdge() { var origin = (Vector2)transform.position; var facing = transform.localScale.x >= 0f ? Vector2.right : Vector2.left; return !Physics2D.Raycast(origin + facing * _edgeCheckFwdOffset, Vector2.down, _edgeCheckDownLen, _groundMask); } // ── 底层 NavAgent 直接访问 ────────────────────────────────────── /// 原始 PB2d NavAgent,供需要 PB2d 高级功能的能力直接使用。 public NavAgent RawNavAgent => _navAgent; // ── 连接段调度核心 ───────────────────────────────────────────── private void HandleLinkStart(NavAgent agent) { var seg = agent.CurrentPathSegment; _currentLinkType = ParseLinkType(seg?.link?.LinkTypeName); _currentLinkStart = seg?.LinkStart ?? Vector2.zero; _currentLinkEnd = seg?.LinkEnd ?? Vector2.zero; OnLinkStarted?.Invoke(_currentLinkType); if (_handlers.TryGetValue(_currentLinkType, out var handler)) { if (handler.CanHandleLink(_currentLinkType, _currentLinkStart, _currentLinkEnd)) { _activeHandler = handler; handler.BeginLinkTraversal(_currentLinkType, _currentLinkStart, _currentLinkEnd, onComplete: () => { _activeHandler = null; _navAgent?.CompleteLinkTraversal(); }); return; } // CanHandleLink 返回 false 且 TBM 已禁用 → NavAgent 会卡住,警告设计者 Debug.LogWarning($"[EnemyNavAgent] '{handler.GetType().Name}' reported CanHandleLink=false " + $"for {_currentLinkType} (TBM disabled). NavAgent may stall. " + $"Check link distance/height vs. ability parameters.", this); } // 无 handler → TBM 兜底(其 FeatureFlag 仍开启) } private void HandleSegmentTraversal(NavAgent agent) { if (_currentLinkType != NavLinkType.None && !agent.IsOnLink) { var finished = _currentLinkType; _currentLinkType = NavLinkType.None; _currentLinkStart = Vector2.zero; _currentLinkEnd = Vector2.zero; OnLinkCompleted?.Invoke(finished); } } private void HandlePathFailed(NavAgent _) { _obstructionSignal = true; // 延后到 FixedUpdate 统一处理,避免在 NavAgent 失败回调内重入 UpdatePath OnNavPathFailed?.Invoke(); } private void HandleGoalReached(NavAgent _) => OnGoalReached?.Invoke(); // ── 物理原生移动桥 ───────────────────────────────────────────── /// /// 跟随路径段时:向 写入朝当前子目标的水平方向+速度(由 velocity+重力执行), /// 并在到达子目标时推进导航到下一段(TBM 段移动已在 Awake 禁用,改由本组件推进)。 /// 不直接写 transform.position → 与 Dynamic 刚体物理不冲突,保留击退/击飞/落地等物理反馈。 /// private void FixedUpdate() { if (_enemyMovement == null || _navAgent == null) return; bool onSegment = _navAgent.IsMovingOnSegment; _enemyMovement.NavDriving = onSegment; // 状态标记(供调试/上层判断"是否导航中") TickStuckDetection(onSegment); if (onSegment) { // 物理原生:朝子目标水平移动,由 EnemyMovement 施加 velocity、刚体重力保持贴地 float dx = _navAgent.PathSubGoal.x - transform.position.x; _enemyMovement.PendingInput.MoveDir = Mathf.Abs(dx) > 0.05f ? Mathf.Sign(dx) : 0f; _enemyMovement.PendingInput.MoveSpeed = _movement != null ? _movement.movementSpeed : 0f; // 段推进:到达当前子目标 → 推进到下一段 if (_navAgent.HasReachedCurrentSubGoal(0.25f)) _navAgent.CompleteSegmentTraversal(); } else if (_wasNavOnSegment && !_navAgent.IsFollowingAPath) { // 导航刚结束(到达目标 / 路径失败)→ 停下水平移动 _enemyMovement.PendingInput.WantStop = true; } // 受阻信号统一处理(寻路失败/卡死):此处不在 NavAgent 回调内,UpdatePath 安全 if (_obstructionSignal) { _obstructionSignal = false; ProcessObstruction(); } _wasNavOnSegment = onSegment; } // 卡死检测:段上移动时,滑动窗口内朝子目标推进不足阈值 → 判受阻(覆盖"障碍未进图"的物理空推)。 private void TickStuckDetection(bool onSegment) { if (!onSegment) { _stuckWinInit = false; return; } float dist = Mathf.Abs(_navAgent.PathSubGoal.x - transform.position.x); if (!_stuckWinInit) { _stuckWinInit = true; _stuckWinTimer = 0f; _stuckWinStartDist = dist; return; } _stuckWinTimer += Time.fixedDeltaTime; if (_stuckWinTimer >= _stuckWindowSeconds) { if (_stuckWinStartDist - dist < _stuckMinProgress) _obstructionSignal = true; // 窗口内几乎没靠近子目标 → 卡死 _stuckWinTimer = 0f; _stuckWinStartDist = dist; } } private void ResetStuckWindow() => _stuckWinInit = false; // 统一受阻处理:尽力改道到最近可达点(走到障碍前),拿不到更近点则停下,交由意图层结算。 private void ProcessObstruction() { _lastMoveObstructed = true; if (!_redirectedToClosest && _navAgent.TryGetClosestReachablePosition(out var closest) && Vector2.Distance(transform.position, closest) > _repathTargetThreshold) { _redirectedToClosest = true; ResetStuckWindow(); _navAgent.UpdatePath(closest); // 走向最近可达点 return; } // 无更近点 / 已在最近点附近(含"障碍未进图"无 closest 数据)→ 停下 _navAgent.Stop(); _enemyMovement.PendingInput.WantStop = true; } // ── 工具 ─────────────────────────────────────────────────────── private static NavLinkType ParseLinkType(string name) => name switch { "corner" => NavLinkType.Corner, "jump" => NavLinkType.Jump, "fall" => NavLinkType.Fall, "teleport" => NavLinkType.Teleport, "climb" => NavLinkType.Climb, "elevator" => NavLinkType.Elevator, null or "" => NavLinkType.Segment, _ => NavLinkType.Custom }; /// NavLinkType → TransformBasedMovement.FeatureFlags 映射。 private static TransformBasedMovement.FeatureFlags TypeToFeatureFlag(NavLinkType type) => type switch { NavLinkType.Jump => TransformBasedMovement.FeatureFlags.JumpLinks, NavLinkType.Fall => TransformBasedMovement.FeatureFlags.FallLinks, NavLinkType.Corner => TransformBasedMovement.FeatureFlags.CornerLinks, NavLinkType.Climb => TransformBasedMovement.FeatureFlags.ClimbLinks, NavLinkType.Elevator => TransformBasedMovement.FeatureFlags.ElevatorLinks, NavLinkType.Teleport => TransformBasedMovement.FeatureFlags.TeleportLinks, _ => (TransformBasedMovement.FeatureFlags)0 }; } }