388 lines
15 KiB
C#
388 lines
15 KiB
C#
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;
|
||
}
|
||
}
|
||
}
|