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 { public enum TacticalLimitMode { TotalDuration, // 总时间限制 TotalMoveCount, // 总移动次数限制 Infinite // 无限运行 (条件打断) } [Description("横向移动/绕圈 (Strafe)。敌人面朝玩家并横向移动一段圆弧。支持时间限制或移动次数限制。单次移动一旦确定目标点,在此次移动完成前不会因为玩家移动而改变航点。")] [NodeIcon("f900ccca7c66371459b52036efeb8778", "cfd0e78235c50db46bc12d1751492ecf")] [Category("Cielonos/Movement")] public class Strafe : MovementBase { #region Parameters [Header("Target & Distance")] [Tooltip("环绕的目标对象。")] [SerializeField] protected SharedVariable m_Target; [Tooltip("绕圈的最小距离 (米)。")] [SerializeField] protected SharedVariable m_MinDistance = 4f; [Tooltip("绕圈的最大距离 (米)。")] [SerializeField] protected SharedVariable m_MaxDistance = 7f; [Header("Strafe Movement")] [Tooltip("移动速度。")] [SerializeField] protected SharedVariable m_StrafeSpeed = 2.5f; [Tooltip("单次横向绕圈的最小移动距离 (步长, 米)。")] [SerializeField] protected SharedVariable m_StrafeDistanceMin = 2f; [Tooltip("单次横向绕圈的最大移动距离 (步长, 米)。")] [SerializeField] protected SharedVariable m_StrafeDistanceMax = 5f; [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; // 方向控制 (确保多次Strafe连贯的话,方向可能改变或保持) private int _strafeDirection; 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; // 如果我们手动face target,则关掉自动寻路旋转 _agent.autoBraking = true; _agent.speed = m_StrafeSpeed.Value; if (_movementSc != null) { _original8WayMovement = _movementSc.is8WayMovement; _movementSc.is8WayMovement = true; } _taskStartTime = Time.time; _currentMoveCount = 0; _isTimeUp = false; _isMoving = false; _transitionPauseEndTime = 0f; // 首段移动的方向随机 _strafeDirection = Random.value > 0.5f ? 1 : -1; 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 (m_LimitMode == TacticalLimitMode.TotalMoveCount && m_MaxMoveCount.Value == 0) { 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; // 兜底 } // --- 几何安全防越界机制 --- // 确保敌人从A点(当前)直线移动到B点(终点)的过程中,连线(弦)不会进入内圈(MinDistance) float mathMinDist = m_MinDistance.Value + 0.1f; // 附加0.1m的安全余量 float currentDist = toSelf.magnitude; float r1 = Mathf.Max(currentDist, mathMinDist); // 从 r1 处作内圈切线所经历的夹角 alpha float alpha = Mathf.Acos(mathMinDist / r1); // 终点最远能在 MaxDistance 处,对应的另一个切线夹角 float maxThetaForMaxR2 = alpha + Mathf.Acos(mathMinDist / Mathf.Max(m_MaxDistance.Value, mathMinDist + 0.1f)); float targetStepDist = Random.Range(m_StrafeDistanceMin.Value, m_StrafeDistanceMax.Value); float absTheta = targetStepDist / r1; // 根据当前半径和目标步长预估需要的弧度 // 1. 限制最大角度:如果走得太远,连目标放在最大边界边缘都会穿模,必须截断本次角度 float maxThetaSafe = maxThetaForMaxR2 * 0.99f; if (absTheta > maxThetaSafe) { absTheta = maxThetaSafe; } // 2. 动态决定目标的最短安全半径 minR2 float minR2 = mathMinDist; if (absTheta > alpha) { // 如果角度超过了 alpha,代表弦已经超过了切点,必须让目标点往外扩,才不至于割进内圈 minR2 = mathMinDist / Mathf.Cos(absTheta - alpha); } minR2 = Mathf.Min(minR2, m_MaxDistance.Value); // 钳位 // 70% 概率维持刚才的方向,30% 反转方向 if (Random.value < 0.3f) _strafeDirection = -_strafeDirection; // 在安全的外部带中随机取一个终点半径 float r2 = Random.Range(minR2, m_MaxDistance.Value); float currentAngle = Mathf.Atan2(toSelf.x, toSelf.z); float angleDelta = absTheta * _strafeDirection; float newAngle = currentAngle + angleDelta; Vector3 destination = targetPos + new Vector3(Mathf.Sin(newAngle), 0f, Mathf.Cos(newAngle)) * r2; // 防撞墙机制:使用 NavMesh.Raycast NavMeshHit hit; if (NavMesh.Raycast(transform.position, destination, out hit, NavMesh.AllAreas)) { Vector3 toDest = (destination - transform.position).normalized; destination = hit.position - toDest * 0.5f; // 这次碰壁了,强行反转下周期的绕圈方向 _strafeDirection = -_strafeDirection; } if (TrySampleAndSetDestination(targetPos, destination)) { _isMoving = true; _singleMoveStartTime = Time.time; if (_agent != null && _agent.isStopped) _agent.isStopped = false; } else { // 如果找不到目标点,强行翻转方向重试一次 _strafeDirection = -_strafeDirection; angleDelta = absTheta * _strafeDirection; newAngle = currentAngle + angleDelta; destination = targetPos + new Vector3(Mathf.Sin(newAngle), 0f, Mathf.Cos(newAngle)) * r2; if (NavMesh.Raycast(transform.position, destination, out hit, NavMesh.AllAreas)) { Vector3 toDest = (destination - transform.position).normalized; destination = hit.position - toDest * 0.5f; } if (TrySampleAndSetDestination(targetPos, destination)) { _isMoving = true; _singleMoveStartTime = Time.time; if (_agent != null && _agent.isStopped) _agent.isStopped = false; } else { // 两边都没法走,当做此次移动完成,进入Pause并计入次数 _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 safeCenter = centerTargetPos + (destination - centerTargetPos).normalized * ((m_MinDistance.Value + m_MaxDistance.Value) * 0.5f); Vector3 retryPos = Vector3.Lerp(destination, safeCenter, i * 0.2f); 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_MinDistance = 4f; m_MaxDistance = 7f; m_StrafeSpeed = 2.5f; m_StrafeDistanceMin = 2f; m_StrafeDistanceMax = 5f; 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; } } }