Files
WhiteMan_Unity2D/Assets/02_Scripts/Enemy/Enemy.cs

276 lines
9.5 KiB
C#

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;
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<RaycastHit2D> _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 void Awake()
{
_currentHealth = _maxHealth;
_rb = GetComponent<Rigidbody2D>();
_anim = GetComponentInChildren<Animator>();
_spriteRenderer = GetComponentInChildren<SpriteRenderer>();
_bodyColliders = GetComponentsInChildren<Collider2D>();
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)
{
ApplySmoothHitPositionCorrection();
_lastVelocity = _rb.linearVelocity;
}
}
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;
_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);
// 새 피격이 반응 속도를 전부 결정하므로, 이전 튕김/넉백 속도는 먼저 제거한다.
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();
}
private void OnCollisionEnter2D(Collision2D collision)
{
UpdateGroundedState(collision);
if (_hitReactionTimer <= 0f || _rb == null) return;
if (collision.collider.GetComponentInParent<PlayerController>() != 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<Collider2D>();
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);
}
}