Files
zeling_v2/Assets/_Game/Scripts/Enemies/AI/BD_PatrolWaypoints.cs
Joywayer bcd8b0e90b feat: Update enemy AI and movement systems
- Enhanced Physics2D layer collision report with new interactions between Player and Enemy layers.
- Refactored BD_InvestigateLastKnown to streamline animation handling and improve readability.
- Simplified BD_MaintainCombatDistance by consolidating movement stop logic.
- Updated BD_MoveToPlayer to set AI phase on start.
- Improved BD_Patrol logic with better handling of stuck states and path failures.
- Enhanced BD_PatrolWaypoints to manage stuck conditions and retry logic more effectively.
- Refined BD_ReturnToHome to remove unnecessary animation calls.
- Updated BD_WalkRandom to ensure AI phase is set correctly on start.
- Improved EnemyAbilityBase to delegate target facing to the movement system.
- Enhanced EnemyBase with new movement methods for better control.
- Refactored EnemyMovement to introduce a new input system for handling movement and facing.
- Added EnemyMoveInput struct to encapsulate movement intentions.
- Updated Physics2DSettings to reflect new layer collision matrix.
- Introduced RTK CLI instructions for optimized command usage.
2026-05-29 17:01:59 +08:00

242 lines
9.7 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.
#if GRAPH_DESIGNER
using UnityEngine;
using Opsive.BehaviorDesigner.Runtime.Tasks;
using Opsive.BehaviorDesigner.Runtime.Tasks.Actions;
using BaseGames.Enemies;
namespace BaseGames.Enemies.AI
{
/// <summary>
/// BD Action按预设路点顺序巡逻支持 Transform 引用或内联 Vector2 坐标两种模式)。
///
/// <list type="bullet">
/// <item><b>m_WaypointsTransform[]</b>:拖入场景中的路点对象;适合动态路点(可在运行时移动)。</item>
/// <item><b>m_InlineWaypointsVector2[]</b>:直接填写世界坐标;无需在场景中放置对象,编辑器中以 Gizmo 可视化路径。</item>
/// <item>两者同时设置时 m_Waypoints 优先。</item>
/// </list>
///
/// 到达最后一个路点后可循环Loop或往返PingPong
/// 与 PathBerserker2d 集成:通过 IPathAgent.RequestMoveTo 导航,支持跨平台跳跃 NavLink。
/// </summary>
[TaskName("Patrol (Waypoints)")]
[TaskCategory("BaseGames/Enemy/Movement")]
[TaskDescription("按预设路点顺序巡逻;支持 Transform 引用或内联 Vector2 坐标,可循环或折返")]
public sealed class BD_PatrolWaypoints : Action
{
[Tooltip("路点列表(世界空间 Transform与 m_InlineWaypoints 同时设置时此项优先")]
[SerializeField] private Transform[] m_Waypoints;
[Tooltip("内联路点坐标(世界空间 Vector2m_Waypoints 为空时使用;在 Scene 视图中以绿色 Gizmo 可视化")]
[SerializeField] private Vector2[] m_InlineWaypoints;
[Tooltip("到达路点的判定半径m")]
[SerializeField] private float m_ArriveRadius = 0.3f;
[Tooltip("true = 往返; false = 循环")]
[SerializeField] private bool m_PingPong = false;
[Tooltip("每个路点到达后等待时长s")]
[SerializeField] private float m_WaitAtWaypoint = 0f;
// 首次启动重试间隔OnStart() 的第一次 RequestCurrent() 可能因 NavAgent 尚未完成
// UpdateMappedPosition()(脚本执行顺序问题)而静默失败。此值只需大于一帧即可。
private const float InitialRetryDelay = 0.05f;
// 正常巡逻中卡住的重试间隔:必须足够长,使任何在途 PB2d 路径请求
// Pending → Finished → HandlePathRequest 消费)都已处理完毕,避免竞争覆盖。
private const float StuckRetryDelay = 0.5f;
private EnemyBase _enemy;
private int _index = 0;
private int _dir = 1;
private float _waitTimer = 0f;
private bool _waiting = false;
private float _stuckTimer = 0f;
private bool _pathFailed = false;
private bool _hasMoved = false; // 首次成功开始移动后置 true
// ── 统一路点访问 ────────────────────────────────────────────────────
private int WaypointCount =>
m_Waypoints != null && m_Waypoints.Length > 0
? m_Waypoints.Length
: m_InlineWaypoints?.Length ?? 0;
private Vector2 GetWaypoint(int index)
{
if (m_Waypoints != null && m_Waypoints.Length > 0)
{
var t = m_Waypoints[index];
return t != null ? (Vector2)t.position : (Vector2)transform.position;
}
return m_InlineWaypoints != null && index < m_InlineWaypoints.Length
? m_InlineWaypoints[index]
: (Vector2)transform.position;
}
// ── BD 生命周期 ─────────────────────────────────────────────────────
public override void OnAwake() => _enemy = gameObject.GetComponent<EnemyBase>();
public override void OnStart()
{
if (WaypointCount == 0) return;
_waiting = false;
_waitTimer = 0f;
_stuckTimer = 0f;
_pathFailed = false;
_hasMoved = false;
_enemy?.SetAiPhase(AiPhase.Patrol);
if (_enemy?.Nav != null)
{
_enemy.Nav.OnGoalReached += HandleGoalReached;
_enemy.Nav.OnNavPathFailed += HandlePathFailed;
}
RequestCurrent();
}
public override TaskStatus OnUpdate()
{
if (_enemy == null || WaypointCount == 0)
return TaskStatus.Failure;
if (_waiting)
{
_waitTimer -= Time.deltaTime;
if (_waitTimer > 0f) return TaskStatus.Running;
_waiting = false;
Advance();
RequestCurrent();
return TaskStatus.Running;
}
// 兜底重试:用时间门控避免与 PB2d 的 Finished-状态路径请求产生竞争。
// 首次启动前_hasMoved=false使用短延迟处理 NavAgent 位置映射尚未就绪的情况;
// 正常巡逻中使用长延迟,确保在途路径请求已被 HandlePathRequest 消费完毕。
if (_enemy.Nav != null)
{
if (!_enemy.Nav.IsMoving)
{
_stuckTimer += Time.deltaTime;
float retryDelay = _hasMoved ? StuckRetryDelay : InitialRetryDelay;
if (_stuckTimer >= retryDelay)
{
_stuckTimer = 0f;
_pathFailed = false;
RequestCurrent();
}
}
else
{
_hasMoved = true;
_stuckTimer = 0f;
}
}
return TaskStatus.Running;
}
public override void OnEnd()
{
if (_enemy?.Nav != null)
{
_enemy.Nav.OnGoalReached -= HandleGoalReached;
_enemy.Nav.OnNavPathFailed -= HandlePathFailed;
}
_enemy?.StopMovement();
}
// ── 内部辅助 ────────────────────────────────────────────────────────
private void HandleGoalReached()
{
_hasMoved = true;
_stuckTimer = 0f;
_pathFailed = false;
if (m_WaitAtWaypoint > 0f)
{
_waiting = true;
_waitTimer = m_WaitAtWaypoint;
_enemy?.StopMovement();
}
else
{
Advance();
RequestCurrent();
}
}
// 路径失败时不在回调中立即重试——此时 NavAgent.HandlePathRequest 尚未调用
// currentPathRequest.Reset(),直接提交新请求会被随后的 Reset() 覆盖清除。
// 改为设置标志,交由 OnUpdate 的计时兜底在下一帧安全重试。
private void HandlePathFailed()
{
_pathFailed = true;
_stuckTimer = 0f; // 重置计时器,使兜底在 StuckRetryDelay 后触发
}
private void RequestCurrent()
{
if (WaypointCount == 0) return;
_enemy.MoveTo(GetWaypoint(_index));
}
private void Advance()
{
if (WaypointCount <= 1) return;
if (m_PingPong)
{
_index += _dir;
if (_index >= WaypointCount) { _index = WaypointCount - 2; _dir = -1; }
else if (_index < 0) { _index = 1; _dir = 1; }
}
else
{
_index = (_index + 1) % WaypointCount;
}
}
// ── Gizmo 可视化(仅 m_InlineWaypoints 模式)────────────────────────
#if UNITY_EDITOR
private new void OnDrawGizmos()
{
if (m_InlineWaypoints == null || m_InlineWaypoints.Length < 1) return;
// m_Waypoints 存在时不绘制,避免与场景路点重叠
if (m_Waypoints != null && m_Waypoints.Length > 0) return;
Gizmos.color = new Color(0.2f, 0.85f, 0.2f, 0.8f);
for (int i = 0; i < m_InlineWaypoints.Length; i++)
{
Gizmos.DrawWireSphere(m_InlineWaypoints[i], m_ArriveRadius);
if (i < m_InlineWaypoints.Length - 1)
Gizmos.DrawLine(m_InlineWaypoints[i], m_InlineWaypoints[i + 1]);
}
// PingPong 时也画返回线Loop 时画首尾连接线
if (m_InlineWaypoints.Length >= 2)
{
if (m_PingPong)
{
// 往返:虚线效果(半透明线)
Gizmos.color = new Color(0.2f, 0.85f, 0.2f, 0.35f);
Gizmos.DrawLine(m_InlineWaypoints[m_InlineWaypoints.Length - 1], m_InlineWaypoints[0]);
}
else
{
// 循环:画首尾连接
Gizmos.color = new Color(0.2f, 0.85f, 0.2f, 0.5f);
Gizmos.DrawLine(m_InlineWaypoints[m_InlineWaypoints.Length - 1], m_InlineWaypoints[0]);
}
}
// 编辑器标签(路点序号)
UnityEditor.Handles.color = new Color(0.2f, 0.85f, 0.2f, 1f);
for (int i = 0; i < m_InlineWaypoints.Length; i++)
UnityEditor.Handles.Label(m_InlineWaypoints[i] + Vector2.up * 0.25f, $"WP{i}");
}
#endif
}
}
#endif