using System.Collections.Generic; using UnityEngine; [RequireComponent(typeof(Collider2D))] [RequireComponent(typeof(Rigidbody2D))] public class Enemy : MonoBehaviour, IDamageable { [Header("Stats")] [SerializeField] private int _maxHealth = 30; [Header("Hit Feedback")] [SerializeField] private float _hitFlashDuration = 0.1f; [SerializeField] private Color _hitFlashColor = Color.red; [Header("Hit Bounce")] [SerializeField] private float _hitReactionDuration = 0.5f; [SerializeField] private float _airborneHitYVelocity = 3f; [SerializeField] private float _wallBounceVelocityMultiplier = 0.8f; [SerializeField] private float _wallBounceMinXVelocity = 1f; [SerializeField] private float _wallBounceUpwardVelocity = 1.5f; [Header("Separation")] [SerializeField] private float _separationRadius = 0.6f; [SerializeField] private float _separationStrength = 2f; [SerializeField] private LayerMask _separationLayer; private static readonly Collider2D[] _separationBuffer = new Collider2D[16]; private int _currentHealth; private Rigidbody2D _rb; private Animator _anim; private SpriteRenderer _spriteRenderer; private Color _originalColor; private float _flashTimer; private float _hitReactionTimer; private bool _isGrounded; private Vector2 _lastVelocity; private Collider2D[] _bodyColliders; private readonly List _castResults = new(); private const float HitPositionSkinWidth = 0.02f; private bool _isHitPositionCorrecting; private bool _correctHitPositionY; private float _hitPositionCorrectionTimer; private float _hitPositionCorrectionDuration; private Vector2 _hitPositionCorrectionStart; private Vector2 _hitPositionCorrectionTarget; private bool _isGrabbed; private Vector2 _grabTargetPosition; private int _grabSolidMask; private void Awake() { _currentHealth = _maxHealth; _rb = GetComponent(); _anim = GetComponentInChildren(); _spriteRenderer = GetComponentInChildren(); _bodyColliders = GetComponentsInChildren(); if (_spriteRenderer != null) _originalColor = _spriteRenderer.color; } private void Update() { if (_flashTimer > 0f) { _flashTimer -= Time.deltaTime; if (_flashTimer <= 0f && _spriteRenderer != null) { Debug.Log($"[Flash END] t={Time.time:F3} → revert to {_originalColor}"); _spriteRenderer.color = _originalColor; } } if (_hitReactionTimer > 0f) _hitReactionTimer -= Time.deltaTime; } private void FixedUpdate() { if (_rb != null) { if (_isGrabbed) { _rb.linearVelocity = Vector2.zero; _rb.MovePosition(_grabTargetPosition); _lastVelocity = Vector2.zero; return; } ApplySmoothHitPositionCorrection(); ApplySeparation(); _lastVelocity = _rb.linearVelocity; } } private void ApplySeparation() { if (_separationRadius <= 0f || _separationStrength <= 0f) return; if (_separationLayer.value == 0) return; ContactFilter2D filter = new ContactFilter2D { useLayerMask = true, layerMask = _separationLayer, useTriggers = false }; int count = Physics2D.OverlapCircle(_rb.position, _separationRadius, filter, _separationBuffer); Vector2 push = Vector2.zero; int contributors = 0; for (int i = 0; i < count; i++) { Collider2D other = _separationBuffer[i]; if (other == null) continue; if (other.attachedRigidbody == _rb) continue; Vector2 away = _rb.position - (Vector2)other.transform.position; float dist = away.magnitude; if (dist >= _separationRadius) continue; Vector2 dir; if (dist < 0.001f) { // 완전히 같은 위치인 경우 무작위 수평 방향으로 분리 dir = UnityEngine.Random.value < 0.5f ? Vector2.left : Vector2.right; } else { dir = away / dist; } float strength = 1f - (dist / _separationRadius); push += dir * strength; contributors++; } if (contributors == 0) return; push /= contributors; // X축으로만 분리. Y는 중력에 맡겨서 바운스/공중부양 방지. _rb.position += new Vector2(push.x, 0f) * (_separationStrength * Time.fixedDeltaTime); } public void TakeDamage(int amount, Vector2 hitVelocity = default, string hitReactionAnimationState = null, Vector2? hitTargetPosition = null, bool correctHitTargetY = false, int hitPositionSolidMask = 0, float hitPositionCorrectionDuration = 0f) { if (_currentHealth <= 0) return; _isGrabbed = false; _currentHealth -= amount; Debug.Log($"{name} 피격: -{amount} (HP: {_currentHealth}/{_maxHealth})"); if (_spriteRenderer != null) { Debug.Log($"[Flash START] t={Time.time:F3} duration={_hitFlashDuration:F3} (current color was {_spriteRenderer.color})"); _spriteRenderer.color = _hitFlashColor; _flashTimer = _hitFlashDuration; } if (_anim != null && !string.IsNullOrEmpty(hitReactionAnimationState)) _anim.Play(hitReactionAnimationState); // 새 피격이 반응 속도를 전부 결정하므로, 이전 튕김/넉백 속도는 먼저 제거한다. if (_rb != null) { _hitReactionTimer = 0f; _rb.linearVelocity = Vector2.zero; _lastVelocity = Vector2.zero; BeginHitTargetPositionCorrection(hitTargetPosition, correctHitTargetY, hitPositionSolidMask, hitPositionCorrectionDuration); Vector2 nextVelocity = GetHitReactionVelocity(hitVelocity); if (nextVelocity != Vector2.zero) { _rb.linearVelocity = nextVelocity; _lastVelocity = nextVelocity; _hitReactionTimer = _hitReactionDuration; } } if (_currentHealth <= 0) Die(); } public void BeginGrab(string grabbedAnimationState, int solidMask) { if (_currentHealth <= 0) return; _isGrabbed = true; _grabSolidMask = solidMask; _isHitPositionCorrecting = false; _hitReactionTimer = 0f; _lastVelocity = Vector2.zero; if (_rb != null) { _rb.linearVelocity = Vector2.zero; _grabTargetPosition = _rb.position; } if (_anim != null && !string.IsNullOrEmpty(grabbedAnimationState)) _anim.Play(grabbedAnimationState); } public void UpdateGrabPosition(Vector2 position) { if (!_isGrabbed || _rb == null) return; _rb.linearVelocity = Vector2.zero; _grabTargetPosition = GetSafeHitTargetPosition(position, _grabSolidMask); } public void EndGrab() { _isGrabbed = false; } private void OnCollisionEnter2D(Collision2D collision) { UpdateGroundedState(collision); if (_hitReactionTimer <= 0f || _rb == null) return; if (collision.collider.GetComponentInParent() != null) return; for (int i = 0; i < collision.contactCount; i++) { Vector2 normal = collision.GetContact(i).normal; if (Mathf.Abs(normal.x) < 0.5f) continue; BounceOffWall(normal); return; } } private void OnCollisionStay2D(Collision2D collision) { UpdateGroundedState(collision); } private void OnCollisionExit2D(Collision2D collision) { _isGrounded = false; } private Vector2 GetHitReactionVelocity(Vector2 hitVelocity) { // 공중 추가타는 고정된 세로 속도를 쓰되, 이전 물리 속도는 절대 이어받지 않는다. Vector2 nextVelocity = hitVelocity; if (!_isGrounded) nextVelocity.y = _airborneHitYVelocity; return nextVelocity; } private void BeginHitTargetPositionCorrection(Vector2? hitTargetPosition, bool correctHitTargetY, int solidMask, float correctionDuration) { _isHitPositionCorrecting = false; if (!hitTargetPosition.HasValue || _rb == null) return; Vector2 targetPosition = hitTargetPosition.Value; if (!correctHitTargetY) targetPosition.y = _rb.position.y; targetPosition = GetSafeHitTargetPosition(targetPosition, solidMask); if ((targetPosition - _rb.position).sqrMagnitude <= 0.0001f) return; if (correctionDuration <= 0f) { _rb.position = targetPosition; return; } _isHitPositionCorrecting = true; _correctHitPositionY = correctHitTargetY; _hitPositionCorrectionTimer = 0f; _hitPositionCorrectionDuration = correctionDuration; _hitPositionCorrectionStart = _rb.position; _hitPositionCorrectionTarget = targetPosition; } private void ApplySmoothHitPositionCorrection() { if (!_isHitPositionCorrecting || _rb == null) return; _hitPositionCorrectionTimer += Time.fixedDeltaTime; float normalizedTime = Mathf.Clamp01(_hitPositionCorrectionTimer / Mathf.Max(_hitPositionCorrectionDuration, Time.fixedDeltaTime)); float easedTime = Mathf.SmoothStep(0f, 1f, normalizedTime); Vector2 nextPosition = _rb.position; nextPosition.x = Mathf.Lerp(_hitPositionCorrectionStart.x, _hitPositionCorrectionTarget.x, easedTime); if (_correctHitPositionY) nextPosition.y = Mathf.Lerp(_hitPositionCorrectionStart.y, _hitPositionCorrectionTarget.y, easedTime); _rb.MovePosition(nextPosition); if (normalizedTime >= 1f) _isHitPositionCorrecting = false; } private Vector2 GetSafeHitTargetPosition(Vector2 targetPosition, int solidMask) { if (solidMask == 0) return targetPosition; Vector2 startPosition = _rb.position; Vector2 moveDelta = targetPosition - startPosition; float distance = moveDelta.magnitude; if (distance <= 0.001f) return targetPosition; Vector2 direction = moveDelta / distance; float closestDistance = GetClosestBodyCastDistance(direction, distance + HitPositionSkinWidth, solidMask); if (closestDistance >= distance + HitPositionSkinWidth) return targetPosition; float allowedDistance = Mathf.Max(closestDistance - HitPositionSkinWidth, 0f); return startPosition + direction * allowedDistance; } private float GetClosestBodyCastDistance(Vector2 direction, float distance, int solidMask) { if (_bodyColliders == null || _bodyColliders.Length == 0) _bodyColliders = GetComponentsInChildren(); ContactFilter2D filter = new ContactFilter2D { useLayerMask = true, layerMask = solidMask, useTriggers = false }; float closest = float.PositiveInfinity; for (int i = 0; i < _bodyColliders.Length; i++) { Collider2D bodyCollider = _bodyColliders[i]; if (bodyCollider == null || bodyCollider.isTrigger) continue; _castResults.Clear(); int hitCount = bodyCollider.Cast(direction, filter, _castResults, distance); for (int j = 0; j < hitCount; j++) { if (_castResults[j].distance < closest) closest = _castResults[j].distance; } } return closest; } private void UpdateGroundedState(Collision2D collision) { for (int i = 0; i < collision.contactCount; i++) { if (collision.GetContact(i).normal.y > 0.5f) { _isGrounded = true; return; } } } private void BounceOffWall(Vector2 wallNormal) { // 피격 반응 중 옆벽에 부딪히면 현재 넉백 속도를 반사한다. Vector2 incomingVelocity = _lastVelocity.sqrMagnitude > _rb.linearVelocity.sqrMagnitude ? _lastVelocity : _rb.linearVelocity; if (Mathf.Abs(incomingVelocity.x) < _wallBounceMinXVelocity) return; Vector2 bouncedVelocity = Vector2.Reflect(incomingVelocity, wallNormal) * _wallBounceVelocityMultiplier; if (bouncedVelocity.y < _wallBounceUpwardVelocity) bouncedVelocity.y = _wallBounceUpwardVelocity; _rb.linearVelocity = bouncedVelocity; _hitReactionTimer = _hitReactionDuration; } private void Die() { Debug.Log($"{name} 사망"); Destroy(gameObject); } }