using Opsive.BehaviorDesigner.AddOns.MovementPack.Runtime.Tasks; using Opsive.BehaviorDesigner.AddOns.Shared.Runtime.Pathfinding; using Opsive.BehaviorDesigner.Runtime.Tasks; using Opsive.BehaviorDesigner.Runtime.Utility; using Opsive.GraphDesigner.Runtime; using Opsive.GraphDesigner.Runtime.Variables; using Opsive.Shared.Utility; using Unity.Entities; using UnityEngine; using UnityEngine.AI; namespace Cielonos.MainGame.Characters.AI { [Description("在初始出生点附近的指定半径内随机选取一个 NavMesh 上的点并移动过去。到达后等待一段时间(可选),然后返回 Success。")] [NodeIcon("Assets/Sprites/Icon/Play.png")] [Category("Cielonos/Movement")] public class WanderInRange : MovementBase { [Tooltip("初始移动速度。")] [SerializeField] protected SharedVariable m_StartSpeed = 10f; [Tooltip("以初始出生点为圆心的游走半径(单位:米)。")] [SerializeField] protected SharedVariable m_WanderRadius = 10f; [Tooltip("到达目标点后等待的时间范围(秒)。Min 与 Max 均为 0 时不等待,直接返回 Success。")] [SerializeField] protected SharedVariable m_WaitAtDestinationDuration = new RangeFloat(0f, 0f); [Tooltip("判定到达目的地所需的剩余距离阈值(单位:米)。")] [SerializeField] protected SharedVariable m_ArrivalDistance = 0.5f; [Tooltip("每次寻路 tick 中,选取随机目标点的最大重试次数。")] [SerializeField] protected SharedVariable m_DestinationRetries = 5; #if UNITY_EDITOR [Tooltip("是否在无效目标点处绘制调试射线?")] [SerializeField] protected bool m_DrawInvalidDestinationRay; #endif // 记录行为树初始化时的出生点位置 private Vector3 m_SpawnPosition; private NavMeshAgentPathfinder _navPathfinder; private NavMeshAgent _agent; // 到达后等待状态 private float m_WaitDuration = -1f; private float m_DestinationReachedTime = -1f; // 本次 OnStart 是否已成功派发了目标点 private bool m_HasDestination; /// /// 记录出生点,仅在行为树初始化时调用一次。 /// public override void OnAwake() { base.OnAwake(); _navPathfinder = m_Pathfinder as NavMeshAgentPathfinder; if (_navPathfinder == null) { Debug.LogError("[PrecisePursue] Requires NavMeshAgentPathfinder."); return; } _agent = _navPathfinder.m_NavMeshAgent; m_SpawnPosition = transform.position; } /// /// 每次节点开始执行时,重置状态并选取新目标。 /// public override void OnStart() { base.OnStart(); m_WaitDuration = -1f; m_DestinationReachedTime = -1f; m_HasDestination = TrySetDestination(); } /// /// 每帧检查是否到达目标点,并处理到达后的等待逻辑。 /// public override TaskStatus OnUpdate() { // 若初始选点失败,再尝试一次;仍然失败则返回 Failure if (!m_HasDestination) { m_HasDestination = TrySetDestination(); if (!m_HasDestination) return TaskStatus.Failure; } _agent.speed = m_StartSpeed.Value; // 到达判定:HasArrived() 或剩余距离小于阈值 if (HasArrived() || RemainingDistance < m_ArrivalDistance.Value) { // 首次到达:决定是否等待 if (m_WaitDuration < 0f) { m_WaitDuration = m_WaitAtDestinationDuration.Value.RandomValue; if (m_WaitDuration > 0f) { m_DestinationReachedTime = Time.time; return TaskStatus.Running; } return TaskStatus.Success; } // 等待中:检查是否已等待足够时间 if (Time.time >= m_DestinationReachedTime + m_WaitDuration) { return TaskStatus.Success; } } return TaskStatus.Running; } /// /// 在出生点半径内随机采样 NavMesh 上的有效点,并设定为寻路目标。 /// /// 成功设定目标点返回 true,所有重试均失败返回 false。 private bool TrySetDestination() { for (int i = 0; i < m_DestinationRetries.Value; i++) { // 在 XZ 平面的圆形范围内随机选点 var randomOffset = Random.insideUnitCircle * m_WanderRadius.Value; var candidate = m_SpawnPosition + new Vector3(randomOffset.x, 0f, randomOffset.y); if (SamplePosition(ref candidate)) { // 防止 NavMesh 采样将点吸附到半径外 if (Vector3.Distance(candidate, m_SpawnPosition) <= m_WanderRadius.Value) { SetDestination(candidate); return true; } } #if UNITY_EDITOR if (m_DrawInvalidDestinationRay) Debug.DrawRay(candidate, Vector3.up * 2f, Color.red, 1f); #endif } return false; } /// /// 节点结束时重置等待状态。 /// public override void OnEnd() { base.OnEnd(); m_WaitDuration = -1f; m_DestinationReachedTime = -1f; m_HasDestination = false; } /// /// 保存当前节点运行状态(等待剩余时间)。 /// public override object Save(World world, Entity entity) { var saveData = new object[3]; saveData[0] = base.Save(world, entity); saveData[1] = m_WaitDuration; // 存储已等待时长而非绝对时间戳,以兼容时间暂停/恢复 saveData[2] = m_DestinationReachedTime >= 0f ? Time.time - m_DestinationReachedTime : -1f; return saveData; } /// /// 恢复节点运行状态。 /// public override void Load(object saveData, World world, Entity entity) { var arr = (object[])saveData; base.Load(arr[0], world, entity); m_WaitDuration = (float)arr[1]; var elapsed = (float)arr[2]; m_DestinationReachedTime = elapsed >= 0f ? Time.time - elapsed : -1f; } /// /// 编辑器重置,恢复字段默认值。 /// public override void Reset() { base.Reset(); m_WanderRadius = 8f; m_WaitAtDestinationDuration = new RangeFloat(0f, 0f); m_ArrivalDistance = 0.5f; m_DestinationRetries = 5; #if UNITY_EDITOR m_DrawInvalidDestinationRay = false; #endif } } }