319 lines
12 KiB
C#
319 lines
12 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
|
||
{
|
||
[Description("前后移动 (Advance/Retreat)。设定一个与目标的期望距离,敌人会自动前进或后退到该距离。支持随机区间与次数/时间限制。")]
|
||
[NodeIcon("f900ccca7c66371459b52036efeb8778", "cfd0e78235c50db46bc12d1751492ecf")]
|
||
[Category("Cielonos/Movement")]
|
||
public class AdvanceRetreat : MovementBase
|
||
{
|
||
#region Parameters
|
||
|
||
[Header("Target")]
|
||
[Tooltip("对峙的目标对象。")]
|
||
[SerializeField] protected SharedVariable<GameObject> m_Target;
|
||
|
||
[Header("Distance Config")]
|
||
[Tooltip("目标距离的最小值 (米)。如果想设定固定距离,将其与最大值设为相同即可。")]
|
||
[SerializeField] protected SharedVariable<float> m_MinTargetDistance = 3f;
|
||
|
||
[Tooltip("目标距离的最大值 (米)。")]
|
||
[SerializeField] protected SharedVariable<float> m_MaxTargetDistance = 6f;
|
||
|
||
[Header("Movement")]
|
||
[Tooltip("前后移动时的速度。")]
|
||
[SerializeField] protected SharedVariable<float> m_MoveSpeed = 2f;
|
||
|
||
[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;
|
||
|
||
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;
|
||
_agent.autoBraking = true;
|
||
_agent.speed = m_MoveSpeed.Value;
|
||
|
||
if (_movementSc != null)
|
||
{
|
||
_original8WayMovement = _movementSc.is8WayMovement;
|
||
_movementSc.is8WayMovement = true;
|
||
}
|
||
|
||
_taskStartTime = Time.time;
|
||
_currentMoveCount = 0;
|
||
_isTimeUp = false;
|
||
_isMoving = false;
|
||
_transitionPauseEndTime = 0f;
|
||
|
||
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 (_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; // 极近距离兜底
|
||
}
|
||
|
||
// 获取本次移动的目标距离
|
||
float targetDist = Random.Range(m_MinTargetDistance.Value, m_MaxTargetDistance.Value);
|
||
|
||
// 如果当前距离目标太近,toSelf.normalized 其实就是互相远离的方向,这里就是后退;
|
||
// 如果太远,toSelf.normalized 也是远离的方向,乘以 targetDist 就是前进。
|
||
// 无论是前进还是后退,目标点坐标在同一条射线上:targetPos + radialDir * targetDist
|
||
// 绝佳的几何性质保证了无论向前还是向后,方向绝对正确无误
|
||
Vector3 radialDir = toSelf.normalized;
|
||
Vector3 destination = targetPos + radialDir * targetDist;
|
||
|
||
if (TrySampleAndSetDestination(targetPos, destination))
|
||
{
|
||
_isMoving = true;
|
||
_singleMoveStartTime = Time.time;
|
||
if (_agent.isStopped) _agent.isStopped = false;
|
||
}
|
||
else
|
||
{
|
||
// 如果完全找不到航点(可能是身后是墙),放弃这次移动
|
||
_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 retryPos = Vector3.Lerp(destination, centerTargetPos, i * 0.15f);
|
||
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_MinTargetDistance = 3f;
|
||
m_MaxTargetDistance = 6f;
|
||
m_MoveSpeed = 2f;
|
||
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;
|
||
}
|
||
|
||
#if UNITY_EDITOR
|
||
protected override void OnDrawGizmosSelected()
|
||
{
|
||
if (m_Target == null || m_Target.Value == null) return;
|
||
Vector3 targetPos = m_Target.Value.transform.position;
|
||
|
||
if (Mathf.Abs(m_MinTargetDistance.Value - m_MaxTargetDistance.Value) < 0.01f)
|
||
{
|
||
UnityEditor.Handles.color = new Color(0f, 1f, 1f, 0.4f);
|
||
UnityEditor.Handles.DrawWireDisc(targetPos, Vector3.up, m_MinTargetDistance.Value);
|
||
}
|
||
else
|
||
{
|
||
UnityEditor.Handles.color = new Color(1f, 0.3f, 0.3f, 0.4f);
|
||
UnityEditor.Handles.DrawWireDisc(targetPos, Vector3.up, m_MinTargetDistance.Value);
|
||
UnityEditor.Handles.color = new Color(0.3f, 0.6f, 1f, 0.4f);
|
||
UnityEditor.Handles.DrawWireDisc(targetPos, Vector3.up, m_MaxTargetDistance.Value);
|
||
}
|
||
}
|
||
#endif
|
||
}
|
||
}
|