Files
zeling_v2/Assets/_Game/Scripts/Enemies/IPathAgent.cs
T
joywayerandClaude Opus 4.8 132cd24908 feat(enemy): 目标点吸附统一处理导航宽度,Waypoint 不再卡崖边
将"角色导航宽度"收口到导航执行层单一入口,意图层(Locomotion/AI)只递原始目标、不再感知身体尺寸:

- NavAgent 新增 TryGetStandablePointNear:把世界点映射到最近可行走段并按 endMargin 内缩两端(与 SetRandomDestinationOnCurrentSegment 同源)
- EnemyNavAgent 新增 ResolveStandablePoint,以 EnemyMovement.EdgeSafeMargin(半宽+落崖余量)为唯一宽度真源;加 _goalMapSearchRadius
- IPathAgent 暴露 ResolveStandablePoint;FlyingDirectNavigator 原样返回(飞行无崖沿约束),NullPathAgent 空实现
- EnemyLocomotion Waypoint 分支改用吸附后的可达点做移动与到达判定,修复路点摆在空中/崖外时永不到达而卡死;吸附失败显式报错暴露误配,不静默兜底

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-22 15:52:18 +08:00

117 lines
5.6 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 System;
using UnityEngine;
namespace BaseGames.Enemies
{
// ── 连接段类型(映射 PB2d LinkTypeName 字符串)───────────────────────
public enum NavLinkType
{
None,
Segment, // 普通地面/平台段
Jump,
Fall,
Corner,
Climb,
Elevator,
Teleport,
Custom
}
/// <summary>
/// 导航代理抽象接口(架构 07_EnemyModule §5)。
/// EnemyBase 和 BD Task 只依赖此接口,不依赖具体导航库。
/// 实现:EnemyNavAgentPathBerserker2d);测试用 NullPathAgent。
/// </summary>
public interface IPathAgent
{
// ── 核心移动 ────────────────────────────────────────────────────
/// <summary>请求移动到世界坐标 target。</summary>
void RequestMoveTo(Vector2 target);
/// <summary>立即停止导航(清除路径)。</summary>
void StopNavigation();
/// <summary>是否已到达目标(距离 ≤ stoppingDistance)。</summary>
bool IsAtDestination();
/// <summary>运行时覆盖移动速度。</summary>
void SetSpeed(float speed);
/// <summary>当前帧是否在移动(速度 > 0.01 且有有效路径)。</summary>
bool IsMoving { get; }
/// <summary>是否接近平台边缘(脚下或前方无地面时为 true)。</summary>
bool IsNearEdge();
// ── 连接段感知(NavLink 平台跳跃/爬梯/传送)─────────────────────
/// <summary>当前是否正在穿越连接段(跳跃/下落/爬梯等)。</summary>
bool IsOnLink { get; }
/// <summary>当前正在穿越的连接段类型;不在连接段时为 None。</summary>
NavLinkType CurrentLinkType { get; }
/// <summary>当前连接段的起始世界坐标;不在连接段时为 Vector2.zero。</summary>
Vector2 CurrentLinkStart { get; }
/// <summary>当前连接段的目标世界坐标;不在连接段时为 Vector2.zero。</summary>
Vector2 CurrentLinkEnd { get; }
/// <summary>检查目标是否可以从当前位置导航到达(同步轻量查询)。</summary>
bool CanReach(Vector2 target);
/// <summary>移动到随机可到达点(巡逻 fallback)。</summary>
bool WalkToRandom();
/// <summary>
/// 段内随机游走:随机点限制在 agent 当前所站的这一条导航段上,不跨越任何 NavLink(跳/落/拐角)。
/// 供 Wander 巡逻使用——地面单位只在当前平台段内来回,避免被崖边夹停或卡在过不去的 link 处。
/// 未映射到导航段(不在图上)时返回 false。
/// </summary>
bool WalkToRandomOnSegment();
/// <summary>
/// 目标点吸附:把任意世界 <paramref name="target"/> 映射到最近可行走段、并按敌人身体宽度从段两端内缩,
/// 得到"身体真正站得住"的点 <paramref name="standable"/>(避免落在角色宽度站不住的崖沿被移动层夹停)。
/// 身体宽度只属于导航执行层——意图层(AI/Locomotion)递原始目标即可,无需自行计算宽度。
/// target 离导航面太远(超出搜索半径)时返回 falsestandable 回落为原始 target。
/// </summary>
bool ResolveStandablePoint(Vector2 target, out Vector2 standable);
// ── 连接段事件 ──────────────────────────────────────────────────
/// <summary>开始穿越连接段时触发(传入连接段类型)。</summary>
event Action<NavLinkType> OnLinkStarted;
/// <summary>连接段穿越完成时触发。</summary>
event Action<NavLinkType> OnLinkCompleted;
/// <summary>路径寻路失败事件(目标不可达时触发)。</summary>
event Action OnNavPathFailed;
/// <summary>到达目标事件(替代轮询 IsAtDestination)。</summary>
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 event Action<NavLinkType> OnLinkStarted { add { } remove { } }
public event Action<NavLinkType> OnLinkCompleted{ add { } remove { } }
public event Action OnNavPathFailed { add { } remove { } }
public event Action OnGoalReached { add { } remove { } }
}
}