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
}
}