206 lines
7.4 KiB
C#
206 lines
7.4 KiB
C#
using Opsive.BehaviorDesigner.AddOns.MovementPack.Runtime.Tasks;
|
||
using Opsive.BehaviorDesigner.AddOns.Shared.Runtime.Pathfinding;
|
||
using Opsive.BehaviorDesigner.Runtime.Tasks;
|
||
using Opsive.BehaviorDesigner.Runtime.Utility;
|
||
using Opsive.GraphDesigner.Runtime;
|
||
using Opsive.GraphDesigner.Runtime.Variables;
|
||
using Opsive.Shared.Utility;
|
||
using Unity.Entities;
|
||
using UnityEngine;
|
||
using UnityEngine.AI;
|
||
|
||
namespace Cielonos.MainGame.Characters.AI
|
||
{
|
||
[Description("在初始出生点附近的指定半径内随机选取一个 NavMesh 上的点并移动过去。到达后等待一段时间(可选),然后返回 Success。")]
|
||
[NodeIcon("Assets/Sprites/Icon/Play.png")]
|
||
[Category("Cielonos/Movement")]
|
||
public class WanderInRange : MovementBase
|
||
{
|
||
[Tooltip("初始移动速度。")]
|
||
[SerializeField] protected SharedVariable<float> m_StartSpeed = 10f;
|
||
|
||
[Tooltip("以初始出生点为圆心的游走半径(单位:米)。")]
|
||
[SerializeField] protected SharedVariable<float> m_WanderRadius = 10f;
|
||
|
||
[Tooltip("到达目标点后等待的时间范围(秒)。Min 与 Max 均为 0 时不等待,直接返回 Success。")]
|
||
[SerializeField] protected SharedVariable<RangeFloat> m_WaitAtDestinationDuration = new RangeFloat(0f, 0f);
|
||
|
||
[Tooltip("判定到达目的地所需的剩余距离阈值(单位:米)。")]
|
||
[SerializeField] protected SharedVariable<float> m_ArrivalDistance = 0.5f;
|
||
|
||
[Tooltip("每次寻路 tick 中,选取随机目标点的最大重试次数。")]
|
||
[SerializeField] protected SharedVariable<int> m_DestinationRetries = 5;
|
||
|
||
#if UNITY_EDITOR
|
||
[Tooltip("是否在无效目标点处绘制调试射线?")]
|
||
[SerializeField] protected bool m_DrawInvalidDestinationRay;
|
||
#endif
|
||
|
||
// 记录行为树初始化时的出生点位置
|
||
private Vector3 m_SpawnPosition;
|
||
private NavMeshAgentPathfinder _navPathfinder;
|
||
private NavMeshAgent _agent;
|
||
|
||
// 到达后等待状态
|
||
private float m_WaitDuration = -1f;
|
||
private float m_DestinationReachedTime = -1f;
|
||
|
||
// 本次 OnStart 是否已成功派发了目标点
|
||
private bool m_HasDestination;
|
||
|
||
/// <summary>
|
||
/// 记录出生点,仅在行为树初始化时调用一次。
|
||
/// </summary>
|
||
public override void OnAwake()
|
||
{
|
||
base.OnAwake();
|
||
_navPathfinder = m_Pathfinder as NavMeshAgentPathfinder;
|
||
if (_navPathfinder == null)
|
||
{
|
||
Debug.LogError("[PrecisePursue] Requires NavMeshAgentPathfinder.");
|
||
return;
|
||
}
|
||
_agent = _navPathfinder.m_NavMeshAgent;
|
||
m_SpawnPosition = transform.position;
|
||
}
|
||
|
||
/// <summary>
|
||
/// 每次节点开始执行时,重置状态并选取新目标。
|
||
/// </summary>
|
||
public override void OnStart()
|
||
{
|
||
base.OnStart();
|
||
|
||
m_WaitDuration = -1f;
|
||
m_DestinationReachedTime = -1f;
|
||
m_HasDestination = TrySetDestination();
|
||
}
|
||
|
||
/// <summary>
|
||
/// 每帧检查是否到达目标点,并处理到达后的等待逻辑。
|
||
/// </summary>
|
||
public override TaskStatus OnUpdate()
|
||
{
|
||
// 若初始选点失败,再尝试一次;仍然失败则返回 Failure
|
||
if (!m_HasDestination)
|
||
{
|
||
m_HasDestination = TrySetDestination();
|
||
if (!m_HasDestination)
|
||
return TaskStatus.Failure;
|
||
}
|
||
|
||
_agent.speed = m_StartSpeed.Value;
|
||
|
||
// 到达判定:HasArrived() 或剩余距离小于阈值
|
||
if (HasArrived() || RemainingDistance < m_ArrivalDistance.Value)
|
||
{
|
||
// 首次到达:决定是否等待
|
||
if (m_WaitDuration < 0f)
|
||
{
|
||
m_WaitDuration = m_WaitAtDestinationDuration.Value.RandomValue;
|
||
|
||
if (m_WaitDuration > 0f)
|
||
{
|
||
m_DestinationReachedTime = Time.time;
|
||
return TaskStatus.Running;
|
||
}
|
||
|
||
return TaskStatus.Success;
|
||
}
|
||
|
||
// 等待中:检查是否已等待足够时间
|
||
if (Time.time >= m_DestinationReachedTime + m_WaitDuration)
|
||
{
|
||
return TaskStatus.Success;
|
||
}
|
||
}
|
||
|
||
return TaskStatus.Running;
|
||
}
|
||
|
||
/// <summary>
|
||
/// 在出生点半径内随机采样 NavMesh 上的有效点,并设定为寻路目标。
|
||
/// </summary>
|
||
/// <returns>成功设定目标点返回 true,所有重试均失败返回 false。</returns>
|
||
private bool TrySetDestination()
|
||
{
|
||
for (int i = 0; i < m_DestinationRetries.Value; i++)
|
||
{
|
||
// 在 XZ 平面的圆形范围内随机选点
|
||
var randomOffset = Random.insideUnitCircle * m_WanderRadius.Value;
|
||
var candidate = m_SpawnPosition + new Vector3(randomOffset.x, 0f, randomOffset.y);
|
||
|
||
if (SamplePosition(ref candidate))
|
||
{
|
||
// 防止 NavMesh 采样将点吸附到半径外
|
||
if (Vector3.Distance(candidate, m_SpawnPosition) <= m_WanderRadius.Value)
|
||
{
|
||
SetDestination(candidate);
|
||
return true;
|
||
}
|
||
}
|
||
|
||
#if UNITY_EDITOR
|
||
if (m_DrawInvalidDestinationRay)
|
||
Debug.DrawRay(candidate, Vector3.up * 2f, Color.red, 1f);
|
||
#endif
|
||
}
|
||
|
||
return false;
|
||
}
|
||
|
||
/// <summary>
|
||
/// 节点结束时重置等待状态。
|
||
/// </summary>
|
||
public override void OnEnd()
|
||
{
|
||
base.OnEnd();
|
||
|
||
m_WaitDuration = -1f;
|
||
m_DestinationReachedTime = -1f;
|
||
m_HasDestination = false;
|
||
}
|
||
|
||
/// <summary>
|
||
/// 保存当前节点运行状态(等待剩余时间)。
|
||
/// </summary>
|
||
public override object Save(World world, Entity entity)
|
||
{
|
||
var saveData = new object[3];
|
||
saveData[0] = base.Save(world, entity);
|
||
saveData[1] = m_WaitDuration;
|
||
// 存储已等待时长而非绝对时间戳,以兼容时间暂停/恢复
|
||
saveData[2] = m_DestinationReachedTime >= 0f ? Time.time - m_DestinationReachedTime : -1f;
|
||
return saveData;
|
||
}
|
||
|
||
/// <summary>
|
||
/// 恢复节点运行状态。
|
||
/// </summary>
|
||
public override void Load(object saveData, World world, Entity entity)
|
||
{
|
||
var arr = (object[])saveData;
|
||
base.Load(arr[0], world, entity);
|
||
m_WaitDuration = (float)arr[1];
|
||
var elapsed = (float)arr[2];
|
||
m_DestinationReachedTime = elapsed >= 0f ? Time.time - elapsed : -1f;
|
||
}
|
||
|
||
/// <summary>
|
||
/// 编辑器重置,恢复字段默认值。
|
||
/// </summary>
|
||
public override void Reset()
|
||
{
|
||
base.Reset();
|
||
|
||
m_WanderRadius = 8f;
|
||
m_WaitAtDestinationDuration = new RangeFloat(0f, 0f);
|
||
m_ArrivalDistance = 0.5f;
|
||
m_DestinationRetries = 5;
|
||
#if UNITY_EDITOR
|
||
m_DrawInvalidDestinationRay = false;
|
||
#endif
|
||
}
|
||
}
|
||
}
|