From 08cdab421d7f9bbb2525889892ad7186c31378e3 Mon Sep 17 00:00:00 2001 From: "DESKTOP-VVOCIJO\\PC" Date: Wed, 20 May 2026 14:56:11 +0900 Subject: [PATCH] =?UTF-8?q?2026-05-20=20=EC=A0=81=20=EA=B2=BD=EC=A7=81?= =?UTF-8?q?=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- Assets/02_Scripts/Combat/ActionData.cs | 1 + Assets/02_Scripts/Combat/AttackHitbox.cs | 4 +- Assets/02_Scripts/Combat/IDamageable.cs | 3 +- Assets/02_Scripts/Enemy/Enemy.cs | 95 +++++++++++++++---- Assets/02_Scripts/Enemy/EnemyAI.cs | 25 ++++- Assets/02_Scripts/Player/PlayerController.cs | 4 +- .../ColorMan/Prefabs/BlackMan.prefab | 4 +- .../WhiteMan/Animations/HitDamage.anim | 2 +- .../WhiteMan/Animations/HitDamageUp.anim | 2 +- Assets/05_Data/Attack/GunFire.asset | 4 +- 10 files changed, 113 insertions(+), 31 deletions(-) diff --git a/Assets/02_Scripts/Combat/ActionData.cs b/Assets/02_Scripts/Combat/ActionData.cs index b54066b..b382dc2 100644 --- a/Assets/02_Scripts/Combat/ActionData.cs +++ b/Assets/02_Scripts/Combat/ActionData.cs @@ -68,6 +68,7 @@ public class ActionData : ScriptableObject // ─── 피격자 반응 (피격된 적의 동작) ───────────────────────────────── [Header("Hit Reaction")] public Vector2 HitVelocity = Vector2.zero; // 적에게 가할 넉백 속도 (X는 공격자 facing 방향) + public float HitStunDuration = 0.25f; // 피격자가 이동/공격을 못 하는 경직 시간 public bool UseHitPositionCorrection; // 적의 위치를 강제로 보정할지 (잡기/연계 안정성) public Vector2 HitTargetOffset = new Vector2(0.8f, 0f); // 보정 시 공격자 기준 적의 목표 위치 public float HitPositionCorrectionDuration = 0.08f; // 보정 보간 시간 (0이면 즉시 텔레포트) diff --git a/Assets/02_Scripts/Combat/AttackHitbox.cs b/Assets/02_Scripts/Combat/AttackHitbox.cs index 4f6a168..7dfe428 100644 --- a/Assets/02_Scripts/Combat/AttackHitbox.cs +++ b/Assets/02_Scripts/Combat/AttackHitbox.cs @@ -35,6 +35,7 @@ public class AttackHitbox : MonoBehaviour private bool _correctHitTargetY; // 위치 보정에서 Y도 보정할지 private int _hitPositionSolidMask; // 보정 시 끼이지 않게 검사할 솔리드 레이어 private float _hitPositionCorrectionDuration; // 보정 보간 시간 (0이면 즉시) + private float _hitStunDuration; // 피격 경직 시간 private string _hitReactionState; // 피격자가 재생할 애니메이션 State 이름 private LayerMask _targetLayer; // 데미지를 줄 레이어 (보통 Enemy) @@ -88,6 +89,7 @@ public void Activate(ActionData data, Vector2 localPosition, Vector2 hitVelocity _correctHitTargetY = correctHitTargetY; _hitPositionSolidMask = hitPositionSolidMask; _hitPositionCorrectionDuration = data.HitPositionCorrectionDuration; + _hitStunDuration = data.HitStunDuration; _hitReactionState = data.HitReactionAnimationState; _targetLayer = targetLayer; _alreadyHit.Clear(); @@ -140,7 +142,7 @@ private void TryDamage(Collider2D other) _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); + target.TakeDamage(_damage, _hitVelocity, _hitReactionState, targetPosition, _correctHitTargetY, _hitPositionSolidMask, _hitPositionCorrectionDuration, _hitStunDuration); OnHit?.Invoke(target); } diff --git a/Assets/02_Scripts/Combat/IDamageable.cs b/Assets/02_Scripts/Combat/IDamageable.cs index 8f77d14..496fb62 100644 --- a/Assets/02_Scripts/Combat/IDamageable.cs +++ b/Assets/02_Scripts/Combat/IDamageable.cs @@ -15,8 +15,9 @@ // correctHitTargetY — 위 위치 보정에서 Y도 보정할지 (false면 X만) // hitPositionSolidMask — 위치 보정 시 끼이지 않게 검사할 솔리드 레이어 // hitPositionCorrectionDuration — 위치 보정 보간 시간 (0이면 즉시 스냅) +// hitStunDuration — 피격 경직 시간. 음수면 피격자 기본값 사용 // ============================================================================ 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); } diff --git a/Assets/02_Scripts/Enemy/Enemy.cs b/Assets/02_Scripts/Enemy/Enemy.cs index a7d75e8..aee92e7 100644 --- a/Assets/02_Scripts/Enemy/Enemy.cs +++ b/Assets/02_Scripts/Enemy/Enemy.cs @@ -1,3 +1,4 @@ +using System; using System.Collections.Generic; using UnityEngine; @@ -25,6 +26,8 @@ public class Enemy : MonoBehaviour, IDamageable [Header("Hit Feedback")] [SerializeField] private float _hitFlashDuration = 0.1f; // 빨강 깜빡 지속 시간 [SerializeField] private Color _hitFlashColor = Color.red; // 깜빡 색상 + [SerializeField] private float _hitStunDuration = 0.25f; // ActionData 값이 없을 때 쓰는 기본 경직 시간 + [SerializeField] private string _hitAnimationState = "HitDamage"; // 공격 데이터에 피격 애니가 없을 때 재생할 기본 State // ─── 넉백 / 벽 반사 파라미터 ───────────────────────────────────────── [Header("Hit Bounce")] @@ -53,6 +56,8 @@ public class Enemy : MonoBehaviour, IDamageable private SpriteRenderer _spriteRenderer; private Color _originalColor; // hit flash 끝나면 복귀할 원본 색 private float _flashTimer; // 깜빡 남은 시간 + private bool _isFlashing; + private float _hitStunTimer; // 피격 경직 남은 시간 private float _hitReactionTimer; // 넉백 유효 시간 카운트다운 (벽 반사 조건) private bool _isGrounded; private Vector2 _lastVelocity; // 직전 프레임 속도 (벽 충돌 시 반사 벡터 계산용) @@ -76,9 +81,11 @@ public class Enemy : MonoBehaviour, IDamageable public bool IsDead => _health != null && _health.IsDead; 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 event Action OnDamaged; + // 컴포넌트 캐싱 + Health.OnDied 구독 (사망 시 HandleDeath 자동 호출). private void Awake() @@ -106,15 +113,75 @@ 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 (_flashTimer <= 0f) + EndHitFlash(); } if (_hitReactionTimer > 0f) _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) 이전 넉백 속도 초기화 후 새 넉백 적용 // 6) 위치 보정 (옵션) // 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; + float appliedHitStunDuration = hitStunDuration >= 0f ? hitStunDuration : _hitStunDuration; + _hitStunTimer = Mathf.Max(_hitStunTimer, appliedHitStunDuration); + OnDamaged?.Invoke(); _isGrabbed = false; - 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); + StartHitFlash(); + PlayHitAnimation(hitReactionAnimationState); // 새 피격이 반응 속도를 전부 결정하므로, 이전 튕김/넉백 속도는 먼저 제거한다. if (_rb != null) diff --git a/Assets/02_Scripts/Enemy/EnemyAI.cs b/Assets/02_Scripts/Enemy/EnemyAI.cs index 8573328..b5a66e2 100644 --- a/Assets/02_Scripts/Enemy/EnemyAI.cs +++ b/Assets/02_Scripts/Enemy/EnemyAI.cs @@ -34,12 +34,11 @@ private enum AIState [SerializeField] private float _attackLockDuration = 0.45f; [SerializeField] private float _attackCooldown = 1f; [SerializeField] private Vector2 _attackHitVelocity = new Vector2(3f, 0f); - [SerializeField] private string _targetHitReactionAnimationState; [Header("Animation")] - [SerializeField] private string _idleAnimationState = ""; - [SerializeField] private string _runAnimationState = ""; - [SerializeField] private string _attackAnimationState = ""; + [SerializeField] private string _idleAnimationState = "Idle"; + [SerializeField] private string _runAnimationState = "Run"; + [SerializeField] private string _attackAnimationState = "PunchA"; private Enemy _enemy; private Health _health; @@ -75,9 +74,17 @@ private void OnEnable() _hasAppliedAttackDamage = false; _attackTimer = 0f; _attackCooldownTimer = 0f; + if (_enemy != null) + _enemy.OnDamaged += HandleDamaged; ResolveTarget(); } + private void OnDisable() + { + if (_enemy != null) + _enemy.OnDamaged -= HandleDamaged; + } + private void Update() { TickCooldown(); @@ -282,6 +289,14 @@ private void CancelAttack() _isAttacking = false; _hasAppliedAttackDamage = false; _attackTimer = 0f; + _state = AIState.Idle; + _activeAnimationState = null; + } + + private void HandleDamaged() + { + CancelAttack(); + _attackCooldownTimer = Mathf.Max(_attackCooldownTimer, _attackCooldown); } private void TryApplyAttackDamage() @@ -295,7 +310,7 @@ private void TryApplyAttackDamage() if (!IsTargetLayerValid()) return; Vector2 hitVelocity = new Vector2(_attackHitVelocity.x * _facingDirection, _attackHitVelocity.y); - _targetDamageable.TakeDamage(_attackDamage, hitVelocity, _targetHitReactionAnimationState); + _targetDamageable.TakeDamage(_attackDamage, hitVelocity); } private bool IsTargetLayerValid() diff --git a/Assets/02_Scripts/Player/PlayerController.cs b/Assets/02_Scripts/Player/PlayerController.cs index 29b0f1a..7f63518 100644 --- a/Assets/02_Scripts/Player/PlayerController.cs +++ b/Assets/02_Scripts/Player/PlayerController.cs @@ -733,7 +733,7 @@ private async Awaitable GrabRoutine(ActionData data, CancellationToken token) } 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) { @@ -1578,7 +1578,7 @@ private void DrawTimelineBar(ActionData data, float elapsed) 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; diff --git a/Assets/03_Character/ColorMan/Prefabs/BlackMan.prefab b/Assets/03_Character/ColorMan/Prefabs/BlackMan.prefab index 015bcbc..226c79f 100644 --- a/Assets/03_Character/ColorMan/Prefabs/BlackMan.prefab +++ b/Assets/03_Character/ColorMan/Prefabs/BlackMan.prefab @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:e0bf271281d824632294a7864608c63657d23755d2db63d80d575cfd277196cd -size 13483 +oid sha256:4888d661c8f09d7104febf7ab540483d237c3e0a406859f2682a68d33de3bf39 +size 14279 diff --git a/Assets/03_Character/WhiteMan/Animations/HitDamage.anim b/Assets/03_Character/WhiteMan/Animations/HitDamage.anim index 3be3229..1b02ba2 100644 --- a/Assets/03_Character/WhiteMan/Animations/HitDamage.anim +++ b/Assets/03_Character/WhiteMan/Animations/HitDamage.anim @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:b5af9d89474f74921b938356a49ce541bb02421b2c552afcda6ee5ec70e52011 +oid sha256:4f36b88a095bbaaf4c275ddcc084d2268d6d4e5b179cd3e3b961993327269295 size 2149 diff --git a/Assets/03_Character/WhiteMan/Animations/HitDamageUp.anim b/Assets/03_Character/WhiteMan/Animations/HitDamageUp.anim index ecc4cc2..88455df 100644 --- a/Assets/03_Character/WhiteMan/Animations/HitDamageUp.anim +++ b/Assets/03_Character/WhiteMan/Animations/HitDamageUp.anim @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:558790ee603969f14df45f2dd35b242afcfbebb2f30eaa4069ef92388c08ec56 +oid sha256:7a5398b2537d7aca368b3022c296413583e8e5d789fa3d503a534f7d983a401a size 2151 diff --git a/Assets/05_Data/Attack/GunFire.asset b/Assets/05_Data/Attack/GunFire.asset index 93a1b0f..2cfa635 100644 --- a/Assets/05_Data/Attack/GunFire.asset +++ b/Assets/05_Data/Attack/GunFire.asset @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:c3b24f0e87eff47b9dfc2d89faaaf70e8490300b7b3034feb3e8ba08e14c4dc9 -size 3198 +oid sha256:4acabf013c85f81776bbb4509457fb8d765e5b19bb11c26c687279c5c202336b +size 3221