diff --git a/Assets/_Game/Scripts/Editor/BaseGames.Editor.asmdef b/Assets/_Game/Scripts/Editor/BaseGames.Editor.asmdef index 4a57d278..fa386782 100644 --- a/Assets/_Game/Scripts/Editor/BaseGames.Editor.asmdef +++ b/Assets/_Game/Scripts/Editor/BaseGames.Editor.asmdef @@ -24,7 +24,6 @@ "BaseGames.Feedback", "BaseGames.Dialogue", "BaseGames.Progression", - "PathBerserker2d", "Unity.Cinemachine", "Kybernetik.Animancer", "BaseGames.Animation", diff --git a/Assets/_Game/Scripts/Editor/Enemies/Navigation/NavSurfaceBakeShortcut.cs b/Assets/_Game/Scripts/Editor/Enemies/Navigation/NavSurfaceBakeShortcut.cs deleted file mode 100644 index ab0305a1..00000000 --- a/Assets/_Game/Scripts/Editor/Enemies/Navigation/NavSurfaceBakeShortcut.cs +++ /dev/null @@ -1,86 +0,0 @@ -using System.Reflection; -using UnityEditor; -using UnityEngine; -using PathBerserker2d; - -namespace BaseGames.Editor -{ - /// - /// 快捷键:BaseGames → Tools → Bake All NavSurfaces(Ctrl+Shift+B) - /// 烘焙当前场景中所有 PathBerserker2d NavSurface 的导航网格。 - /// 等效于在每个 NavSurface Inspector 中逐一点击 "Bake"。 - /// - public static class NavSurfaceBakeShortcut - { - // NavSurface.StartBakeJob() 和 NavSurface.BakeJob 均为 internal,通过反射访问。 - private static readonly MethodInfo s_startBakeJobMethod = - typeof(NavSurface).GetMethod("StartBakeJob", BindingFlags.NonPublic | BindingFlags.Instance); - private static readonly PropertyInfo s_bakeJobProp = - typeof(NavSurface).GetProperty("BakeJob", BindingFlags.NonPublic | BindingFlags.Instance); - - [MenuItem("BaseGames/Scene/Bake All NavSurfaces %#b", priority = 100)] - public static void BakeAll() - { - var surfaces = Object.FindObjectsByType(FindObjectsSortMode.None); - if (surfaces.Length == 0) - { - Debug.Log("[NavSurfaceBake] 当前场景没有找到 NavSurface 组件。"); - return; - } - - int count = 0; - foreach (var surface in surfaces) - { - if (surface == null) continue; - - s_startBakeJobMethod?.Invoke(surface, null); - EditorApplication.update -= MakeWatcher(surface); - EditorApplication.update += MakeWatcher(surface); - count++; - } - - Debug.Log($"[NavSurfaceBake] 开始烘焙 {count} 个 NavSurface……"); - } - - [MenuItem("BaseGames/Scene/Bake All NavSurfaces %#b", validate = true)] - private static bool BakeAllValidate() - { - // 仅在非 Play Mode 时可用(NavSurface.Bake 仅支持编辑器模式) - return !Application.isPlaying; - } - - // ── 每个 NavSurface 独立监听烘焙完成 ────────────────────────────── - - private static EditorApplication.CallbackFunction MakeWatcher(NavSurface surface) - { - EditorApplication.CallbackFunction watcher = null; - watcher = () => - { - if (surface == null) - { - EditorApplication.update -= watcher; - return; - } - - var bakeJob = s_bakeJobProp?.GetValue(surface); - if (bakeJob == null) - { - EditorApplication.update -= watcher; - return; - } - - bool isFinished = (bool)bakeJob.GetType() - .GetProperty("IsFinished")!.GetValue(bakeJob); - if (isFinished) - { - EditorApplication.update -= watcher; - EditorUtility.SetDirty(surface); - float totalTime = (float)bakeJob.GetType() - .GetProperty("TotalBakeTime")!.GetValue(bakeJob); - Debug.Log($"[NavSurfaceBake] ✓ {surface.name} 烘焙完成({totalTime} ms)"); - } - }; - return watcher; - } - } -} diff --git a/Assets/_Game/Scripts/Editor/Enemies/Navigation/NavSurfaceBakeShortcut.cs.meta b/Assets/_Game/Scripts/Editor/Enemies/Navigation/NavSurfaceBakeShortcut.cs.meta deleted file mode 100644 index 723f231f..00000000 --- a/Assets/_Game/Scripts/Editor/Enemies/Navigation/NavSurfaceBakeShortcut.cs.meta +++ /dev/null @@ -1,11 +0,0 @@ -fileFormatVersion: 2 -guid: 291f3b33fb176b8469ebaaa8afa317ba -MonoImporter: - externalObjects: {} - serializedVersion: 2 - defaultReferences: [] - executionOrder: 0 - icon: {instanceID: 0} - userData: - assetBundleName: - assetBundleVariant: diff --git a/Assets/_Game/Scripts/Editor/Scene/SceneObjectPlacerTool.cs b/Assets/_Game/Scripts/Editor/Scene/SceneObjectPlacerTool.cs index 76fd65f9..0d342133 100644 --- a/Assets/_Game/Scripts/Editor/Scene/SceneObjectPlacerTool.cs +++ b/Assets/_Game/Scripts/Editor/Scene/SceneObjectPlacerTool.cs @@ -21,7 +21,6 @@ using BaseGames.Player; using BaseGames.Player.States; using BaseGames.Skills; using BaseGames.World; -using PathBerserker2d; using Unity.Cinemachine; using UnityEditor; using UnityEngine; @@ -303,10 +302,7 @@ namespace BaseGames.Editor EnemyBase enemyBase = GetOrAddComponent(go); EnemyStats enemyStats = GetOrAddComponent(go); EnemyMovement movement = GetOrAddComponent(go); - GetOrAddComponent(go); - NavAgent navAgent = GetOrAddComponent(go); - AssignBool(navAgent, "allowCloseEnoughPath", true); // 追击近似目标(玩家常不在导航段上)寻路到最近可达点 - GetOrAddComponent(go); // required by EnemyNavAgent [RequireComponent] + GetOrAddComponent(go); // 地面导航(直接移动,无寻路图) PhysicsPerceptionSystem sensorHub = GetOrAddComponent(go); var (hurtBox, bodyContact, contactHitBox) = @@ -405,7 +401,7 @@ namespace BaseGames.Editor SetupPerceptionSystemSlots(sensorHub, new[] { "aggro", "attack_melee", "attack_range", "los" }, report); report.Add("填写 _bossId。"); - report.Add("★ 挂载 BossSkillExecutor 并指定 BossSkillSO 列表(技能执行层);NavAgent 需手工添加。"); + report.Add("★ 挂载 BossSkillExecutor 并指定 BossSkillSO 列表(技能执行层)。"); report.Add("★ AI(BrainGraph):先写一个 [AiDefinition(\"\")] 的 AiScript 子类," + "再挂 EnemyAiBrain 组件并把 _definitionId 设为该 id。"); report.Add("多阶段 Boss 可在此 GameObject 上继续 AddComponent 阶段切换控制器。"); @@ -448,10 +444,7 @@ namespace BaseGames.Editor EnemyBase enemyBase = GetOrAddComponent(go); EnemyStats enemyStats = GetOrAddComponent(go); EnemyMovement movement = GetOrAddComponent(go); - GetOrAddComponent(go); - NavAgent navAgent = GetOrAddComponent(go); - AssignBool(navAgent, "allowCloseEnoughPath", true); // 追击近似目标(玩家常不在导航段上)寻路到最近可达点 - GetOrAddComponent(go); // required by EnemyNavAgent [RequireComponent] + GetOrAddComponent(go); // 地面导航(直接移动,无寻路图) PhysicsPerceptionSystem sensorHub = GetOrAddComponent(go); var (hurtBox, bodyContact, contactHitBox) = @@ -545,9 +538,7 @@ namespace BaseGames.Editor EnemyBase enemyBase = GetOrAddComponent(go); EnemyStats enemyStats = GetOrAddComponent(go); EnemyMovement movement = GetOrAddComponent(go); - GetOrAddComponent(go); - GetOrAddComponent(go); - GetOrAddComponent(go); // required by EnemyNavAgent [RequireComponent] + GetOrAddComponent(go); // 地面导航(直接移动,无寻路图) PhysicsPerceptionSystem sensorHub = GetOrAddComponent(go); // HurtBox + ContactDamageZone(身体接触伤害常驻开启;HurtBox 悬挂期外关闭见下) @@ -647,10 +638,7 @@ namespace BaseGames.Editor EnemyBase enemyBase = GetOrAddComponent(go); EnemyStats enemyStats = GetOrAddComponent(go); EnemyMovement movement = GetOrAddComponent(go); - GetOrAddComponent(go); - NavAgent navAgent = GetOrAddComponent(go); - AssignBool(navAgent, "allowCloseEnoughPath", true); // 追击近似目标(玩家常不在导航段上)寻路到最近可达点 - GetOrAddComponent(go); // required by EnemyNavAgent [RequireComponent] + GetOrAddComponent(go); // 地面导航(直接移动,无寻路图) PhysicsPerceptionSystem sensorHub = GetOrAddComponent(go); var (hurtBox, bodyContact, contactHitBox) = @@ -739,9 +727,7 @@ namespace BaseGames.Editor EnemyStats enemyStats = GetOrAddComponent(go); EnemyFeedback feedback = GetOrAddComponent(go); EnemyMovement movement = GetOrAddComponent(go); - GetOrAddComponent(go); - GetOrAddComponent(go); - GetOrAddComponent(go); // required by EnemyNavAgent [RequireComponent] + GetOrAddComponent(go); // 地面导航(直接移动,无寻路图) PhysicsPerceptionSystem sensorHub = GetOrAddComponent(go); var (hurtBox, bodyContact, contactHitBox) = @@ -870,9 +856,7 @@ namespace BaseGames.Editor EnemyBase enemyBase = GetOrAddComponent(go); EnemyStats enemyStats = GetOrAddComponent(go); EnemyMovement movement = GetOrAddComponent(go); - GetOrAddComponent(go); - GetOrAddComponent(go); - GetOrAddComponent(go); // required by EnemyNavAgent [RequireComponent] + GetOrAddComponent(go); // 地面导航(直接移动,无寻路图) PhysicsPerceptionSystem sensorHub = GetOrAddComponent(go); var (hurtBox, bodyContact, contactHitBox) = @@ -982,10 +966,7 @@ namespace BaseGames.Editor EnemyBase enemyBase = GetOrAddComponent(go); EnemyStats enemyStats = GetOrAddComponent(go); EnemyMovement movement = GetOrAddComponent(go); - GetOrAddComponent(go); - NavAgent navAgent = GetOrAddComponent(go); - AssignBool(navAgent, "allowCloseEnoughPath", true); // 追击近似目标(玩家常不在导航段上)寻路到最近可达点 - GetOrAddComponent(go); // required by EnemyNavAgent [RequireComponent] + GetOrAddComponent(go); // 地面导航(直接移动,无寻路图) PhysicsPerceptionSystem sensorHub = GetOrAddComponent(go); var (hurtBox, bodyContact, contactHitBox) = @@ -1086,9 +1067,7 @@ namespace BaseGames.Editor EnemyStats bossStats = GetOrAddComponent(go); EnemyFeedback feedback = GetOrAddComponent(go); EnemyMovement movement = GetOrAddComponent(go); - GetOrAddComponent(go); - GetOrAddComponent(go); - GetOrAddComponent(go); // required by EnemyNavAgent [RequireComponent] + GetOrAddComponent(go); // 地面导航(直接移动,无寻路图) BossSkillExecutor skillExec = GetOrAddComponent(go); ChaoFengFloatController floatCtrl = GetOrAddComponent(go); ChaoFengKnockdownCounter knockdown = GetOrAddComponent(go); @@ -2082,22 +2061,6 @@ namespace BaseGames.Editor MarkDirtyAndLog("Tilemap Ground", gridGo, report); } - [MenuItem("BaseGames/Scene/Place/Nav Surface", priority = 170)] - public static void PlaceNavSurface() - { - var report = new List(); - - GameObject go = new GameObject("NavSurface"); - Undo.RegisterCreatedObjectUndo(go, "Place Nav Surface"); - go.transform.position = GetDropPosition(); - GetOrAddComponent(go); - - report.Add("NavSurface 已添加。在 Inspector 中点击 Bake 生成导航网格。"); - - Selection.activeGameObject = go; - MarkDirtyAndLog("Nav Surface", go, report); - } - [MenuItem("BaseGames/Scene/Place/Obstacle (Static)", priority = 190)] public static void PlaceObstacle() { @@ -2185,14 +2148,14 @@ namespace BaseGames.Editor col = box; break; } - AlignColliderBottomToPivot(col); // 底部对齐 y=0:原点=脚底,供 NavAgent 检测原点落在导航线上 + AlignColliderBottomToPivot(col); // 底部对齐 y=0:原点=脚底 return col; } /// /// 把碰撞体底部对齐到 transform 原点(y=0)——原点即脚底。 - /// PathBerserker2d NavAgent 用 transform 原点(0,0)判定是否在导航段上,故所有创建物的碰撞体底部须在 y=0, - /// 否则站到地面时原点悬在导航线上方,导致 HasValidPosition=False、寻路失效。 + /// 所有创建物的碰撞体底部对齐 y=0(原点=脚底),使地面探测与站位判定以脚底为基准, + /// 否则站到地面时原点悬在地面上方,导致地面检测/落点判定出现偏差。 /// private static void AlignColliderBottomToPivot(Collider2D col) { diff --git a/Assets/_Game/Scripts/Editor/Scene/SceneScaffoldTools.cs b/Assets/_Game/Scripts/Editor/Scene/SceneScaffoldTools.cs index c091f8c2..4f45ce31 100644 --- a/Assets/_Game/Scripts/Editor/Scene/SceneScaffoldTools.cs +++ b/Assets/_Game/Scripts/Editor/Scene/SceneScaffoldTools.cs @@ -15,7 +15,6 @@ using BaseGames.UI.Splash; using BaseGames.World; using BaseGames.World.Map; using BaseGames.World.Streaming; -using PathBerserker2d; using Unity.Cinemachine; using UnityEditor; using UnityEditor.SceneManagement; @@ -764,10 +763,6 @@ namespace BaseGames.Editor groundRb.bodyType = RigidbodyType2D.Static; GetOrAddComponent(groundTileGo); - // NavSurface for PathBerserker2d - GameObject navGo = GetOrCreateChild(envGroup, "NavSurface").gameObject; - GetOrAddComponent(navGo); - // ── [Transitions] ────────────────────────────────────────────── GetOrCreateChild(root.transform, "[Transitions]"); @@ -813,7 +808,7 @@ namespace BaseGames.Editor else report.Add($"MapRoomDataSO 已存在({roomSoPath}),请确认其 RoomId / Exits 与场景保持同步。"); report.Add("绑定 LensConfig SO 后单击 Inspector 中「从可视区域更新限位区域」计算正确的 BoxCollider。"); - report.Add("使用 Tile Palette 在 Ground Tilemap 上绘制地形,然后在 NavSurface Inspector 中点击 Bake。"); + report.Add("使用 Tile Palette 在 Ground Tilemap 上绘制地形。"); report.Add("[Transitions] 子节点下使用 BaseGames/Scene/Place/Room Transition 添加过渡点。"); MarkDirtyAndLog("Game Room 脚手架", root, report); diff --git a/Assets/_Game/Scripts/Enemies/IPathAgent.cs b/Assets/_Game/Scripts/Enemies/IPathAgent.cs deleted file mode 100644 index bc41b14a..00000000 --- a/Assets/_Game/Scripts/Enemies/IPathAgent.cs +++ /dev/null @@ -1,127 +0,0 @@ -using System; -using UnityEngine; - -namespace BaseGames.Enemies -{ - // ── 连接段类型(映射 PB2d LinkTypeName 字符串)─────────────────────── - public enum NavLinkType - { - None, - Segment, // 普通地面/平台段 - Jump, - Fall, - Corner, - Climb, - Elevator, - Teleport, - Custom - } - - /// - /// 导航代理抽象接口(架构 07_EnemyModule §5)。 - /// EnemyBase 和 BD Task 只依赖此接口,不依赖具体导航库。 - /// 实现:EnemyNavAgent(PathBerserker2d);测试用 NullPathAgent。 - /// - public interface IPathAgent - { - // ── 核心移动 ──────────────────────────────────────────────────── - /// 请求移动到世界坐标 target。 - void RequestMoveTo(Vector2 target); - - /// 立即停止导航(清除路径)。 - void StopNavigation(); - - /// 是否已到达目标(距离 ≤ stoppingDistance)。 - bool IsAtDestination(); - - /// 运行时覆盖移动速度。 - void SetSpeed(float speed); - - /// 当前帧是否在移动(速度 > 0.01 且有有效路径)。 - bool IsMoving { get; } - - /// 是否接近平台边缘(脚下或前方无地面时为 true)。 - bool IsNearEdge(); - - // ── 连接段感知(NavLink 平台跳跃/爬梯/传送)───────────────────── - /// 当前是否正在穿越连接段(跳跃/下落/爬梯等)。 - bool IsOnLink { get; } - - /// 当前正在穿越的连接段类型;不在连接段时为 None。 - NavLinkType CurrentLinkType { get; } - - /// 当前连接段的起始世界坐标;不在连接段时为 Vector2.zero。 - Vector2 CurrentLinkStart { get; } - - /// 当前连接段的目标世界坐标;不在连接段时为 Vector2.zero。 - Vector2 CurrentLinkEnd { get; } - - /// 检查目标是否可以从当前位置导航到达(同步轻量查询)。 - bool CanReach(Vector2 target); - - /// 移动到随机可到达点(巡逻 fallback)。 - bool WalkToRandom(); - - /// - /// 段内随机游走:随机点限制在 agent 当前所站的这一条导航段上,不跨越任何 NavLink(跳/落/拐角)。 - /// 供 Wander 巡逻使用——地面单位只在当前平台段内来回,避免被崖边夹停或卡在过不去的 link 处。 - /// 未映射到导航段(不在图上)时返回 false。 - /// - bool WalkToRandomOnSegment(); - - /// - /// 目标点吸附:把任意世界 映射到最近可行走段、并按敌人身体宽度从段两端内缩, - /// 得到"身体真正站得住"的点 (避免落在角色宽度站不住的崖沿被移动层夹停)。 - /// 身体宽度只属于导航执行层——意图层(AI/Locomotion)递原始目标即可,无需自行计算宽度。 - /// target 离导航面太远(超出搜索半径)时返回 false,standable 回落为原始 target。 - /// - bool ResolveStandablePoint(Vector2 target, out Vector2 standable); - - /// - /// 上次寻路移动是否因目标不可达/被挡而受阻(走到最近可达点或原地停下,而非到达原目标)。 - /// 供意图层(Locomotion)判断本次移动应否视为结算/推进。对新目标 RequestMoveTo 后复位。 - /// - bool LastMoveObstructed { get; } - - /// 取上次寻路失败时算出的最近可达世界点(尽力贴近目标的点)。无则返回 false。 - bool TryGetClosestReachable(out Vector2 worldPos); - - // ── 连接段事件 ────────────────────────────────────────────────── - /// 开始穿越连接段时触发(传入连接段类型)。 - event Action OnLinkStarted; - - /// 连接段穿越完成时触发。 - event Action OnLinkCompleted; - - /// 路径寻路失败事件(目标不可达时触发)。 - event Action OnNavPathFailed; - - /// 到达目标事件(替代轮询 IsAtDestination)。 - event Action OnGoalReached; - } - - // ── 无导航 / 测试用空实现 ───────────────────────────────────────────── - public sealed class NullPathAgent : IPathAgent - { - public void RequestMoveTo(Vector2 _) { } - public void StopNavigation() { } - public bool IsAtDestination() => true; - public void SetSpeed(float _) { } - public bool IsMoving => false; - public bool IsNearEdge() => false; - public bool IsOnLink => false; - public NavLinkType CurrentLinkType => NavLinkType.None; - public Vector2 CurrentLinkStart => Vector2.zero; - public Vector2 CurrentLinkEnd => Vector2.zero; - public bool CanReach(Vector2 _) => false; - public bool WalkToRandom() => false; - public bool WalkToRandomOnSegment() => false; - public bool ResolveStandablePoint(Vector2 target, out Vector2 standable) { standable = target; return false; } - public bool LastMoveObstructed => false; - public bool TryGetClosestReachable(out Vector2 worldPos) { worldPos = Vector2.zero; return false; } - public event Action OnLinkStarted { add { } remove { } } - public event Action OnLinkCompleted{ add { } remove { } } - public event Action OnNavPathFailed { add { } remove { } } - public event Action OnGoalReached { add { } remove { } } - } -} diff --git a/Assets/_Game/Scripts/Enemies/IPathAgent.cs.meta b/Assets/_Game/Scripts/Enemies/IPathAgent.cs.meta deleted file mode 100644 index b9f413ea..00000000 --- a/Assets/_Game/Scripts/Enemies/IPathAgent.cs.meta +++ /dev/null @@ -1,11 +0,0 @@ -fileFormatVersion: 2 -guid: 99f648f6975de20469192afe7fcc4e0a -MonoImporter: - externalObjects: {} - serializedVersion: 2 - defaultReferences: [] - executionOrder: 0 - icon: {instanceID: 0} - userData: - assetBundleName: - assetBundleVariant: diff --git a/Assets/_Game/Scripts/Enemies/Navigation/BaseGames.Enemies.Navigation.asmdef b/Assets/_Game/Scripts/Enemies/Navigation/BaseGames.Enemies.Navigation.asmdef index be5a1583..9a1c2d72 100644 --- a/Assets/_Game/Scripts/Enemies/Navigation/BaseGames.Enemies.Navigation.asmdef +++ b/Assets/_Game/Scripts/Enemies/Navigation/BaseGames.Enemies.Navigation.asmdef @@ -9,8 +9,7 @@ "rootNamespace": "BaseGames.Enemies.Navigation", "references": [ "BaseGames.Enemies", - "BaseGames.AI", - "PathBerserker2d" + "BaseGames.AI" ], "autoReferenced": true, "overrideReferences": false, diff --git a/Assets/_Game/Scripts/Enemies/Navigation/EnemyNavAgent.cs b/Assets/_Game/Scripts/Enemies/Navigation/EnemyNavAgent.cs deleted file mode 100644 index f9ee5561..00000000 --- a/Assets/_Game/Scripts/Enemies/Navigation/EnemyNavAgent.cs +++ /dev/null @@ -1,391 +0,0 @@ -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 StallDetector _segStall; - - // 连接段状态缓存 - 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; - - _segStall = new StallDetector(_stuckWindowSeconds, _stuckMinProgress); - } - - 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) { _segStall.Reset(transform.position.x); return; } - if (_segStall.Tick(transform.position.x, Time.fixedDeltaTime)) - _obstructionSignal = true; // 窗口内几乎没推进 → 卡死 - } - - private void ResetStuckWindow() => _segStall.Reset(transform.position.x); - - // 统一受阻处理:尽力改道到最近可达点(走到障碍前),拿不到更近点则停下,交由意图层结算。 - 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 - }; - - /// 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 - }; - } -} - diff --git a/Assets/_Game/Scripts/Enemies/Navigation/EnemyNavAgent.cs.meta b/Assets/_Game/Scripts/Enemies/Navigation/EnemyNavAgent.cs.meta deleted file mode 100644 index 46fd2196..00000000 --- a/Assets/_Game/Scripts/Enemies/Navigation/EnemyNavAgent.cs.meta +++ /dev/null @@ -1,11 +0,0 @@ -fileFormatVersion: 2 -guid: 44871319d7318de40b9ac21757b69c78 -MonoImporter: - externalObjects: {} - serializedVersion: 2 - defaultReferences: [] - executionOrder: 0 - icon: {instanceID: 0} - userData: - assetBundleName: - assetBundleVariant: