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; 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 void Awake() { _currentHealth = _maxHealth; _rb = GetComponent(); _anim = GetComponentInChildren(); _spriteRenderer = GetComponentInChildren(); if (_spriteRenderer != null) _originalColor = _spriteRenderer.color; } private void Update() { if (_flashTimer > 0f) { _flashTimer -= Time.deltaTime; if (_flashTimer <= 0f && _spriteRenderer != null) _spriteRenderer.color = _originalColor; } if (_hitReactionTimer > 0f) _hitReactionTimer -= Time.deltaTime; } private void FixedUpdate() { if (_rb != null) _lastVelocity = _rb.linearVelocity; } public void TakeDamage(int amount, Vector2 hitVelocity = default, string hitReactionAnimationState = null) { if (_currentHealth <= 0) return; _currentHealth -= amount; Debug.Log($"{name} 피격: -{amount} (HP: {_currentHealth}/{_maxHealth})"); if (_spriteRenderer != null) { _spriteRenderer.color = _hitFlashColor; _flashTimer = _hitFlashDuration; } if (_anim != null && !string.IsNullOrEmpty(hitReactionAnimationState)) _anim.Play(hitReactionAnimationState); // HitVelocity is an immediate launch/knockback velocity, not an additive force. if (_rb != null) { Vector2 nextVelocity = GetHitReactionVelocity(hitVelocity); if (nextVelocity != Vector2.zero) { _rb.linearVelocity = nextVelocity; _hitReactionTimer = _hitReactionDuration; } } if (_currentHealth <= 0) Die(); } 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) { // Airborne follow-up hits pop the enemy with a fixed Y velocity for stable combos. if (_hitReactionTimer <= 0f || _isGrounded) return hitVelocity; Vector2 currentVelocity = _rb.linearVelocity; Vector2 nextVelocity = hitVelocity == Vector2.zero ? currentVelocity : hitVelocity; nextVelocity.y = _airborneHitYVelocity; return nextVelocity; } 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) { // While in hit reaction, side-wall impacts reflect the current knockback. 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); } }