Files
zeling_v2/Assets/_Game/Scripts/Enemies/Navigation/EnemyNavAgent.cs
T

406 lines
20 KiB
C#
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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
};
}
}