406 lines
20 KiB
C#
406 lines
20 KiB
C#
using UnityEngine;
|
||
using PathBerserker2d;
|
||
using BaseGames.Enemies;
|
||
using System;
|
||
using System.Collections.Generic;
|
||
|
||
namespace BaseGames.Enemies.Navigation
|
||
{
|
||
/// <summary>
|
||
/// PathBerserker2d 导航代理包装器(架构 07_EnemyModule §5 能力驱动版)。
|
||
///
|
||
/// 设计原则:
|
||
/// <list type="bullet">
|
||
/// <item>连接段穿越(跳跃/攀爬/传送等)委托给 <see cref="INavLinkHandler"/> 能力组件执行</item>
|
||
/// <item>能力自行驱动动画 + 物理,本组件只做"调度 → 等回调 → 通知 PB2d 继续"</item>
|
||
/// <item>无对应能力时自动回退到 TransformBasedMovement(兜底行为)</item>
|
||
/// <item>Awake 自动发现 GameObject 上的全部 INavLinkHandler,并禁用 TBM 对应 FeatureFlag</item>
|
||
/// </list>
|
||
/// </summary>
|
||
[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;
|
||
|
||
/// <summary>上次寻路移动是否因不可达/被挡而受阻(走到最近点或原地停,而非到达原目标)。新目标 RequestMoveTo 后复位。</summary>
|
||
public bool LastMoveObstructed => _lastMoveObstructed;
|
||
|
||
/// <summary>取上次寻路失败时算出的最近可达世界点。无则返回 false。</summary>
|
||
public bool TryGetClosestReachable(out Vector2 worldPos)
|
||
{
|
||
if (_navAgent != null) return _navAgent.TryGetClosestReachablePosition(out worldPos);
|
||
worldPos = Vector2.zero;
|
||
return false;
|
||
}
|
||
|
||
// ── IPathAgent 事件 ────────────────────────────────────────────
|
||
public event Action<NavLinkType> OnLinkStarted;
|
||
public event Action<NavLinkType> OnLinkCompleted;
|
||
public event Action OnNavPathFailed;
|
||
public event Action OnGoalReached;
|
||
|
||
// ── 私有 ────────────────────────────────────────────────────────
|
||
private NavAgent _navAgent;
|
||
private TransformBasedMovement _movement;
|
||
private EnemyMovement _enemyMovement;
|
||
|
||
// 能力 handler 注册表(NavLinkType → INavLinkHandler)
|
||
private readonly Dictionary<NavLinkType, INavLinkHandler> _handlers
|
||
= new Dictionary<NavLinkType, INavLinkHandler>();
|
||
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<NavAgent>();
|
||
_movement = GetComponent<TransformBasedMovement>();
|
||
_enemyMovement = GetComponent<EnemyMovement>();
|
||
|
||
// 物理原生移动:禁用 TBM 段移动——不让它直接写 transform.position(会与 Dynamic 刚体物理冲突)。
|
||
// 段上的水平移动改由 EnemyMovement 用 velocity 执行、刚体重力保持贴地(见 FixedUpdate)。
|
||
if (_movement != null)
|
||
_movement.enabledFeatures &= ~TransformBasedMovement.FeatureFlags.SegmentMovement;
|
||
|
||
// 自动发现 INavLinkHandler 组件并注册(包含子对象)
|
||
foreach (var handler in GetComponentsInChildren<INavLinkHandler>(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 注册 ───────────────────────────────────────────────
|
||
/// <summary>
|
||
/// 注册能力为某连接段类型的处理器。
|
||
/// 注册后 TransformBasedMovement 中对应 FeatureFlag 将被禁用,由能力全权负责该类型穿越。
|
||
/// </summary>
|
||
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;
|
||
}
|
||
}
|
||
|
||
/// <summary>移除某处理器(例如能力被永久禁用时)。TBM FeatureFlag 自动恢复。</summary>
|
||
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 直接访问 ──────────────────────────────────────
|
||
/// <summary>原始 PB2d NavAgent,供需要 PB2d 高级功能的能力直接使用。</summary>
|
||
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();
|
||
|
||
// ── 物理原生移动桥 ─────────────────────────────────────────────
|
||
/// <summary>
|
||
/// 跟随路径段时:向 <see cref="EnemyMovement"/> 写入朝当前子目标的水平方向+速度(由 velocity+重力执行),
|
||
/// 并在到达子目标时推进导航到下一段(TBM 段移动已在 Awake 禁用,改由本组件推进)。
|
||
/// 不直接写 transform.position → 与 Dynamic 刚体物理不冲突,保留击退/击飞/落地等物理反馈。
|
||
/// </summary>
|
||
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();
|
||
// 同步防抖记账:改道等价于把当前寻路目标换成 closest,避免 _lastPathGoal/_lastPathTime
|
||
// 停留在原(受阻)目标,导致下一次对新目标的 RequestMoveTo 被防抖误吞。
|
||
_lastPathGoal = closest;
|
||
_lastPathTime = Time.time;
|
||
_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
|
||
};
|
||
|
||
/// <summary>NavLinkType → TransformBasedMovement.FeatureFlags 映射。</summary>
|
||
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
|
||
};
|
||
}
|
||
}
|
||
|