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.
This commit is contained in:
2026-06-02 16:10:44 +08:00
parent bcd8b0e90b
commit 06048c966a
47 changed files with 1912 additions and 1195 deletions

View File

@@ -0,0 +1,45 @@
using System.Collections.Generic;
using UnityEngine;
namespace BaseGames.Combat
{
/// <summary>
/// HurtBox 所有者保护组件。防止同一角色身上的多个 HurtBox 在同一次 HitBox 激活中被重复伤害。
///
/// 工作原理:每次 HitBox 激活携带唯一 ActivationId由 HitBox.Activate() 生成递增值)。
/// HurtBox.ReceiveDamage 调用 TryRegisterHit(id) 时,同一 id 只有首次调用返回 true
/// 后续同 id 调用(来自同一角色的其他 HurtBox 子节点)返回 false 并跳过伤害流水线,
/// 从而保证"多个 HurtBox 共享同一 HP 池时,一次攻击只扣一次血"。
///
/// 生命周期:由 HurtBox.Awake() 在角色根节点自动添加(如不存在则创建)。无需手动挂载。
/// 逐帧通过 frameCount 差异懒清空处理集,零 GC 开销。
/// </summary>
[AddComponentMenu("")] // 隐藏菜单:由 HurtBox 自动管理
public sealed class HurtBoxOwnerGuard : MonoBehaviour
{
private readonly HashSet<uint> _processedIds = new(4);
private int _lastClearFrame = -1;
/// <summary>
/// 尝试注册一次命中。
/// </summary>
/// <param name="activationId">HitBox 激活 ID0 = 无追踪路径,始终允许通过)。</param>
/// <returns>true = 首次注册应继续处理伤害false = 同 id 已被处理,跳过。</returns>
public bool TryRegisterHit(uint activationId)
{
// activationId == 0LethalTrap / BodyContactDamage 等旁路路径,不做去重
if (activationId == 0) return true;
EnsureClearedThisFrame();
return _processedIds.Add(activationId);
}
private void EnsureClearedThisFrame()
{
int frame = Time.frameCount;
if (frame == _lastClearFrame) return;
_processedIds.Clear();
_lastClearFrame = frame;
}
}
}