Files
zeling_v2/Assets/_Game/Scripts/Enemies/Navigation/EnemyNavAgent.cs
T
joywayerandClaude Opus 4.8 66d100f353 fix(enemy): 目标点吸附改用固定大搜索距离,与 PB2d 映射任意点机制一致
上版把搜索距离绑到 agent→目标距离(仿 UpdatePath),仍脆弱:吸附距离取决于 agent 离得多远
而非目标离导航段多远——agent 贴在高处路点正下方时距离小、reach 小仍会漏。

查明 PB2d 映射机制(NavGraph.TryMapPointSurface):queryAABB 边长=传入值、可靠搜索半径仅其一半;
PB2d 映射"鼠标点击等任意点"用固定的 closestToSegmentMaxDistance(默认 20,"想多大就多大")。
据此改为:EnemyNavAgent._maxSnapDistance(默认 10m,真实"点离最近段"上限),底层传 2× 补偿 AABB 半宽。
只要目标在 _maxSnapDistance 内一定吸附到最近烘焙段,返回 false 仅表示目标真正脱离导航面。

Play 实测:两路点 + 压力用例(agent 正上方 3m/5m)ResolveStandablePoint 均 ok=True 且映射到地面段。

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-23 10:35:24 +08:00

314 lines
16 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;
// 重寻路防抖状态
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;
// ── 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 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;
_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 _) => 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; // 状态标记(供调试/上层判断"是否导航中"
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;
// 段推进:到达当前子目标 → 推进到下一段(原由 TBM 负责,禁用后由本组件负责)
if (_navAgent.HasReachedCurrentSubGoal(0.25f))
_navAgent.CompleteSegmentTraversal();
}
else if (_wasNavOnSegment && !_navAgent.IsFollowingAPath)
{
// 导航刚结束(到达目标 / 路径失败)→ 停下水平移动
_enemyMovement.PendingInput.WantStop = true;
}
_wasNavOnSegment = onSegment;
}
// ── 工具 ───────────────────────────────────────────────────────
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
};
}
}