Files
Cielonos/Assets/Scripts/MainGame/Characters/Automata/AI/Actions/Movement/Strafe.cs

388 lines
15 KiB
C#
Raw Normal View History

2026-03-20 12:07:44 -04:00
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<GameObject> m_Target;
[Tooltip("绕圈的最小距离 (米)。")]
[SerializeField] protected SharedVariable<float> m_MinDistance = 4f;
[Tooltip("绕圈的最大距离 (米)。")]
[SerializeField] protected SharedVariable<float> m_MaxDistance = 7f;
[Header("Strafe Movement")]
[Tooltip("移动速度。")]
[SerializeField] protected SharedVariable<float> m_StrafeSpeed = 2.5f;
[Tooltip("单次横向绕圈的最小移动距离 (步长, 米)。")]
[SerializeField] protected SharedVariable<float> m_StrafeDistanceMin = 2f;
[Tooltip("单次横向绕圈的最大移动距离 (步长, 米)。")]
[SerializeField] protected SharedVariable<float> m_StrafeDistanceMax = 5f;
[Header("Facing")]
[Tooltip("是否在移动过程中面朝目标。如果不勾选,则依赖外部系统的朝向控制。")]
[SerializeField] protected SharedVariable<bool> m_FaceTarget = true;
[Tooltip("面朝目标的旋转角速度 (度/秒)。")]
[SerializeField] protected SharedVariable<float> m_RotationSpeed = 360f;
[Header("Limits & Timing")]
[Tooltip("任务退出限制模式:总时间 / 总次数 / 无限")]
[SerializeField] protected TacticalLimitMode m_LimitMode = TacticalLimitMode.TotalDuration;
[Tooltip("最大移动次数(用于 TotalMoveCount 模式)。")]
[SerializeField] protected SharedVariable<int> m_MaxMoveCount = 3;
[Tooltip("最长总持续时间(秒,用于 TotalDuration 模式)。")]
[SerializeField] protected SharedVariable<float> m_MaxDuration = 8f;
[Tooltip("在总时间结束时,是否强制走完最后一次移动后再退出。")]
[SerializeField] protected SharedVariable<bool> m_FinishLastMoveBeforeEnd = false;
[Tooltip("单次移动的最大持续时间 (秒)。到达时间或到达航点都会结束单词移动。")]
[SerializeField] protected SharedVariable<float> m_SingleMoveMaxDuration = 2f;
[Tooltip("每次到达航点后的短暂停顿 (秒)。")]
[SerializeField] protected SharedVariable<float> 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<Automata>();
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;
}
}
}