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

388 lines
15 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
{
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;
}
}
}