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 { /// /// 对峙与游走 (Standoff) /// 敌人面朝玩家,在指定对峙距离带 (MinDistance ~ MaxDistance) 上 /// 进行横向绕圈、前后踱步和短暂停滞的随机组合移动。 /// 通过运动模式权重可塑造不同敌人的"性格"(激进型 / 谨慎型)。 /// [Description("对峙与游走。敌人面朝玩家,在攻击距离边缘进行横向绕圈与随机踱步。")] [NodeIcon("f900ccca7c66371459b52036efeb8778", "cfd0e78235c50db46bc12d1751492ecf")] [Category("Cielonos/Movement")] public class Standoff : MovementBase { /// /// 运动模式枚举。 /// public enum StandoffMovementMode : byte { /// 绕着目标横向移动(左右绕圈)。 Strafe, /// 小幅度前进或后退。 AdvanceRetreat, /// 原地短暂停滞(蓄势待发)。 Pause, } #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("Movement")] [Tooltip("横向移动时的速度。")] [SerializeField] protected SharedVariable m_StrafeSpeed = 2.5f; [Tooltip("前后踱步时的速度。")] [SerializeField] protected SharedVariable m_AdvanceRetreatSpeed = 1.5f; [Tooltip("面朝目标的旋转角速度 (度/秒)。")] [SerializeField] protected SharedVariable m_RotationSpeed = 360f; [Header("Strafe")] [Tooltip("横向绕圈时,每段圆弧的最小角度 (度)。")] [SerializeField] protected SharedVariable m_StrafeArcMin = 30f; [Tooltip("横向绕圈时,每段圆弧的最大角度 (度)。")] [SerializeField] protected SharedVariable m_StrafeArcMax = 90f; [Header("Advance / Retreat")] [Tooltip("前后踱步的最小距离 (米)。")] [SerializeField] protected SharedVariable m_AdvanceRetreatDistanceMin = 1f; [Tooltip("前后踱步的最大距离 (米)。")] [SerializeField] protected SharedVariable m_AdvanceRetreatDistanceMax = 3f; [Header("Mode Weights & Duration")] [Tooltip("横向绕圈的权重。")] [SerializeField] protected SharedVariable m_StrafeWeight = 5f; [Tooltip("前后踱步的权重。")] [SerializeField] protected SharedVariable m_AdvanceRetreatWeight = 3f; [Tooltip("短暂停滞的权重。")] [SerializeField] protected SharedVariable m_PauseWeight = 2f; [Tooltip("每个运动模式持续的最短时间 (秒)。")] [SerializeField] protected SharedVariable m_ModeDurationMin = 1f; [Tooltip("每个运动模式持续的最长时间 (秒)。")] [SerializeField] protected SharedVariable m_ModeDurationMax = 3f; [Tooltip("模式切换之间 / 到达航点后的短暂停顿时间 (秒)。设为 0 则无停顿。")] [SerializeField] protected SharedVariable m_TransitionPause = 0.2f; [Header("Task Duration")] [Tooltip("是否无限期运行 (仅靠 Conditional Abort 打断)。")] [SerializeField] protected SharedVariable m_IsInfinite = false; [Tooltip("如果非无限, 最长持续时间 (秒)。到期后返回 Success。")] [SerializeField] protected SharedVariable m_MaxDuration = 8f; #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 StandoffMovementMode _currentMode; private float _modeStartTime; private float _modeDuration; private bool _isInTransitionPause; private float _transitionPauseEndTime; // 当前运动参数 private int _strafeDirection; // +1 = 右绕, -1 = 左绕 private bool _hasValidDestination; // NavMesh 采样 private const int k_MaxSampleRetries = 5; // 安全距离余量: 航点不会正好落在边界上,留出这个余量 private const float k_DistanceMargin = 0.5f; // 最小有效航点距离: 航点与当前位置之间低于此距离视为"太近",不生成 private const float k_MinMeaningfulDistance = 0.8f; #endregion #region Convenience /// 安全最小距离: 确保至少为 0.5m。 private float SafeMinDistance => Mathf.Max(m_MinDistance.Value, 0.5f); /// 安全最大距离: 确保至少比最小距离大 0.5m。 private float SafeMaxDistance => Mathf.Max(m_MaxDistance.Value, SafeMinDistance + 0.5f); /// 距离带中心半径。 private float CenterRadius => (SafeMinDistance + SafeMaxDistance) * 0.5f; /// 用于航点钳位的有效最小距离 (含安全余量)。 private float ClampMinDistance => SafeMinDistance + k_DistanceMargin; /// 用于航点钳位的有效最大距离 (含安全余量)。 private float ClampMaxDistance => Mathf.Max(SafeMaxDistance - k_DistanceMargin, ClampMinDistance + 0.1f); private float GetDistanceToTarget() { Vector3 diff = transform.position - m_Target.Value.transform.position; diff.y = 0f; return diff.magnitude; } private Vector3 GetFlatOffsetFromTarget() { Vector3 diff = transform.position - m_Target.Value.transform.position; diff.y = 0f; return diff; } #endregion #region Lifecycle public override void OnAwake() { base.OnAwake(); _navPathfinder = m_Pathfinder as NavMeshAgentPathfinder; if (_navPathfinder == null) { Debug.LogError("[Standoff] Requires NavMeshAgentPathfinder."); return; } _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) { Debug.LogError("[Standoff] A target must be set."); return; } if (_agent == null) return; _originalMaxSpeed = _navPathfinder.Speed; _originalUpdateRotation = _agent.updateRotation; _agent.updateRotation = false; _agent.autoBraking = true; if (_movementSc != null) { _original8WayMovement = _movementSc.is8WayMovement; _movementSc.is8WayMovement = true; } _taskStartTime = Time.time; _isInTransitionPause = false; _hasValidDestination = false; SelectNewMode(); } public override TaskStatus OnUpdate() { if (m_Target == null || m_Target.Value == null) return TaskStatus.Failure; if (_agent == null) return TaskStatus.Failure; // 1. 任务总时长 if (!m_IsInfinite.Value && Time.time - _taskStartTime >= m_MaxDuration.Value) return TaskStatus.Success; // 2. 始终面朝目标 RotateTowardsTarget(); // 3. 过渡停顿 if (_isInTransitionPause) { StopAgent(); if (Time.time >= _transitionPauseEndTime) { _isInTransitionPause = false; if (Time.time - _modeStartTime >= _modeDuration) { SelectNewMode(); } else { GenerateWaypoint(); } } return TaskStatus.Running; } // 4. 模式到期 if (Time.time - _modeStartTime >= _modeDuration) { EnterTransitionPause(); return TaskStatus.Running; } // 5. 执行当前模式 if (_currentMode == StandoffMovementMode.Pause) { StopAgent(); } else { if (_hasValidDestination && HasArrived()) { EnterTransitionPause(); } else if (!_hasValidDestination) { GenerateWaypoint(); } } return TaskStatus.Running; } public override void OnEnd() { base.OnEnd(); if (_agent != null && _agent.isOnNavMesh && _agent.isActiveAndEnabled) { _agent.isStopped = true; _agent.velocity = Vector3.zero; _agent.ResetPath(); } if (_agent != null) { _agent.updateRotation = _originalUpdateRotation; _agent.autoBraking = true; _agent.speed = _originalMaxSpeed; } if (_movementSc != null) { _movementSc.is8WayMovement = _original8WayMovement; } } public override void OnBehaviorTreeStopped(bool paused) { base.OnBehaviorTreeStopped(paused); if (_agent != null && _agent.updateRotation != _originalUpdateRotation) _agent.updateRotation = _originalUpdateRotation; if (_movementSc != null && _movementSc.is8WayMovement != _original8WayMovement) _movementSc.is8WayMovement = _original8WayMovement; } #endregion #region Rotation private void RotateTowardsTarget() { Vector3 direction = m_Target.Value.transform.position - transform.position; direction.y = 0f; if (direction.sqrMagnitude < 0.001f) return; Quaternion targetRotation = Quaternion.LookRotation(direction); transform.rotation = Quaternion.RotateTowards( transform.rotation, targetRotation, m_RotationSpeed.Value * Time.deltaTime ); } #endregion #region Mode Selection private void EnterTransitionPause() { _isInTransitionPause = true; _hasValidDestination = false; _transitionPauseEndTime = Time.time + m_TransitionPause.Value; } private void SelectNewMode() { _modeStartTime = Time.time; _modeDuration = Random.Range(m_ModeDurationMin.Value, m_ModeDurationMax.Value); _hasValidDestination = false; float totalWeight = m_StrafeWeight.Value + m_AdvanceRetreatWeight.Value + m_PauseWeight.Value; if (totalWeight <= 0f) { _currentMode = StandoffMovementMode.Pause; return; } float roll = Random.Range(0f, totalWeight); if (roll < m_StrafeWeight.Value) { _currentMode = StandoffMovementMode.Strafe; _strafeDirection = Random.value > 0.5f ? 1 : -1; _agent.speed = m_StrafeSpeed.Value; } else if (roll < m_StrafeWeight.Value + m_AdvanceRetreatWeight.Value) { _currentMode = StandoffMovementMode.AdvanceRetreat; _agent.speed = m_AdvanceRetreatSpeed.Value; } else { _currentMode = StandoffMovementMode.Pause; _agent.speed = 0f; return; } GenerateWaypoint(); } #endregion #region Waypoint Generation private void GenerateWaypoint() { bool success = false; switch (_currentMode) { case StandoffMovementMode.Strafe: success = GenerateStrafeWaypoint(); break; case StandoffMovementMode.AdvanceRetreat: success = GenerateAdvanceRetreatWaypoint(); break; } // 如果航点生成失败 (NavMesh 采样失败、空间不足等), // 跳过当前模式,立刻进入过渡并重选 if (!success) { EnterTransitionPause(); } } /// /// 横向绕圈航点: 在以目标为圆心的圆弧上取一个安全的远航点。 /// 每次生成航点时随机取一个距离带内的半径,让路径更有机感。 /// private bool GenerateStrafeWaypoint() { Vector3 targetPos = m_Target.Value.transform.position; Vector3 toSelf = GetFlatOffsetFromTarget(); float currentDist = toSelf.magnitude; // 如果太近,先撤到中心半径 if (currentDist < SafeMinDistance) { Vector3 retreatDir = currentDist > 0.01f ? toSelf.normalized : -transform.forward; Vector3 safePos = targetPos + retreatDir * CenterRadius; return TrySetValidDestination(safePos); } float currentAngle = Mathf.Atan2(toSelf.x, toSelf.z) * Mathf.Rad2Deg; float arcAngle = Random.Range(m_StrafeArcMin.Value, m_StrafeArcMax.Value); float newAngle = currentAngle + arcAngle * _strafeDirection; // 每次航点使用距离带内的随机半径,产生自然波动 float radius = Random.Range(ClampMinDistance, ClampMaxDistance); float rad = newAngle * Mathf.Deg2Rad; Vector3 destination = targetPos + new Vector3(Mathf.Sin(rad), 0f, Mathf.Cos(rad)) * radius; destination = ClampDestinationToSafeBand(targetPos, destination); return TrySetValidDestination(destination); } /// /// 前后踱步航点: 每次生成航点时实时判断前进/后退方向。 /// 按距离与距离带中线的关系决定: /// - 距离 ≤ 中线: 偏向后退 /// - 距离 > 中线: 偏向前进 /// 确保航点不会卡在边界上。 /// private bool GenerateAdvanceRetreatWaypoint() { Vector3 targetPos = m_Target.Value.transform.position; Vector3 toSelf = GetFlatOffsetFromTarget(); float currentDist = toSelf.magnitude; // 如果太近,强制后退到中心半径 if (currentDist < SafeMinDistance) { Vector3 retreatDir = currentDist > 0.01f ? toSelf.normalized : -transform.forward; Vector3 safePos = targetPos + retreatDir * CenterRadius; return TrySetValidDestination(safePos); } // --- 实时决定方向 --- float center = CenterRadius; float advanceRetreatSign; // 距离中线的偏移量: 正 = 偏远, 负 = 偏近 float offsetFromCenter = currentDist - center; float bandHalfWidth = (SafeMaxDistance - SafeMinDistance) * 0.5f; if (bandHalfWidth < 0.1f) { // 距离带极窄, 强制后退 advanceRetreatSign = -1f; } else { // 偏离中线越远, 回到中线方向的概率越大 // normalizedOffset: -1(在最小距离处) ~ +1(在最大距离处) float normalizedOffset = Mathf.Clamp(offsetFromCenter / bandHalfWidth, -1f, 1f); // 基础概率: 前进概率 = 0.5 + normalizedOffset * 0.4 // 在最小距离: 前进概率 10% (几乎只后退) // 在中线: 前进概率 50% (五五开) // 在最大距离: 前进概率 90% (几乎只前进) float advanceProbability = 0.5f + normalizedOffset * 0.4f; advanceRetreatSign = Random.value < advanceProbability ? 1f : -1f; } // --- 计算目标距离 --- Vector3 radialDir = toSelf.normalized; float stepDistance = Random.Range(m_AdvanceRetreatDistanceMin.Value, m_AdvanceRetreatDistanceMax.Value); // 靠近: dist 减小, 远离: dist 增大 float newDist = currentDist - stepDistance * advanceRetreatSign; // 钳位到含余量的安全范围 newDist = Mathf.Clamp(newDist, ClampMinDistance, ClampMaxDistance); // --- 航点太近检查 --- float actualMoveDist = Mathf.Abs(newDist - currentDist); if (actualMoveDist < k_MinMeaningfulDistance) { // 航点距当前位置太近,翻转方向重新计算 advanceRetreatSign = -advanceRetreatSign; newDist = currentDist - stepDistance * advanceRetreatSign; newDist = Mathf.Clamp(newDist, ClampMinDistance, ClampMaxDistance); actualMoveDist = Mathf.Abs(newDist - currentDist); if (actualMoveDist < k_MinMeaningfulDistance) { // 两个方向都没有足够空间,放弃此模式 return false; } } Vector3 destination = targetPos + radialDir * newDist; return TrySetValidDestination(destination); } /// /// 将目标点钳位到含余量的安全距离带内。 /// private Vector3 ClampDestinationToSafeBand(Vector3 targetPos, Vector3 destination) { Vector3 fromTarget = destination - targetPos; fromTarget.y = 0f; float dist = fromTarget.magnitude; if (dist < 0.01f) { return targetPos + (-transform.forward) * CenterRadius; } float clampedDist = Mathf.Clamp(dist, ClampMinDistance, ClampMaxDistance); if (Mathf.Abs(clampedDist - dist) > 0.01f) { return targetPos + fromTarget.normalized * clampedDist; } return destination; } /// /// 尝试设置有效航点。成功时标记 _hasValidDestination 并启动 Agent。 /// private bool TrySetValidDestination(Vector3 destination) { if (TrySampleAndSetDestination(destination)) { _hasValidDestination = true; EnsureAgentMoving(); return true; } return false; } #endregion #region NavMesh Utility private void StopAgent() { if (_agent.isOnNavMesh && !_agent.isStopped) { _agent.isStopped = true; _agent.velocity = Vector3.zero; } } private void EnsureAgentMoving() { if (_agent.isOnNavMesh && _agent.isStopped) { _agent.isStopped = false; } } private bool TrySampleAndSetDestination(Vector3 destination) { Vector3 sampledPos = destination; if (SamplePosition(ref sampledPos)) { sampledPos = ClampDestinationToSafeBand(m_Target.Value.transform.position, sampledPos); SetDestination(sampledPos); return true; } Vector3 targetPos = m_Target.Value.transform.position; for (int i = 1; i <= k_MaxSampleRetries; i++) { Vector3 safeCenter = targetPos + (destination - targetPos).normalized * CenterRadius; Vector3 retryPos = Vector3.Lerp(destination, safeCenter, i * 0.2f); if (SamplePosition(ref retryPos)) { retryPos = ClampDestinationToSafeBand(targetPos, retryPos); SetDestination(retryPos); return true; } } return false; } #endregion #region Reset & Debug public override void Reset() { base.Reset(); m_Target = null; m_MinDistance = 4f; m_MaxDistance = 7f; m_StrafeSpeed = 2.5f; m_AdvanceRetreatSpeed = 1.5f; m_RotationSpeed = 360f; m_StrafeArcMin = 30f; m_StrafeArcMax = 90f; m_AdvanceRetreatDistanceMin = 1f; m_AdvanceRetreatDistanceMax = 3f; m_StrafeWeight = 5f; m_AdvanceRetreatWeight = 3f; m_PauseWeight = 2f; m_ModeDurationMin = 1f; m_ModeDurationMax = 3f; m_TransitionPause = 0.2f; m_IsInfinite = false; m_MaxDuration = 8f; } #if UNITY_EDITOR protected override void OnDrawGizmosSelected() { if (m_Target == null || m_Target.Value == null) return; Vector3 targetPos = m_Target.Value.transform.position; // 最小距离 (红色 — 不可进入) UnityEditor.Handles.color = new Color(1f, 0.3f, 0.3f, 0.3f); UnityEditor.Handles.DrawWireDisc(targetPos, Vector3.up, m_MinDistance.Value); // 有效活动范围 (黄色实线) UnityEditor.Handles.color = new Color(1f, 0.8f, 0f, 0.25f); UnityEditor.Handles.DrawWireDisc(targetPos, Vector3.up, m_MinDistance.Value + k_DistanceMargin); UnityEditor.Handles.DrawWireDisc(targetPos, Vector3.up, Mathf.Max(m_MaxDistance.Value - k_DistanceMargin, m_MinDistance.Value + k_DistanceMargin + 0.1f)); // 最大距离 (蓝色) UnityEditor.Handles.color = new Color(0.3f, 0.6f, 1f, 0.3f); UnityEditor.Handles.DrawWireDisc(targetPos, Vector3.up, m_MaxDistance.Value); // 中心半径 (青色) UnityEditor.Handles.color = new Color(0f, 1f, 1f, 0.1f); UnityEditor.Handles.DrawWireDisc(targetPos, Vector3.up, (m_MinDistance.Value + m_MaxDistance.Value) * 0.5f); } #endif #endregion } }