Files
zeling_v2/Assets/_Game/Scripts/Enemies/Navigation/EnemyNavAgent.cs
Joywayer 06048c966a feat: Add HurtBoxOwnerGuard to prevent multiple damage registrations from the same HitBox activation
- Implemented HurtBoxOwnerGuard to ensure that multiple HurtBoxes on the same character do not register damage multiple times during a single HitBox activation.
- Added custom editor for HitBox to facilitate the creation of shape colliders with HitBoxColliderProxy.
- Introduced PhysicsPerceptionSystem for enemy perception, supporting multiple detection modes including RangeCircle, BatchLOS, FanCast, and BoxCast.
- Created EnemyPatrolZone to define patrol and chase areas for enemies, allowing for shared zones among multiple enemies.
- Added BD_IsOutsideZone conditional task for Behavior Designer to check if an enemy or player is outside a defined patrol zone.
2026-06-02 16:10:44 +08:00

255 lines
12 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;
// ── 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>();
// 自动发现 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) => _navAgent?.UpdatePath(target);
public void StopNavigation()
{
_activeHandler?.AbortLinkTraversal();
_activeHandler = null;
_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;
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();
// ── NavDriving 信号桥 ───────────────────────────────────────────
/// <summary>
/// 每物理帧向 <see cref="EnemyMovement"/> 写入导航方向信号,并设置 NavDriving 标志。
/// NavDriving=true 时 EnemyMovement 只更新朝向TBM 保留对 transform.position 的控制权。
/// </summary>
private void FixedUpdate()
{
if (_enemyMovement == null || _navAgent == null) return;
bool onSegment = _navAgent.IsMovingOnSegment;
_enemyMovement.NavDriving = onSegment;
if (onSegment)
{
float dx = _navAgent.PathSubGoal.x - transform.position.x;
_enemyMovement.PendingInput.MoveDir = Mathf.Abs(dx) > 0.01f ? Mathf.Sign(dx) : 0f;
}
else if (_wasNavOnSegment && !_navAgent.IsFollowingAPath)
{
// 导航刚结束(到达目标 / 路径失败)→ 清除残留 MoveDir
_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
};
}
}