Files
Cielonos/Assets/Scripts/MainGame/Characters/Automata/AI/Actions/Movement/Standoff.cs
SoulliesOfficial f26f9fd374 爆更
2026-03-20 12:07:44 -04:00

651 lines
23 KiB
C#
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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
{
/// <summary>
/// 对峙与游走 (Standoff)
/// 敌人面朝玩家,在指定对峙距离带 (MinDistance ~ MaxDistance) 上
/// 进行横向绕圈、前后踱步和短暂停滞的随机组合移动。
/// 通过运动模式权重可塑造不同敌人的"性格"(激进型 / 谨慎型)。
/// </summary>
[Description("对峙与游走。敌人面朝玩家,在攻击距离边缘进行横向绕圈与随机踱步。")]
[NodeIcon("f900ccca7c66371459b52036efeb8778", "cfd0e78235c50db46bc12d1751492ecf")]
[Category("Cielonos/Movement")]
public class Standoff : MovementBase
{
/// <summary>
/// 运动模式枚举。
/// </summary>
public enum StandoffMovementMode : byte
{
/// <summary>绕着目标横向移动(左右绕圈)。</summary>
Strafe,
/// <summary>小幅度前进或后退。</summary>
AdvanceRetreat,
/// <summary>原地短暂停滞(蓄势待发)。</summary>
Pause,
}
#region Parameters
[Header("Target & Distance")]
[Tooltip("对峙的目标对象。")]
[SerializeField] protected SharedVariable<GameObject> m_Target;
[Tooltip("与目标保持的最小距离 (米)。敌人不会比这更近。")]
[SerializeField] protected SharedVariable<float> m_MinDistance = 4f;
[Tooltip("与目标保持的最大距离 (米)。敌人不会比这更远。")]
[SerializeField] protected SharedVariable<float> m_MaxDistance = 7f;
[Header("Movement")]
[Tooltip("横向移动时的速度。")]
[SerializeField] protected SharedVariable<float> m_StrafeSpeed = 2.5f;
[Tooltip("前后踱步时的速度。")]
[SerializeField] protected SharedVariable<float> m_AdvanceRetreatSpeed = 1.5f;
[Tooltip("面朝目标的旋转角速度 (度/秒)。")]
[SerializeField] protected SharedVariable<float> m_RotationSpeed = 360f;
[Header("Strafe")]
[Tooltip("横向绕圈时,每段圆弧的最小角度 (度)。")]
[SerializeField] protected SharedVariable<float> m_StrafeArcMin = 30f;
[Tooltip("横向绕圈时,每段圆弧的最大角度 (度)。")]
[SerializeField] protected SharedVariable<float> m_StrafeArcMax = 90f;
[Header("Advance / Retreat")]
[Tooltip("前后踱步的最小距离 (米)。")]
[SerializeField] protected SharedVariable<float> m_AdvanceRetreatDistanceMin = 1f;
[Tooltip("前后踱步的最大距离 (米)。")]
[SerializeField] protected SharedVariable<float> m_AdvanceRetreatDistanceMax = 3f;
[Header("Mode Weights & Duration")]
[Tooltip("横向绕圈的权重。")]
[SerializeField] protected SharedVariable<float> m_StrafeWeight = 5f;
[Tooltip("前后踱步的权重。")]
[SerializeField] protected SharedVariable<float> m_AdvanceRetreatWeight = 3f;
[Tooltip("短暂停滞的权重。")]
[SerializeField] protected SharedVariable<float> m_PauseWeight = 2f;
[Tooltip("每个运动模式持续的最短时间 (秒)。")]
[SerializeField] protected SharedVariable<float> m_ModeDurationMin = 1f;
[Tooltip("每个运动模式持续的最长时间 (秒)。")]
[SerializeField] protected SharedVariable<float> m_ModeDurationMax = 3f;
[Tooltip("模式切换之间 / 到达航点后的短暂停顿时间 (秒)。设为 0 则无停顿。")]
[SerializeField] protected SharedVariable<float> m_TransitionPause = 0.2f;
[Header("Task Duration")]
[Tooltip("是否无限期运行 (仅靠 Conditional Abort 打断)。")]
[SerializeField] protected SharedVariable<bool> m_IsInfinite = false;
[Tooltip("如果非无限, 最长持续时间 (秒)。到期后返回 Success。")]
[SerializeField] protected SharedVariable<float> 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
/// <summary>安全最小距离: 确保至少为 0.5m。</summary>
private float SafeMinDistance => Mathf.Max(m_MinDistance.Value, 0.5f);
/// <summary>安全最大距离: 确保至少比最小距离大 0.5m。</summary>
private float SafeMaxDistance => Mathf.Max(m_MaxDistance.Value, SafeMinDistance + 0.5f);
/// <summary>距离带中心半径。</summary>
private float CenterRadius => (SafeMinDistance + SafeMaxDistance) * 0.5f;
/// <summary>用于航点钳位的有效最小距离 (含安全余量)。</summary>
private float ClampMinDistance => SafeMinDistance + k_DistanceMargin;
/// <summary>用于航点钳位的有效最大距离 (含安全余量)。</summary>
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<Automata>();
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();
}
}
/// <summary>
/// 横向绕圈航点: 在以目标为圆心的圆弧上取一个安全的远航点。
/// 每次生成航点时随机取一个距离带内的半径,让路径更有机感。
/// </summary>
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);
}
/// <summary>
/// 前后踱步航点: 每次生成航点时实时判断前进/后退方向。
/// 按距离与距离带中线的关系决定:
/// - 距离 ≤ 中线: 偏向后退
/// - 距离 > 中线: 偏向前进
/// 确保航点不会卡在边界上。
/// </summary>
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);
}
/// <summary>
/// 将目标点钳位到含余量的安全距离带内。
/// </summary>
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;
}
/// <summary>
/// 尝试设置有效航点。成功时标记 _hasValidDestination 并启动 Agent。
/// </summary>
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
}
}