using Opsive.BehaviorDesigner.AddOns.MovementPack.Runtime.Tasks; using Opsive.BehaviorDesigner.AddOns.Shared.Runtime.Pathfinding; using Opsive.BehaviorDesigner.Runtime.Tasks; using Opsive.GraphDesigner.Runtime; using Opsive.GraphDesigner.Runtime.Variables; using Opsive.Shared.Utility; using UnityEngine; using UnityEngine.AI; namespace Cielonos.MainGame.Characters.AI { [Description("前后移动 (Advance/Retreat)。设定一个与目标的期望距离,敌人会自动前进或后退到该距离。支持随机区间与次数/时间限制。")] [NodeIcon("f900ccca7c66371459b52036efeb8778", "cfd0e78235c50db46bc12d1751492ecf")] [Category("Cielonos/Movement")] public class AdvanceRetreat : MovementBase { #region Parameters [Header("Target")] [Tooltip("对峙的目标对象。")] [SerializeField] protected SharedVariable m_Target; [Header("Distance Config")] [Tooltip("目标距离的最小值 (米)。如果想设定固定距离,将其与最大值设为相同即可。")] [SerializeField] protected SharedVariable m_MinTargetDistance = 3f; [Tooltip("目标距离的最大值 (米)。")] [SerializeField] protected SharedVariable m_MaxTargetDistance = 6f; [Header("Movement")] [Tooltip("前后移动时的速度。")] [SerializeField] protected SharedVariable m_MoveSpeed = 2f; [Header("Facing")] [Tooltip("是否在移动过程中面朝目标。如果不勾选,则依赖外部系统的朝向控制。")] [SerializeField] protected SharedVariable m_FaceTarget = true; [Tooltip("面朝目标的旋转角速度 (度/秒)。")] [SerializeField] protected SharedVariable m_RotationSpeed = 360f; [Header("Limits & Timing")] [Tooltip("任务退出限制模式:总时间 / 总次数 / 无限")] [SerializeField] protected TacticalLimitMode m_LimitMode = TacticalLimitMode.TotalDuration; [Tooltip("最大移动次数(用于 TotalMoveCount 模式)。")] [SerializeField] protected SharedVariable m_MaxMoveCount = 3; [Tooltip("最长总持续时间(秒,用于 TotalDuration 模式)。")] [SerializeField] protected SharedVariable m_MaxDuration = 8f; [Tooltip("在总时间结束时,是否强制走完最后一次移动后再退出。")] [SerializeField] protected SharedVariable m_FinishLastMoveBeforeEnd = false; [Tooltip("单次移动的最大持续时间 (秒)。到达时间或到达航点都会结束单词移动。")] [SerializeField] protected SharedVariable m_SingleMoveMaxDuration = 2f; [Tooltip("每次到达航点后的短暂停顿 (秒)。")] [SerializeField] protected SharedVariable m_TransitionPause = 0.2f; #endregion #region Private State private NavMeshAgentPathfinder _navPathfinder; private NavMeshAgent _agent; private AutomataLandMovementSubcontroller _movementSc; private float _originalMaxSpeed; private bool _originalUpdateRotation; private bool _original8WayMovement; // 总体进度控制 private float _taskStartTime; private int _currentMoveCount; private bool _isTimeUp; // 单次移动状态 private float _singleMoveStartTime; private bool _isMoving; private float _transitionPauseEndTime; private const int k_MaxSampleRetries = 5; #endregion public override void OnAwake() { base.OnAwake(); _navPathfinder = m_Pathfinder as NavMeshAgentPathfinder; if (_navPathfinder != null) _agent = _navPathfinder.m_NavMeshAgent; var automata = gameObject.GetComponent(); if (automata != null) _movementSc = automata.movementSc as AutomataLandMovementSubcontroller; } public override void OnStart() { base.OnStart(); if (m_Target == null || m_Target.Value == null || _agent == null) return; _originalMaxSpeed = _navPathfinder.Speed; _originalUpdateRotation = _agent.updateRotation; _agent.updateRotation = !m_FaceTarget.Value; _agent.autoBraking = true; _agent.speed = m_MoveSpeed.Value; if (_movementSc != null) { _original8WayMovement = _movementSc.is8WayMovement; _movementSc.is8WayMovement = true; } _taskStartTime = Time.time; _currentMoveCount = 0; _isTimeUp = false; _isMoving = false; _transitionPauseEndTime = 0f; StartNewMove(); } public override TaskStatus OnUpdate() { if (m_Target == null || m_Target.Value == null || _agent == null) return TaskStatus.Failure; if (m_FaceTarget.Value) RotateTowardsTarget(); // 检查外部限制 if (m_LimitMode == TacticalLimitMode.TotalDuration && !_isTimeUp) { if (Time.time - _taskStartTime >= m_MaxDuration.Value) { _isTimeUp = true; if (!m_FinishLastMoveBeforeEnd.Value) { return TaskStatus.Success; } } } if (_isMoving) { bool arrived = HasArrived(); bool singleMoveTimeUp = (Time.time - _singleMoveStartTime) >= m_SingleMoveMaxDuration.Value; if (arrived || singleMoveTimeUp) { StopAgent(); _isMoving = false; _currentMoveCount++; _transitionPauseEndTime = Time.time + m_TransitionPause.Value; } } else { if (Time.time >= _transitionPauseEndTime) { if (_isTimeUp) return TaskStatus.Success; if (m_LimitMode == TacticalLimitMode.TotalMoveCount && _currentMoveCount >= m_MaxMoveCount.Value) return TaskStatus.Success; StartNewMove(); } } return TaskStatus.Running; } private void StartNewMove() { Vector3 targetPos = m_Target.Value.transform.position; Vector3 toSelf = transform.position - targetPos; toSelf.y = 0f; if (toSelf.sqrMagnitude < 0.01f) { toSelf = -transform.forward; // 极近距离兜底 } // 获取本次移动的目标距离 float targetDist = Random.Range(m_MinTargetDistance.Value, m_MaxTargetDistance.Value); // 如果当前距离目标太近,toSelf.normalized 其实就是互相远离的方向,这里就是后退; // 如果太远,toSelf.normalized 也是远离的方向,乘以 targetDist 就是前进。 // 无论是前进还是后退,目标点坐标在同一条射线上:targetPos + radialDir * targetDist // 绝佳的几何性质保证了无论向前还是向后,方向绝对正确无误 Vector3 radialDir = toSelf.normalized; Vector3 destination = targetPos + radialDir * targetDist; if (TrySampleAndSetDestination(targetPos, destination)) { _isMoving = true; _singleMoveStartTime = Time.time; if (_agent.isStopped) _agent.isStopped = false; } else { // 如果完全找不到航点(可能是身后是墙),放弃这次移动 _isMoving = false; _currentMoveCount++; _transitionPauseEndTime = Time.time + m_TransitionPause.Value; } } private bool TrySampleAndSetDestination(Vector3 centerTargetPos, Vector3 destination) { Vector3 sampledPos = destination; if (SamplePosition(ref sampledPos)) { SetDestination(sampledPos); return true; } // 采样失败时,尝试向目标方向靠拢 for (int i = 1; i <= k_MaxSampleRetries; i++) { // 如果是后退撞墙,逐步缩短目标距离(即靠近目标)进行采样 Vector3 retryPos = Vector3.Lerp(destination, centerTargetPos, i * 0.15f); if (SamplePosition(ref retryPos)) { SetDestination(retryPos); return true; } } return false; } private void RotateTowardsTarget() { Vector3 direction = m_Target.Value.transform.position - transform.position; direction.y = 0f; if (direction.sqrMagnitude < 0.001f) return; Quaternion targetRot = Quaternion.LookRotation(direction); transform.rotation = Quaternion.RotateTowards(transform.rotation, targetRot, m_RotationSpeed.Value * Time.deltaTime); } private void StopAgent() { if (_agent != null && _agent.isOnNavMesh && !_agent.isStopped) { _agent.isStopped = true; _agent.velocity = Vector3.zero; } } public override void OnEnd() { base.OnEnd(); CleanUpAgent(); } public override void OnBehaviorTreeStopped(bool paused) { base.OnBehaviorTreeStopped(paused); CleanUpAgent(); } private void CleanUpAgent() { if (_agent != null) { if (_agent.isOnNavMesh && _agent.isActiveAndEnabled) { _agent.isStopped = true; _agent.velocity = Vector3.zero; _agent.ResetPath(); } _agent.updateRotation = _originalUpdateRotation; _agent.autoBraking = true; _agent.speed = _originalMaxSpeed; } if (_movementSc != null) { _movementSc.is8WayMovement = _original8WayMovement; } } public override void Reset() { base.Reset(); m_Target = null; m_MinTargetDistance = 3f; m_MaxTargetDistance = 6f; m_MoveSpeed = 2f; m_FaceTarget = true; m_RotationSpeed = 360f; m_LimitMode = TacticalLimitMode.TotalDuration; m_MaxMoveCount = 3; m_MaxDuration = 8f; m_FinishLastMoveBeforeEnd = false; m_SingleMoveMaxDuration = 2f; m_TransitionPause = 0.2f; } #if UNITY_EDITOR protected override void OnDrawGizmosSelected() { if (m_Target == null || m_Target.Value == null) return; Vector3 targetPos = m_Target.Value.transform.position; if (Mathf.Abs(m_MinTargetDistance.Value - m_MaxTargetDistance.Value) < 0.01f) { UnityEditor.Handles.color = new Color(0f, 1f, 1f, 0.4f); UnityEditor.Handles.DrawWireDisc(targetPos, Vector3.up, m_MinTargetDistance.Value); } else { UnityEditor.Handles.color = new Color(1f, 0.3f, 0.3f, 0.4f); UnityEditor.Handles.DrawWireDisc(targetPos, Vector3.up, m_MinTargetDistance.Value); UnityEditor.Handles.color = new Color(0.3f, 0.6f, 1f, 0.4f); UnityEditor.Handles.DrawWireDisc(targetPos, Vector3.up, m_MaxTargetDistance.Value); } } #endif } }