using System.Collections.Generic; using UnityEngine; using BaseGames.Core; namespace BaseGames.Combat { /// /// 攻击判定盒。挂载在武器 Prefab 或技能 HitBox Prefab 的子节点上。 /// 直接挂在 Player Prefab 子节点 [HitBoxGround/Up/Down/Air]。 /// Collider2D 需设 IsTrigger = true,Layer = PlayerHitBox 或 EnemyHitBox。 /// [RequireComponent(typeof(Collider2D))] public class HitBox : MonoBehaviour { [SerializeField] private DamageSourceSO _defaultSource; [SerializeField] private float _hitCooldown = 0.1f; /// /// HitBox 标识符,供 PlayerAnimationEvents / EnemyAnimationEvents 按名称精确激活特定判定盒。 /// 留空表示"无 Id";事件 payload 为空时将操作所有 HitBox。 /// [SerializeField] private string _id = ""; public string Id => _id; /// /// 对立阵营 HitBox 所在的 Layer 掩码(用于拼刀检测)。 /// Inspector 中将 PlayerHitBox 与 EnemyHitBox 两个 Layer 均勾选。 /// [SerializeField] private LayerMask _rivalHitBoxMask; private DamageSourceSO _currentSource; private Transform _attackerTransform; private Rigidbody2D _ownerRigidbody; private bool _isActive; private IClashService _clashService; /// HitBox 当前是否激活(供 ClashResolver 查询)。 public bool IsActive => _isActive; /// 当前 Source 是否携带 CanClash 标记(供 ClashResolver 查询)。 public bool CanClash => _currentSource != null && _currentSource.Flags.HasFlag(DamageFlags.CanClash); /// 宿主角色的 Rigidbody2D(用于拼刀弹开力计算)。 public Rigidbody2D OwnerRigidbody => _ownerRigidbody; // 拼刀检测所需的对立层掩码(Inspector 配置) /// 命中确认委托(PlayerCombat / EnemyCombat 订阅)。 public event System.Action OnHitConfirmed; /// /// 激活 HitBox。source/attacker 均可选,未传则使用 Inspector 默认值。 /// ⚠️ 不存在 Activate(float duration) 重载。 /// public void Activate(DamageSourceSO source = null, Transform attacker = null) { _currentSource = source ?? _defaultSource; _attackerTransform = attacker ?? transform; _isActive = true; // 缓存宿主 Rigidbody2D(沿父层级向上查找) _ownerRigidbody = _attackerTransform.GetComponentInParent(); // 每次激活清空当前激活期已命中目标集合(防止连击连段导致同一阶段多次命中目标) _hitThisActivation.Clear(); _hitCooldownTimers.Clear(); } public void Deactivate() { _isActive = false; _hitThisActivation.Clear(); _hitCooldownTimers.Clear(); } /// 仅替换当前 DamageSource(不改变激活状态,供 PlayerCombat 连击段切换)。 public void SetDamageSource(DamageSourceSO source) { if (source != null) _currentSource = source; } private void Awake() { // 确保 Collider2D 是 Trigger var col = GetComponent(); if (!col.isTrigger) Debug.LogWarning($"[HitBox] {name}: Collider2D.isTrigger 应为 true。", this); // 缓存 IClashService:OnTriggerEnter2D 为物理热路径,避免每次调用 Dictionary 查找 _clashService = ServiceLocator.GetOrDefault(); } private void OnDisable() { _isActive = false; _hitThisActivation.Clear(); _hitCooldownTimers.Clear(); } private void OnTriggerExit2D(Collider2D other) { // 目标离开判定区域时清除其冷却记录,防止持续激活的 HitBox(环境危险等) // 因有效目标持续流动而无限积累已离场对象。 // 注意:_hitThisActivation 刻意保留,确保同一激活期内不重复命中。 _hitCooldownTimers.Remove(other); } private void OnTriggerEnter2D(Collider2D other) { if (!_isActive) return; if (_currentSource == null) { Debug.LogWarning($"[HitBox] {name}: 无 DamageSourceSO,跳过命中。", this); return; } // 同一激活期防止对同一 Collider 重复命中(一次攻击每个目标至多命中一次) if (!_hitThisActivation.Add(other)) return; if (!CheckCooldown(other)) return; Vector2 knockDir = ((Vector2)other.bounds.center - (Vector2)_attackerTransform.position).normalized; // ⚡ 零 GC:struct 工厂,运行时字段内联传入 var info = DamageInfo.From( _currentSource, knockDir, _attackerTransform.position, _attackerTransform.gameObject.layer); // ① 拼刀检测:当前 HitBox 携带 CanClash 标记,且碰到对立阵营的 HitBox 层 int otherLayer = other.gameObject.layer; bool isRivalHitBoxLayer = (_rivalHitBoxMask.value & (1 << otherLayer)) != 0; if (isRivalHitBoxLayer && CanClash) { var rivalHitBox = other.GetComponent(); if (rivalHitBox != null && rivalHitBox.IsActive && rivalHitBox.CanClash) { _clashService?.ResolveClash(this, rivalHitBox); return; // 拼刀,中止伤害流水线 } } // ② 命中 HurtBox var hurtBox = other.GetComponent(); if (hurtBox != null) { hurtBox.ReceiveDamage(info); OnHitConfirmed?.Invoke(info); return; } // ③ 命中 IBreakable(机关/障碍物) other.GetComponent()?.TryInteract(info); } // ── 当前激活期已命中目标集合(防止复合子 Collider 导致同帧多次命中)──────────── private readonly HashSet _hitThisActivation = new(8); // ── 同目标多帧命中冷却(防止 Trigger 处于重叠状态时重入等殊情导致的连击)── private readonly Dictionary _hitCooldownTimers = new(8); private bool CheckCooldown(Collider2D other) { float now = Time.time; if (_hitCooldownTimers.TryGetValue(other, out float last) && now - last < _hitCooldown) return false; _hitCooldownTimers[other] = now; return true; } #if UNITY_EDITOR private void OnDrawGizmos() { var col = GetComponent(); if (col == null) return; // 激活时显示橙色判定框,非激活时显示极淡轮廓 Gizmos.color = _isActive ? new UnityEngine.Color(1f, 0.5f, 0f, 0.55f) : new UnityEngine.Color(1f, 0.5f, 0f, 0.1f); Gizmos.DrawCube(col.bounds.center, col.bounds.size); Gizmos.color = new UnityEngine.Color(1f, 0.5f, 0f, 0.9f); Gizmos.DrawWireCube(col.bounds.center, col.bounds.size); } #endif } }