2026-05-20 적 경직추가
This commit is contained in:
@@ -68,6 +68,7 @@ public class ActionData : ScriptableObject
|
|||||||
// ─── 피격자 반응 (피격된 적의 동작) ─────────────────────────────────
|
// ─── 피격자 반응 (피격된 적의 동작) ─────────────────────────────────
|
||||||
[Header("Hit Reaction")]
|
[Header("Hit Reaction")]
|
||||||
public Vector2 HitVelocity = Vector2.zero; // 적에게 가할 넉백 속도 (X는 공격자 facing 방향)
|
public Vector2 HitVelocity = Vector2.zero; // 적에게 가할 넉백 속도 (X는 공격자 facing 방향)
|
||||||
|
public float HitStunDuration = 0.25f; // 피격자가 이동/공격을 못 하는 경직 시간
|
||||||
public bool UseHitPositionCorrection; // 적의 위치를 강제로 보정할지 (잡기/연계 안정성)
|
public bool UseHitPositionCorrection; // 적의 위치를 강제로 보정할지 (잡기/연계 안정성)
|
||||||
public Vector2 HitTargetOffset = new Vector2(0.8f, 0f); // 보정 시 공격자 기준 적의 목표 위치
|
public Vector2 HitTargetOffset = new Vector2(0.8f, 0f); // 보정 시 공격자 기준 적의 목표 위치
|
||||||
public float HitPositionCorrectionDuration = 0.08f; // 보정 보간 시간 (0이면 즉시 텔레포트)
|
public float HitPositionCorrectionDuration = 0.08f; // 보정 보간 시간 (0이면 즉시 텔레포트)
|
||||||
|
|||||||
@@ -35,6 +35,7 @@ public class AttackHitbox : MonoBehaviour
|
|||||||
private bool _correctHitTargetY; // 위치 보정에서 Y도 보정할지
|
private bool _correctHitTargetY; // 위치 보정에서 Y도 보정할지
|
||||||
private int _hitPositionSolidMask; // 보정 시 끼이지 않게 검사할 솔리드 레이어
|
private int _hitPositionSolidMask; // 보정 시 끼이지 않게 검사할 솔리드 레이어
|
||||||
private float _hitPositionCorrectionDuration; // 보정 보간 시간 (0이면 즉시)
|
private float _hitPositionCorrectionDuration; // 보정 보간 시간 (0이면 즉시)
|
||||||
|
private float _hitStunDuration; // 피격 경직 시간
|
||||||
private string _hitReactionState; // 피격자가 재생할 애니메이션 State 이름
|
private string _hitReactionState; // 피격자가 재생할 애니메이션 State 이름
|
||||||
private LayerMask _targetLayer; // 데미지를 줄 레이어 (보통 Enemy)
|
private LayerMask _targetLayer; // 데미지를 줄 레이어 (보통 Enemy)
|
||||||
|
|
||||||
@@ -88,6 +89,7 @@ public void Activate(ActionData data, Vector2 localPosition, Vector2 hitVelocity
|
|||||||
_correctHitTargetY = correctHitTargetY;
|
_correctHitTargetY = correctHitTargetY;
|
||||||
_hitPositionSolidMask = hitPositionSolidMask;
|
_hitPositionSolidMask = hitPositionSolidMask;
|
||||||
_hitPositionCorrectionDuration = data.HitPositionCorrectionDuration;
|
_hitPositionCorrectionDuration = data.HitPositionCorrectionDuration;
|
||||||
|
_hitStunDuration = data.HitStunDuration;
|
||||||
_hitReactionState = data.HitReactionAnimationState;
|
_hitReactionState = data.HitReactionAnimationState;
|
||||||
_targetLayer = targetLayer;
|
_targetLayer = targetLayer;
|
||||||
_alreadyHit.Clear();
|
_alreadyHit.Clear();
|
||||||
@@ -140,7 +142,7 @@ private void TryDamage(Collider2D other)
|
|||||||
_alreadyHit.Add(target);
|
_alreadyHit.Add(target);
|
||||||
Debug.Log($"[Hitbox] t={Time.time:F3} damage={_damage} → {other.name} (parent={(target as MonoBehaviour)?.gameObject.name})");
|
Debug.Log($"[Hitbox] t={Time.time:F3} damage={_damage} → {other.name} (parent={(target as MonoBehaviour)?.gameObject.name})");
|
||||||
Vector2? targetPosition = GetCorrectionTargetPosition(other);
|
Vector2? targetPosition = GetCorrectionTargetPosition(other);
|
||||||
target.TakeDamage(_damage, _hitVelocity, _hitReactionState, targetPosition, _correctHitTargetY, _hitPositionSolidMask, _hitPositionCorrectionDuration);
|
target.TakeDamage(_damage, _hitVelocity, _hitReactionState, targetPosition, _correctHitTargetY, _hitPositionSolidMask, _hitPositionCorrectionDuration, _hitStunDuration);
|
||||||
OnHit?.Invoke(target);
|
OnHit?.Invoke(target);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -15,8 +15,9 @@
|
|||||||
// correctHitTargetY — 위 위치 보정에서 Y도 보정할지 (false면 X만)
|
// correctHitTargetY — 위 위치 보정에서 Y도 보정할지 (false면 X만)
|
||||||
// hitPositionSolidMask — 위치 보정 시 끼이지 않게 검사할 솔리드 레이어
|
// hitPositionSolidMask — 위치 보정 시 끼이지 않게 검사할 솔리드 레이어
|
||||||
// hitPositionCorrectionDuration — 위치 보정 보간 시간 (0이면 즉시 스냅)
|
// hitPositionCorrectionDuration — 위치 보정 보간 시간 (0이면 즉시 스냅)
|
||||||
|
// hitStunDuration — 피격 경직 시간. 음수면 피격자 기본값 사용
|
||||||
// ============================================================================
|
// ============================================================================
|
||||||
public interface IDamageable
|
public interface IDamageable
|
||||||
{
|
{
|
||||||
void TakeDamage(int amount, Vector2 hitVelocity = default, string hitReactionAnimationState = null, Vector2? hitTargetPosition = null, bool correctHitTargetY = false, int hitPositionSolidMask = 0, float hitPositionCorrectionDuration = 0f);
|
void TakeDamage(int amount, Vector2 hitVelocity = default, string hitReactionAnimationState = null, Vector2? hitTargetPosition = null, bool correctHitTargetY = false, int hitPositionSolidMask = 0, float hitPositionCorrectionDuration = 0f, float hitStunDuration = -1f);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
using System;
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using UnityEngine;
|
using UnityEngine;
|
||||||
|
|
||||||
@@ -25,6 +26,8 @@ public class Enemy : MonoBehaviour, IDamageable
|
|||||||
[Header("Hit Feedback")]
|
[Header("Hit Feedback")]
|
||||||
[SerializeField] private float _hitFlashDuration = 0.1f; // 빨강 깜빡 지속 시간
|
[SerializeField] private float _hitFlashDuration = 0.1f; // 빨강 깜빡 지속 시간
|
||||||
[SerializeField] private Color _hitFlashColor = Color.red; // 깜빡 색상
|
[SerializeField] private Color _hitFlashColor = Color.red; // 깜빡 색상
|
||||||
|
[SerializeField] private float _hitStunDuration = 0.25f; // ActionData 값이 없을 때 쓰는 기본 경직 시간
|
||||||
|
[SerializeField] private string _hitAnimationState = "HitDamage"; // 공격 데이터에 피격 애니가 없을 때 재생할 기본 State
|
||||||
|
|
||||||
// ─── 넉백 / 벽 반사 파라미터 ─────────────────────────────────────────
|
// ─── 넉백 / 벽 반사 파라미터 ─────────────────────────────────────────
|
||||||
[Header("Hit Bounce")]
|
[Header("Hit Bounce")]
|
||||||
@@ -53,6 +56,8 @@ public class Enemy : MonoBehaviour, IDamageable
|
|||||||
private SpriteRenderer _spriteRenderer;
|
private SpriteRenderer _spriteRenderer;
|
||||||
private Color _originalColor; // hit flash 끝나면 복귀할 원본 색
|
private Color _originalColor; // hit flash 끝나면 복귀할 원본 색
|
||||||
private float _flashTimer; // 깜빡 남은 시간
|
private float _flashTimer; // 깜빡 남은 시간
|
||||||
|
private bool _isFlashing;
|
||||||
|
private float _hitStunTimer; // 피격 경직 남은 시간
|
||||||
private float _hitReactionTimer; // 넉백 유효 시간 카운트다운 (벽 반사 조건)
|
private float _hitReactionTimer; // 넉백 유효 시간 카운트다운 (벽 반사 조건)
|
||||||
private bool _isGrounded;
|
private bool _isGrounded;
|
||||||
private Vector2 _lastVelocity; // 직전 프레임 속도 (벽 충돌 시 반사 벡터 계산용)
|
private Vector2 _lastVelocity; // 직전 프레임 속도 (벽 충돌 시 반사 벡터 계산용)
|
||||||
@@ -76,9 +81,11 @@ public class Enemy : MonoBehaviour, IDamageable
|
|||||||
|
|
||||||
public bool IsDead => _health != null && _health.IsDead;
|
public bool IsDead => _health != null && _health.IsDead;
|
||||||
public bool IsGrabbed => _isGrabbed;
|
public bool IsGrabbed => _isGrabbed;
|
||||||
public bool IsInHitReaction => _hitReactionTimer > 0f || _isHitPositionCorrecting || _flashTimer > 0f;
|
public bool IsInHitReaction => _hitStunTimer > 0f || _hitReactionTimer > 0f || _isHitPositionCorrecting || _flashTimer > 0f;
|
||||||
public bool CanUseAI => !IsDead && !_isGrabbed && !IsInHitReaction;
|
public bool CanUseAI => !IsDead && !_isGrabbed && !IsInHitReaction;
|
||||||
|
|
||||||
|
public event Action OnDamaged;
|
||||||
|
|
||||||
|
|
||||||
// 컴포넌트 캐싱 + Health.OnDied 구독 (사망 시 HandleDeath 자동 호출).
|
// 컴포넌트 캐싱 + Health.OnDied 구독 (사망 시 HandleDeath 자동 호출).
|
||||||
private void Awake()
|
private void Awake()
|
||||||
@@ -106,15 +113,75 @@ private void Update()
|
|||||||
if (_flashTimer > 0f)
|
if (_flashTimer > 0f)
|
||||||
{
|
{
|
||||||
_flashTimer -= Time.deltaTime;
|
_flashTimer -= Time.deltaTime;
|
||||||
if (_flashTimer <= 0f && _spriteRenderer != null)
|
if (_flashTimer <= 0f)
|
||||||
{
|
EndHitFlash();
|
||||||
Debug.Log($"[Flash END] t={Time.time:F3} → revert to {_originalColor}");
|
|
||||||
_spriteRenderer.color = _originalColor;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (_hitReactionTimer > 0f)
|
if (_hitReactionTimer > 0f)
|
||||||
_hitReactionTimer -= Time.deltaTime;
|
_hitReactionTimer -= Time.deltaTime;
|
||||||
|
|
||||||
|
if (_hitStunTimer > 0f)
|
||||||
|
_hitStunTimer -= Time.deltaTime;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void LateUpdate()
|
||||||
|
{
|
||||||
|
if (_isFlashing)
|
||||||
|
ApplyHitFlashColor();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void StartHitFlash()
|
||||||
|
{
|
||||||
|
if (_spriteRenderer == null || _hitFlashDuration <= 0f) return;
|
||||||
|
|
||||||
|
_flashTimer = Mathf.Max(_flashTimer, _hitFlashDuration);
|
||||||
|
_isFlashing = true;
|
||||||
|
ApplyHitFlashColor();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void ApplyHitFlashColor()
|
||||||
|
{
|
||||||
|
if (_spriteRenderer != null)
|
||||||
|
_spriteRenderer.color = _hitFlashColor;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void EndHitFlash()
|
||||||
|
{
|
||||||
|
_flashTimer = 0f;
|
||||||
|
_isFlashing = false;
|
||||||
|
|
||||||
|
if (_spriteRenderer != null)
|
||||||
|
_spriteRenderer.color = _originalColor;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void PlayHitAnimation(string hitReactionAnimationState)
|
||||||
|
{
|
||||||
|
string stateName = !string.IsNullOrEmpty(hitReactionAnimationState)
|
||||||
|
? hitReactionAnimationState
|
||||||
|
: _hitAnimationState;
|
||||||
|
|
||||||
|
if (_anim == null || string.IsNullOrEmpty(stateName)) return;
|
||||||
|
|
||||||
|
int stateHash = Animator.StringToHash(stateName);
|
||||||
|
if (_anim.HasState(0, stateHash))
|
||||||
|
{
|
||||||
|
_anim.Play(stateHash, 0, 0f);
|
||||||
|
_anim.Update(0f);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!stateName.Contains("."))
|
||||||
|
{
|
||||||
|
int baseLayerStateHash = Animator.StringToHash($"Base Layer.{stateName}");
|
||||||
|
if (_anim.HasState(0, baseLayerStateHash))
|
||||||
|
{
|
||||||
|
_anim.Play(baseLayerStateHash, 0, 0f);
|
||||||
|
_anim.Update(0f);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Debug.LogWarning($"{name} 피격 애니메이션 State를 찾을 수 없습니다: {stateName}", this);
|
||||||
}
|
}
|
||||||
|
|
||||||
// 매 물리 프레임의 메인:
|
// 매 물리 프레임의 메인:
|
||||||
@@ -204,21 +271,17 @@ private void ApplySeparation()
|
|||||||
// 5) 이전 넉백 속도 초기화 후 새 넉백 적용
|
// 5) 이전 넉백 속도 초기화 후 새 넉백 적용
|
||||||
// 6) 위치 보정 (옵션)
|
// 6) 위치 보정 (옵션)
|
||||||
// 7) Health.TakeDamage로 HP 감소 → 0이면 OnDied 이벤트로 HandleDeath 트리거
|
// 7) Health.TakeDamage로 HP 감소 → 0이면 OnDied 이벤트로 HandleDeath 트리거
|
||||||
public void TakeDamage(int amount, Vector2 hitVelocity = default, string hitReactionAnimationState = null, Vector2? hitTargetPosition = null, bool correctHitTargetY = false, int hitPositionSolidMask = 0, float hitPositionCorrectionDuration = 0f)
|
public void TakeDamage(int amount, Vector2 hitVelocity = default, string hitReactionAnimationState = null, Vector2? hitTargetPosition = null, bool correctHitTargetY = false, int hitPositionSolidMask = 0, float hitPositionCorrectionDuration = 0f, float hitStunDuration = -1f)
|
||||||
{
|
{
|
||||||
if (_health == null || _health.IsDead) return;
|
if (_health == null || _health.IsDead) return;
|
||||||
|
|
||||||
|
float appliedHitStunDuration = hitStunDuration >= 0f ? hitStunDuration : _hitStunDuration;
|
||||||
|
_hitStunTimer = Mathf.Max(_hitStunTimer, appliedHitStunDuration);
|
||||||
|
OnDamaged?.Invoke();
|
||||||
_isGrabbed = false;
|
_isGrabbed = false;
|
||||||
|
|
||||||
if (_spriteRenderer != null)
|
StartHitFlash();
|
||||||
{
|
PlayHitAnimation(hitReactionAnimationState);
|
||||||
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)
|
if (_rb != null)
|
||||||
|
|||||||
@@ -34,12 +34,11 @@ private enum AIState
|
|||||||
[SerializeField] private float _attackLockDuration = 0.45f;
|
[SerializeField] private float _attackLockDuration = 0.45f;
|
||||||
[SerializeField] private float _attackCooldown = 1f;
|
[SerializeField] private float _attackCooldown = 1f;
|
||||||
[SerializeField] private Vector2 _attackHitVelocity = new Vector2(3f, 0f);
|
[SerializeField] private Vector2 _attackHitVelocity = new Vector2(3f, 0f);
|
||||||
[SerializeField] private string _targetHitReactionAnimationState;
|
|
||||||
|
|
||||||
[Header("Animation")]
|
[Header("Animation")]
|
||||||
[SerializeField] private string _idleAnimationState = "";
|
[SerializeField] private string _idleAnimationState = "Idle";
|
||||||
[SerializeField] private string _runAnimationState = "";
|
[SerializeField] private string _runAnimationState = "Run";
|
||||||
[SerializeField] private string _attackAnimationState = "";
|
[SerializeField] private string _attackAnimationState = "PunchA";
|
||||||
|
|
||||||
private Enemy _enemy;
|
private Enemy _enemy;
|
||||||
private Health _health;
|
private Health _health;
|
||||||
@@ -75,9 +74,17 @@ private void OnEnable()
|
|||||||
_hasAppliedAttackDamage = false;
|
_hasAppliedAttackDamage = false;
|
||||||
_attackTimer = 0f;
|
_attackTimer = 0f;
|
||||||
_attackCooldownTimer = 0f;
|
_attackCooldownTimer = 0f;
|
||||||
|
if (_enemy != null)
|
||||||
|
_enemy.OnDamaged += HandleDamaged;
|
||||||
ResolveTarget();
|
ResolveTarget();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private void OnDisable()
|
||||||
|
{
|
||||||
|
if (_enemy != null)
|
||||||
|
_enemy.OnDamaged -= HandleDamaged;
|
||||||
|
}
|
||||||
|
|
||||||
private void Update()
|
private void Update()
|
||||||
{
|
{
|
||||||
TickCooldown();
|
TickCooldown();
|
||||||
@@ -282,6 +289,14 @@ private void CancelAttack()
|
|||||||
_isAttacking = false;
|
_isAttacking = false;
|
||||||
_hasAppliedAttackDamage = false;
|
_hasAppliedAttackDamage = false;
|
||||||
_attackTimer = 0f;
|
_attackTimer = 0f;
|
||||||
|
_state = AIState.Idle;
|
||||||
|
_activeAnimationState = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void HandleDamaged()
|
||||||
|
{
|
||||||
|
CancelAttack();
|
||||||
|
_attackCooldownTimer = Mathf.Max(_attackCooldownTimer, _attackCooldown);
|
||||||
}
|
}
|
||||||
|
|
||||||
private void TryApplyAttackDamage()
|
private void TryApplyAttackDamage()
|
||||||
@@ -295,7 +310,7 @@ private void TryApplyAttackDamage()
|
|||||||
if (!IsTargetLayerValid()) return;
|
if (!IsTargetLayerValid()) return;
|
||||||
|
|
||||||
Vector2 hitVelocity = new Vector2(_attackHitVelocity.x * _facingDirection, _attackHitVelocity.y);
|
Vector2 hitVelocity = new Vector2(_attackHitVelocity.x * _facingDirection, _attackHitVelocity.y);
|
||||||
_targetDamageable.TakeDamage(_attackDamage, hitVelocity, _targetHitReactionAnimationState);
|
_targetDamageable.TakeDamage(_attackDamage, hitVelocity);
|
||||||
}
|
}
|
||||||
|
|
||||||
private bool IsTargetLayerValid()
|
private bool IsTargetLayerValid()
|
||||||
|
|||||||
@@ -733,7 +733,7 @@ private async Awaitable GrabRoutine(ActionData data, CancellationToken token)
|
|||||||
}
|
}
|
||||||
|
|
||||||
target.EndGrab();
|
target.EndGrab();
|
||||||
target.TakeDamage(data.Damage, GetHitVelocity(data.HitVelocity), data.HitReactionAnimationState);
|
target.TakeDamage(data.Damage, GetHitVelocity(data.HitVelocity), data.HitReactionAnimationState, hitStunDuration: data.HitStunDuration);
|
||||||
}
|
}
|
||||||
catch (System.OperationCanceledException)
|
catch (System.OperationCanceledException)
|
||||||
{
|
{
|
||||||
@@ -1578,7 +1578,7 @@ private void DrawTimelineBar(ActionData data, float elapsed)
|
|||||||
GUI.color = Color.white;
|
GUI.color = Color.white;
|
||||||
}
|
}
|
||||||
|
|
||||||
public void TakeDamage(int amount, Vector2 hitVelocity = default, string hitReactionAnimationState = null, Vector2? hitTargetPosition = null, bool correctHitTargetY = false, int hitPositionSolidMask = 0, float hitPositionCorrectionDuration = 0)
|
public void TakeDamage(int amount, Vector2 hitVelocity = default, string hitReactionAnimationState = null, Vector2? hitTargetPosition = null, bool correctHitTargetY = false, int hitPositionSolidMask = 0, float hitPositionCorrectionDuration = 0, float hitStunDuration = -1f)
|
||||||
{
|
{
|
||||||
if (_health == null || _health.IsDead) return;
|
if (_health == null || _health.IsDead) return;
|
||||||
|
|
||||||
|
|||||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Reference in New Issue
Block a user