Files
zeling_v2/Assets/_Game/Scripts/Enemies/AI/BD_ChasePlayer.cs
Joywayer 06048c966a feat: Add HurtBoxOwnerGuard to prevent multiple damage registrations from the same HitBox activation
- Implemented HurtBoxOwnerGuard to ensure that multiple HurtBoxes on the same character do not register damage multiple times during a single HitBox activation.
- Added custom editor for HitBox to facilitate the creation of shape colliders with HitBoxColliderProxy.
- Introduced PhysicsPerceptionSystem for enemy perception, supporting multiple detection modes including RangeCircle, BatchLOS, FanCast, and BoxCast.
- Created EnemyPatrolZone to define patrol and chase areas for enemies, allowing for shared zones among multiple enemies.
- Added BD_IsOutsideZone conditional task for Behavior Designer to check if an enemy or player is outside a defined patrol zone.
2026-06-02 16:10:44 +08:00

146 lines
6.1 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.
#if GRAPH_DESIGNER
using UnityEngine;
using Opsive.BehaviorDesigner.Runtime.Tasks;
using Opsive.BehaviorDesigner.Runtime.Tasks.Actions;
using BaseGames.Enemies;
namespace BaseGames.Enemies.AI
{
/// <summary>
/// BD Action追击玩家三阶段视线丢失模型
///
/// <list type="bullet">
/// <item><b>Tracking</b>:持有视线,以奔跑速度追击并更新 LastKnownPlayerPosition。</item>
/// <item><b>Searching</b>:视线丢失超过 LostSightBuffer~0.3s)后进入。速度降低,向最后可见位置行进;若恢复视线即回到 Tracking。</item>
/// <item><b>Lost</b>Searching 持续超过 LoseLinkTimeout → 返回 Failure让 BD 树切入 BD_InvestigateLastKnown。</item>
/// </list>
///
/// 短暂遮挡(如绕过柱子)在 LostSightBuffer 内不会改变速度,避免追击手感割裂。
/// </summary>
[TaskName("Chase Player")]
[TaskCategory("BaseGames/Enemy/Movement")]
[TaskDescription("三段式追击Tracking→Searching→Lost丢失视线后进入缓冲期")]
public sealed class BD_ChasePlayer : Action
{
[Header("距离限制")]
[Tooltip("追击距离上限m0 = 使用 EnemyStatsSO.MaxChaseDistance")]
[SerializeField] [Min(0f)] private float m_MaxChaseDistance = 0f;
[Header("视线丢失")]
[Tooltip("视线丢失判定超时s0 = 使用 EnemyStatsSO.LoseLinkTimeout")]
[SerializeField] [Min(0f)] private float m_LoseLinkTimeout = 0f;
[Tooltip("视线刚丢失后的缓冲期s此期间保持追击速度短暂遮挡不触发搜索模式")]
[SerializeField] [Min(0f)] private float m_LostSightBuffer = 0.3f;
[Header("路径规划")]
[Tooltip("路径重规划阈值m玩家移动超过此距离后才重新规划避免每帧请求")]
[SerializeField] [Min(0.1f)] private float m_ReplanThreshold = 1.5f;
[Header("搜索阶段")]
[Tooltip("Searching 阶段速度倍率(相对于行走速度;<1 = 减速搜索)")]
[SerializeField] [UnityEngine.Range(0.3f, 1.5f)] private float m_SearchSpeedMultiplier = 0.7f;
private enum ChaseSubState { Tracking, Searching }
private EnemyBase _enemy;
private float _losTimer;
private Vector2 _lastReplanPos;
private ChaseSubState _subState;
public override void OnAwake() => _enemy = gameObject.GetComponent<EnemyBase>();
public override void OnStart()
{
_enemy.SetAiPhase(AiPhase.Chase);
_losTimer = 0f;
_lastReplanPos = Vector2.positiveInfinity;
_subState = ChaseSubState.Tracking;
float runSpeed = _enemy.Stats?.RunSpeed ?? 4f;
_enemy.Nav?.SetSpeed(runSpeed);
var ac = _enemy.AnimConfig;
if (_enemy.Animancer != null && ac?.Run != null)
_enemy.Animancer.Play(ac.Run);
}
public override TaskStatus OnUpdate()
{
if (_enemy == null || _enemy.PlayerTransform == null)
return TaskStatus.Failure;
float maxDist = m_MaxChaseDistance > 0f ? m_MaxChaseDistance : (_enemy.Stats?.MaxChaseDistance ?? 15f);
float loseTime = m_LoseLinkTimeout > 0f ? m_LoseLinkTimeout : (_enemy.Stats?.LoseLinkTimeout ?? 2f);
// 超出最大追击距离 → 放弃追击
if (_enemy.Stats != null && _enemy.Stats.SqrDistanceToPlayer > maxDist * maxDist)
return TaskStatus.Failure;
Vector2 playerPos = _enemy.PlayerTransform.position;
// 若配置了巡逻区域,且玩家超出追击边界 → 放弃追击(优先级高于纯距离限制)
var zone = _enemy.PatrolZone;
if (zone != null && !zone.ContainsChase(playerPos))
return TaskStatus.Failure;
if (_enemy.IsPlayerVisible())
{
// 视线恢复Searching → Tracking恢复奔跑速度
if (_subState == ChaseSubState.Searching)
{
_subState = ChaseSubState.Tracking;
_enemy.Nav?.SetSpeed(_enemy.Stats?.RunSpeed ?? 4f);
}
_losTimer = 0f;
_enemy.LastKnownPlayerPosition = playerPos;
}
else
{
_losTimer += Time.deltaTime;
if (_subState == ChaseSubState.Tracking)
{
// 缓冲期结束 → 切入 Searching减速并向最后可见位置行进
if (_losTimer >= m_LostSightBuffer)
{
_subState = ChaseSubState.Searching;
float searchSpeed = (_enemy.Stats?.WalkSpeed ?? 2f) * m_SearchSpeedMultiplier;
_enemy.Nav?.SetSpeed(searchSpeed);
_enemy.MoveTo(_enemy.LastKnownPlayerPosition);
_lastReplanPos = _enemy.LastKnownPlayerPosition;
}
}
else // Searching
{
if (_losTimer >= loseTime)
return TaskStatus.Failure; // 搜索超时 → 进入 BD_InvestigateLastKnown
}
}
// Tracking 阶段按阈值重规划路径
if (_subState == ChaseSubState.Tracking)
{
float sqrReplan = m_ReplanThreshold * m_ReplanThreshold;
if ((playerPos - _lastReplanPos).sqrMagnitude > sqrReplan)
{
_enemy.MoveTo(playerPos);
_lastReplanPos = playerPos;
}
}
_enemy.FacePlayer();
return TaskStatus.Running;
}
public override void OnEnd()
{
float walkSpeed = _enemy?.Stats?.WalkSpeed ?? 2f;
_enemy?.Nav?.SetSpeed(walkSpeed);
_enemy?.StopMovement();
_subState = ChaseSubState.Tracking; // 重置子状态,防止下次激活时以错误状态重入
}
}
}
#endif