Files
zeling_v2/Assets/_Game/Scripts/Enemies/Navigation/FlyingDirectNavigator.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

190 lines
7.8 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;
using BaseGames.Enemies;
namespace BaseGames.Enemies.Navigation
{
/// <summary>
/// 飞行单位直线导航代理。
/// 不依赖 PathBerserker2d 寻路,直接通过 Rigidbody2D.MovePosition 向目标点直飞。
///
/// 特性:
/// <list type="bullet">
/// <item>空闲时施加正弦波形悬停偏移,产生飘浮感。</item>
/// <item>NavLink 相关成员均返回安全默认值(飞行单位不使用平台连接段)。</item>
/// <item>目标到达后触发 <see cref="OnGoalReached"/> 事件(替代轮询 IsAtDestination)。</item>
/// </list>
///
/// 使用方式:挂载到飞行怪 Prefab 根节点,替代 EnemyNavAgent
/// <see cref="EnemyBase.Awake"/> 的 GetComponent&lt;IPathAgent&gt;() 自动发现此组件。
/// </summary>
[RequireComponent(typeof(Rigidbody2D))]
public sealed class FlyingDirectNavigator : MonoBehaviour, IPathAgent
{
[Header("移动参数")]
[Tooltip("直飞速度(m/s")]
[SerializeField] private float _moveSpeed = 3f;
[Tooltip("到达目标的判定距离(m")]
[SerializeField] private float _stoppingDist = 0.15f;
[Header("游荡参数(WalkToRandom")]
[Tooltip("随机游荡目标点距当前位置的最大半径(m)。")]
[Min(0.1f)]
[SerializeField] private float _randomWalkRadius = 3f;
[Header("悬停参数(空闲 / 无目标时)")]
[Tooltip("悬停横向摆动速度(m/s")]
[SerializeField] private float _hoverLateralSpeed = 0.5f;
[Tooltip("悬停纵向正弦频率(Hz")]
[SerializeField] private float _hoverSineFrequency = 1.2f;
[Tooltip("悬停纵向正弦振幅(m/s")]
[SerializeField] private float _hoverSineAmplitude = 0.25f;
[Tooltip("横向方向翻转周期(s")]
[SerializeField] private float _hoverFlipInterval = 1.5f;
// ── IPathAgent 事件 ────────────────────────────────────────────
public event Action<NavLinkType> OnLinkStarted { add { } remove { } }
public event Action<NavLinkType> OnLinkCompleted { add { } remove { } }
public event Action OnNavPathFailed { add { } remove { } }
public event Action OnGoalReached;
// ── 状态 ───────────────────────────────────────────────────────
private Rigidbody2D _rb;
private EnemyMovement _movement;
private Vector2? _destination;
private bool _isMoving;
private bool _goalFired;
private float _hoverTimer;
private float _hoverFlipTimer;
private float _hoverDir = 1f;
// ── IPathAgent 属性 ────────────────────────────────────────────
public bool IsMoving => _isMoving;
public bool IsOnLink => false;
public NavLinkType CurrentLinkType => NavLinkType.None;
public Vector2 CurrentLinkStart => Vector2.zero;
public Vector2 CurrentLinkEnd => Vector2.zero;
// ── Unity 生命周期 ─────────────────────────────────────────────
private void Awake()
{
_rb = GetComponent<Rigidbody2D>();
_movement = GetComponent<EnemyMovement>();
_rb.gravityScale = 0f;
_rb.constraints = RigidbodyConstraints2D.FreezeRotation;
}
private void FixedUpdate()
{
if (_destination.HasValue)
UpdateChase();
else
UpdateHover();
}
// ── IPathAgent 方法 ────────────────────────────────────────────
public void RequestMoveTo(Vector2 target)
{
_destination = target;
_isMoving = true;
_goalFired = false;
}
public void StopNavigation()
{
_destination = null;
_isMoving = false;
_rb.velocity = Vector2.zero;
}
public bool IsAtDestination()
{
if (!_destination.HasValue) return true;
return ((Vector2)transform.position - _destination.Value).sqrMagnitude
<= _stoppingDist * _stoppingDist;
}
public void SetSpeed(float speed) => _moveSpeed = speed;
public bool IsNearEdge() => false; // 飞行单位不检测平台边缘
public bool CanReach(Vector2 target) => true; // 飞行单位直飞,始终可达
public bool WalkToRandom()
{
// 在当前位置随机偏移一个 2D 方向(供 BD_WalkRandom 调用)
Vector2 offset = UnityEngine.Random.insideUnitCircle.normalized * _randomWalkRadius;
RequestMoveTo((Vector2)transform.position + offset);
return true;
}
// 飞行单位无"导航段"概念,其 WalkToRandom 本就是当前位置的有界局部偏移,
// 天然满足"就近游走"语义,直接复用。
public bool WalkToRandomOnSegment() => WalkToRandom();
// 飞行单位直飞、无地面/崖沿/身体宽度约束——任意目标本就"站得住",原样返回即可。
public bool ResolveStandablePoint(Vector2 target, out Vector2 standable)
{
standable = target;
return true;
}
// ── 内部移动逻辑 ───────────────────────────────────────────────
private void UpdateChase()
{
Vector2 myPos = _rb.position;
Vector2 target = _destination.Value;
float sqrDist = (target - myPos).sqrMagnitude;
if (sqrDist <= _stoppingDist * _stoppingDist)
{
_rb.velocity = Vector2.zero;
_isMoving = false;
_destination = null;
if (!_goalFired)
{
_goalFired = true;
OnGoalReached?.Invoke();
}
return;
}
Vector2 newPos = Vector2.MoveTowards(myPos, target, _moveSpeed * Time.fixedDeltaTime);
_rb.MovePosition(newPos);
// 面向移动方向(通过 EnemyMovement 输入信号,保持 _facingDir 与动画系统同步)
float dx = target.x - myPos.x;
if (Mathf.Abs(dx) > 0.05f)
{
int dir = dx > 0f ? 1 : -1;
if (_movement != null)
{
_movement.PendingInput.WantFace = true;
_movement.PendingInput.FaceDir = dir;
}
else
{
// 降级:没有 EnemyMovement 时直接翻转(独立飞行单位)
var s = transform.localScale;
s.x = Mathf.Abs(s.x) * dir;
transform.localScale = s;
}
}
}
private void UpdateHover()
{
_hoverFlipTimer += Time.fixedDeltaTime;
if (_hoverFlipTimer >= _hoverFlipInterval)
{
_hoverFlipTimer = 0f;
_hoverDir = -_hoverDir;
}
float sineY = Mathf.Sin(Time.time * _hoverSineFrequency * Mathf.PI * 2f) * _hoverSineAmplitude;
_rb.velocity = new Vector2(_hoverDir * _hoverLateralSpeed, sineY);
}
}
}