using System.Collections.Generic; using UnityEngine; // ============================================================================ // AttackHitbox // ---------------------------------------------------------------------------- // 플레이어(또는 임의 캐릭터)의 공격 판정 콜라이더. // Player의 자식 GameObject로 두고, 액션마다 Activate/Deactivate로 hit window 표현. // // 핵심: // - CircleCollider2D Trigger (물리 충돌 아닌 트리거 감지만) // - 평소엔 disabled, 액션의 HitTiming~HitDuration 동안만 enabled // - 활성 순간 이미 범위 안에 있던 적도 ScanImmediateOverlap으로 즉시 검사 // (OnTriggerEnter는 다음 frame에야 발화되므로 짧은 hit window엔 부족함) // - _alreadyHit HashSet으로 같은 적에 중복 데미지 방지 // ============================================================================ [RequireComponent(typeof(CircleCollider2D))] public class AttackHitbox : MonoBehaviour { // PlayerController가 구독해서 "방금 hit한 적" 추적용 (잡기 타겟 우선 등에 활용). public event System.Action OnHit; private CircleCollider2D _circleCollider; // Circle 모양 판정용 private BoxCollider2D _boxCollider; // Box 모양 판정용 (Awake에서 자동 생성) private HitShape _activeShape; // 현재 활성 도형 (ScanImmediateOverlap에서 분기) private float _activeRadius; // Circle일 때 사용하는 반경 (스캔/Gizmo용 백업) private Vector2 _activeHitSize; // Box일 때 사용하는 크기 백업 // ─── 현재 활성 액션의 데미지/효과 데이터 (Activate에서 세팅) ───────── private int _damage; private Vector2 _hitVelocity; // 피해자에게 가할 넉백 속도 private Vector2 _hitSourcePosition; // 공격자 위치 (피격자 위치 보정 거리 계산용) private Vector2? _hitTargetPosition; // 피격자 강제 이동 목표 위치 (null이면 보정 안 함) private float _hitPositionMinDistance; // 이 거리보다 가까우면 위치 보정 적용 private bool _correctHitTargetY; // 위치 보정에서 Y도 보정할지 private int _hitPositionSolidMask; // 보정 시 끼이지 않게 검사할 솔리드 레이어 private float _hitPositionCorrectionDuration; // 보정 보간 시간 (0이면 즉시) private string _hitReactionState; // 피격자가 재생할 애니메이션 State 이름 private LayerMask _targetLayer; // 데미지를 줄 레이어 (보통 Enemy) // 한 번 hit한 IDamageable은 이 액션 활성 동안 다시 hit되지 않음. private readonly HashSet _alreadyHit = new(); private void Awake() { _circleCollider = GetComponent(); // 플레이어 몸체는 적과 물리 충돌하지 않으므로, 공격 판정은 트리거만 사용. _circleCollider.isTrigger = true; _circleCollider.enabled = false; // BoxCollider2D는 없으면 자동 추가. 기존 프리팹과의 호환성 유지. _boxCollider = GetComponent(); if (_boxCollider == null) _boxCollider = gameObject.AddComponent(); _boxCollider.isTrigger = true; _boxCollider.enabled = false; } // 액션 시작 시 호출. 도형/위치/데미지 세팅 후 해당 콜라이더만 활성화. // _alreadyHit를 클리어해서 새 공격으로 다시 hit 가능하게 함. public void Activate(ActionData data, Vector2 localPosition, Vector2 hitVelocity, Vector2 sourcePosition, Vector2? hitTargetPosition, bool correctHitTargetY, int hitPositionSolidMask, LayerMask targetLayer) { transform.localPosition = localPosition; // 도형에 맞는 콜라이더만 활성화. 다른 도형 콜라이더는 비활성으로 보장. _activeShape = data.Shape; if (data.Shape == HitShape.Box) { _boxCollider.size = data.HitSize; _boxCollider.offset = Vector2.zero; _boxCollider.enabled = true; _circleCollider.enabled = false; _activeHitSize = data.HitSize; } else { _circleCollider.radius = data.Radius; _circleCollider.enabled = true; _boxCollider.enabled = false; _activeRadius = data.Radius; } _damage = data.Damage; _hitVelocity = hitVelocity; _hitSourcePosition = sourcePosition; _hitTargetPosition = hitTargetPosition; _hitPositionMinDistance = Mathf.Abs(data.HitTargetOffset.x); _correctHitTargetY = correctHitTargetY; _hitPositionSolidMask = hitPositionSolidMask; _hitPositionCorrectionDuration = data.HitPositionCorrectionDuration; _hitReactionState = data.HitReactionAnimationState; _targetLayer = targetLayer; _alreadyHit.Clear(); // 판정이 켜진 순간 이미 범위 안에 있던 적도 같은 프레임에 잡아낸다. ScanImmediateOverlap(); } // 액션의 HitDuration이 끝나면 호출. 모든 콜라이더 비활성화 + hit 기록 초기화. public void Deactivate() { _circleCollider.enabled = false; _boxCollider.enabled = false; _alreadyHit.Clear(); } // 활성 순간 즉시 검사: 도형에 맞는 OverlapAll로 현재 겹친 콜라이더를 모두 가져와 TryDamage. // 이게 없으면 짧은 hit window (예: HitDuration=0.02)에 OnTriggerEnter가 못 따라옴. private void ScanImmediateOverlap() { Collider2D[] hits = _activeShape == HitShape.Box ? Physics2D.OverlapBoxAll(transform.position, _activeHitSize, 0f, _targetLayer) : Physics2D.OverlapCircleAll(transform.position, _activeRadius, _targetLayer); foreach (var hit in hits) TryDamage(hit); } // 트리거 이벤트로 들어온 적도 같은 함수로 처리. // Stay까지 받는 이유: 활성 중간에 새로 들어온 적도 잡으려고. private void OnTriggerEnter2D(Collider2D other) => TryDamage(other); private void OnTriggerStay2D(Collider2D other) => TryDamage(other); // 데미지 적용 흐름: // 1) 레이어 마스크로 적 여부 확인 // 2) Collider 자신 또는 부모에서 IDamageable 검색 (자식 콜라이더 대응) // 3) 이미 hit한 대상은 건너뜀 (HashSet) // 4) 데미지 + 넉백 + 위치 보정 정보를 전달하여 TakeDamage 호출 // 5) OnHit 이벤트 발화 (PlayerController가 마지막 hit한 적 기억) private void TryDamage(Collider2D other) { if ((_targetLayer.value & (1 << other.gameObject.layer)) == 0) return; // 피격 콜라이더는 자식에 있고, 데미지 처리는 루트에 있을 수 있다. if (!other.TryGetComponent(out var target)) target = other.GetComponentInParent(); if (target == null) return; if (_alreadyHit.Contains(target)) return; _alreadyHit.Add(target); Debug.Log($"[Hitbox] t={Time.time:F3} damage={_damage} → {other.name} (parent={(target as MonoBehaviour)?.gameObject.name})"); Vector2? targetPosition = GetCorrectionTargetPosition(other); target.TakeDamage(_damage, _hitVelocity, _hitReactionState, targetPosition, _correctHitTargetY, _hitPositionSolidMask, _hitPositionCorrectionDuration); OnHit?.Invoke(target); } // 위치 보정 목표 결정. // 적이 공격자에게 너무 가까우면 (X 거리 < minDistance) hitTargetPosition을 반환, // 충분히 떨어져 있으면 null 반환해서 보정 안 함. // (가까운 적만 끌어당기는 "흡착" 효과 구현) private Vector2? GetCorrectionTargetPosition(Collider2D other) { if (!_hitTargetPosition.HasValue) return null; if (_hitPositionMinDistance <= 0.001f) return null; Vector2 currentPosition = other.attachedRigidbody != null ? other.attachedRigidbody.position : (Vector2)other.transform.root.position; float currentDistance = Mathf.Abs(currentPosition.x - _hitSourcePosition.x); return currentDistance < _hitPositionMinDistance ? _hitTargetPosition : null; } }