diff --git a/Assets/PathBerserker2d/Scripts/PathBerserker2d/NavAgent/NavAgent.cs b/Assets/PathBerserker2d/Scripts/PathBerserker2d/NavAgent/NavAgent.cs index 6ce3a13c..fb5a725e 100644 --- a/Assets/PathBerserker2d/Scripts/PathBerserker2d/NavAgent/NavAgent.cs +++ b/Assets/PathBerserker2d/Scripts/PathBerserker2d/NavAgent/NavAgent.cs @@ -594,6 +594,32 @@ namespace PathBerserker2d return PathTo(goal); } + // [项目新增] 段内随机游走:目标点限制在 agent 当前所站的这一条 NavSegment(cluster) 上, + // 不会跨越任何 NavLink(跳/落/拐角),避免地面单位游走时被崖边夹停或卡在无法穿越的 link 处。 + // 需要 agent 当前已映射到某条段上(currentMappedPosition 有效),否则返回 false。 + // 注意:一条 cluster 是一条直线段;被拐角/斜坡拆成多段的平台只会在当前这一段内游走。 + /// + /// Start pathfinding to a random position ON THE SAME segment the agent currently stands on. + /// Never crosses a NavLink, so a ground unit stays on its current platform segment. + /// insets the random range from BOTH segment ends (world units), + /// so the goal is never so close to the edge that a bodied agent can't stand there + /// (pass the agent's half-width + ledge-check margin). If the segment is shorter than + /// 2*endMargin the goal degenerates to the segment midpoint. + /// Returns false if the agent is not currently mapped to a segment. + /// + public bool SetRandomDestinationOnCurrentSegment(float endMargin = 0f) + { + if (currentMappedPosition.IsInvalid()) + return false; + + var cluster = currentMappedPosition.cluster; + float len = cluster.Length; + float m = Mathf.Clamp(endMargin, 0f, len * 0.5f); // 段太短 → 退化到中点 + float t = UnityEngine.Random.Range(m, len - m); + Vector2 goal = cluster.owner.LocalToWorld.MultiplyPoint3x4(cluster.GetPositionAlongSegment(t)); + return PathTo(goal); + } + /// /// If you implement link traversal yourself, call this to complete a link traversal. diff --git a/Assets/_Game/Scripts/Enemies/EnemyMovement.cs b/Assets/_Game/Scripts/Enemies/EnemyMovement.cs index 58ec7df3..700cabe7 100644 --- a/Assets/_Game/Scripts/Enemies/EnemyMovement.cs +++ b/Assets/_Game/Scripts/Enemies/EnemyMovement.cs @@ -262,6 +262,21 @@ namespace BaseGames.Enemies Vector2.down, _ledgeCheckDownDist, _groundMask); } + /// + /// 身体中心到"能站住的平台边缘最近点"的水平安全余量 = 碰撞体水平半宽 + 悬崖检测前向偏移。 + /// WouldFallAhead 在"碰撞体前缘 + _ledgeCheckFwdOffset"处探地,故身体中心必须离平台边缘至少此距离, + /// 否则前缘探出崖沿被夹停。供 nav 段内随机游走把随机点从段两端各内缩此值,避免随机到身体站不住的崖沿点。 + /// + public float EdgeSafeMargin + { + get + { + var col = GroundColliderOrSelf(); + float halfWidth = col != null ? col.bounds.extents.x : 0f; + return halfWidth + Mathf.Max(0f, _ledgeCheckFwdOffset); + } + } + private void WallAndLedgeCheck() { if (_wallCheckDist > 0f) diff --git a/Assets/_Game/Scripts/Enemies/IPathAgent.cs b/Assets/_Game/Scripts/Enemies/IPathAgent.cs index a0368b6f..ce6a3513 100644 --- a/Assets/_Game/Scripts/Enemies/IPathAgent.cs +++ b/Assets/_Game/Scripts/Enemies/IPathAgent.cs @@ -62,6 +62,13 @@ namespace BaseGames.Enemies /// 移动到随机可到达点(巡逻 fallback)。 bool WalkToRandom(); + /// + /// 段内随机游走:随机点限制在 agent 当前所站的这一条导航段上,不跨越任何 NavLink(跳/落/拐角)。 + /// 供 Wander 巡逻使用——地面单位只在当前平台段内来回,避免被崖边夹停或卡在过不去的 link 处。 + /// 未映射到导航段(不在图上)时返回 false。 + /// + bool WalkToRandomOnSegment(); + // ── 连接段事件 ────────────────────────────────────────────────── /// 开始穿越连接段时触发(传入连接段类型)。 event Action OnLinkStarted; @@ -91,6 +98,7 @@ namespace BaseGames.Enemies public Vector2 CurrentLinkEnd => Vector2.zero; public bool CanReach(Vector2 _) => false; public bool WalkToRandom() => false; + public bool WalkToRandomOnSegment() => false; public event Action OnLinkStarted { add { } remove { } } public event Action OnLinkCompleted{ add { } remove { } } public event Action OnNavPathFailed { add { } remove { } } diff --git a/Assets/_Game/Scripts/Enemies/Navigation/EnemyLocomotion.cs b/Assets/_Game/Scripts/Enemies/Navigation/EnemyLocomotion.cs index b1746cf4..a5c192b8 100644 --- a/Assets/_Game/Scripts/Enemies/Navigation/EnemyLocomotion.cs +++ b/Assets/_Game/Scripts/Enemies/Navigation/EnemyLocomotion.cs @@ -111,7 +111,9 @@ namespace BaseGames.Enemies switch (_patrolStrategy) { case PatrolStrategy.Wander: - if (_enemy.Nav != null && !_enemy.Nav.IsMoving) _enemy.Nav.WalkToRandom(); + // 段内随机游走:随机点限制在当前所站的导航段上,不跨越 NavLink, + // 避免地面单位游走时被崖边夹停或卡在过不去的跳/落 link 处。 + if (_enemy.Nav != null && !_enemy.Nav.IsMoving) _enemy.Nav.WalkToRandomOnSegment(); break; case PatrolStrategy.Pace: // 撞墙/悬崖翻向的来回踱步(速度驱动,不依赖 nav) diff --git a/Assets/_Game/Scripts/Enemies/Navigation/EnemyNavAgent.cs b/Assets/_Game/Scripts/Enemies/Navigation/EnemyNavAgent.cs index 88480c25..ba067a0d 100644 --- a/Assets/_Game/Scripts/Enemies/Navigation/EnemyNavAgent.cs +++ b/Assets/_Game/Scripts/Enemies/Navigation/EnemyNavAgent.cs @@ -171,6 +171,11 @@ namespace BaseGames.Enemies.Navigation public bool WalkToRandom() => _navAgent?.SetRandomDestination() ?? false; + // 段内随机游走:按身体前缘安全余量内缩段两端,避免随机点落在角色宽度站不住的崖沿(WouldFallAhead 夹停)。 + public bool WalkToRandomOnSegment() + => _navAgent?.SetRandomDestinationOnCurrentSegment( + _enemyMovement != null ? _enemyMovement.EdgeSafeMargin : 0f) ?? false; + public bool IsNearEdge() { var origin = (Vector2)transform.position; diff --git a/Assets/_Game/Scripts/Enemies/Navigation/FlyingDirectNavigator.cs b/Assets/_Game/Scripts/Enemies/Navigation/FlyingDirectNavigator.cs index 934d09f5..13a0e86b 100644 --- a/Assets/_Game/Scripts/Enemies/Navigation/FlyingDirectNavigator.cs +++ b/Assets/_Game/Scripts/Enemies/Navigation/FlyingDirectNavigator.cs @@ -119,6 +119,10 @@ namespace BaseGames.Enemies.Navigation return true; } + // 飞行单位无"导航段"概念,其 WalkToRandom 本就是当前位置的有界局部偏移, + // 天然满足"就近游走"语义,直接复用。 + public bool WalkToRandomOnSegment() => WalkToRandom(); + // ── 内部移动逻辑 ─────────────────────────────────────────────── private void UpdateChase() {