diff --git a/Assets/4.PlayerAnimation/OutAnime/AttackAnime.fbx.meta b/Assets/4.PlayerAnimation/OutAnime/AttackAnime.fbx.meta index 22d47e1f..cc81eccc 100644 --- a/Assets/4.PlayerAnimation/OutAnime/AttackAnime.fbx.meta +++ b/Assets/4.PlayerAnimation/OutAnime/AttackAnime.fbx.meta @@ -64,15 +64,15 @@ ModelImporter: floatParameter: 0 intParameter: 0 messageOptions: 0 - - time: 0.6131805 - functionName: OnAttackShake + - time: 0.8470821 + functionName: StopWeaponCollision data: objectReferenceParameter: {instanceID: 0} floatParameter: 0 intParameter: 0 messageOptions: 0 - - time: 0.8470821 - functionName: StopWeaponCollision + - time: 0.8582375 + functionName: OnAttackEnd data: objectReferenceParameter: {instanceID: 0} floatParameter: 0 @@ -121,13 +121,6 @@ ModelImporter: floatParameter: 0 intParameter: 0 messageOptions: 0 - - time: 0.4597701 - functionName: OnAttackShake - data: - objectReferenceParameter: {instanceID: 0} - floatParameter: 0 - intParameter: 0 - messageOptions: 0 - time: 0.8300493 functionName: StartWeaponCollision data: @@ -142,6 +135,13 @@ ModelImporter: floatParameter: 0 intParameter: 0 messageOptions: 0 + - time: 0.85823756 + functionName: OnAttackEnd + data: + objectReferenceParameter: {instanceID: 0} + floatParameter: 0 + intParameter: 0 + messageOptions: 0 transformMask: [] maskType: 3 maskSource: {instanceID: 0} @@ -192,6 +192,13 @@ ModelImporter: floatParameter: 0 intParameter: 0 messageOptions: 0 + - time: 0.5632184 + functionName: OnAttackEnd + data: + objectReferenceParameter: {instanceID: 0} + floatParameter: 0 + intParameter: 0 + messageOptions: 0 transformMask: [] maskType: 3 maskSource: {instanceID: 0} diff --git a/Assets/5.TestScript/Attack.cs b/Assets/5.TestScript/Attack.cs index 46d10118..738d6fc1 100644 --- a/Assets/5.TestScript/Attack.cs +++ b/Assets/5.TestScript/Attack.cs @@ -1,4 +1,5 @@ using UnityEngine; +using System.Collections; public class PlayerAttack : MonoBehaviour { @@ -10,96 +11,109 @@ public class PlayerAttack : MonoBehaviour [SerializeField] private WeaponHitBox weaponHitBox; [Header("--- 설정 ---")] - [SerializeField] private float attackCooldown = 0.5f; + [SerializeField] private float attackCooldown = 0.4f; [SerializeField] private float fullChargeTime = 2f; + [SerializeField] private float postComboDelay = 1.2f; // 막타 후 딜레이 (n초) private float _lastAttackTime, _chargeTimer; - private bool _isCharging; + private bool _isCharging, _canAttack = true, _isAttacking = false; + private int _comboCount = 0; // 콤보 카운트 public float ChargeProgress => Mathf.Clamp01(_chargeTimer / fullChargeTime); public bool IsCharging => _isCharging; + // ⭐ 이동 스크립트에서 공격 중에 멈추게 할 때 사용하세요! + public bool IsAttacking => _isAttacking; + private void Update() { if (_isCharging) { _chargeTimer += Time.deltaTime; - if (Input.GetMouseButtonDown(1)) { CancelCharging(); Debug.Log("차징 취소됨"); } + if (Input.GetMouseButtonDown(1)) { CancelCharging(); } } } public void PerformNormalAttack() { - if (playerHealth != null && playerHealth.isHit) return; + if (!_canAttack || (playerHealth != null && playerHealth.isHit)) return; if (interaction.CurrentWeapon == null || pAnim == null) return; if (Time.time < _lastAttackTime + attackCooldown) return; + _isAttacking = true; // 🚫 공격 중 이동 제한 시작 + _comboCount = (_comboCount % 3) + 1; // 1 -> 2 -> 3타 순환 + pAnim.TriggerAttack(); _lastAttackTime = Time.time; - // ❌ 여기서 ShakeAttack()을 호출하지 않습니다. (애니메이션 이벤트로 이동) } - // ⭐ [추가] 애니메이션 이벤트에서 실제 타격 시점에 호출할 함수 public void OnAttackShake() { - Debug.Log("[Event] 타격 타이밍! 카메라 흔들기 명령 전송"); - if (CinemachineShake.Instance != null) + if (CinemachineShake.Instance == null) return; + + if (_comboCount == 3) // 🔥 인왕 스타일 3타 막타 연출! { - CinemachineShake.Instance.ShakeAttack(); // 애니메이션 타이밍에 맞춰 쾅! + CinemachineShake.Instance.HitSlow(0.2f, 0.05f); // 묵직한 슬로우 + CinemachineShake.Instance.CameraKick(10f); // 화끈한 카메라 킥 + CinemachineShake.Instance.ShakeAttack(); + } + else // 일반 1, 2타 + { + CinemachineShake.Instance.HitSlow(0.1f, 0.2f); + CinemachineShake.Instance.ShakeAttack(); } } - // (기존 StartWeaponCollision, StopWeaponCollision 로직 유지...) - public void StartWeaponCollision() + // ⭐ 공격 애니메이션 마지막 프레임에 꼭 넣으세요! + public void OnAttackEnd() { - if (interaction.CurrentWeapon == null || weaponHitBox == null) return; - float damage = stats.BaseAttackDamage + interaction.CurrentWeapon.Config.BaseDamage; - weaponHitBox.EnableHitBox(damage); + if (_comboCount == 3) // 막타가 끝났다면 후딜레이 시작 + { + StartCoroutine(PostComboRecovery()); + } + else + { + _isAttacking = false; // 1, 2타는 즉시 이동 가능 + } } - public void StopWeaponCollision() + private IEnumerator PostComboRecovery() { - if (weaponHitBox != null) weaponHitBox.DisableHitBox(); + _canAttack = false; + _isAttacking = true; // 딜레이 중에도 이동 제한 + yield return new WaitForSeconds(postComboDelay); + _canAttack = true; + _isAttacking = false; // 이제 움직이기 가능 + _comboCount = 0; // 콤보 리셋 + } + + public void CancelCharging() // 🛠️ 에러 해결: 확실하게 포함됨! + { + _isCharging = false; + _chargeTimer = 0f; + if (pAnim != null) pAnim.SetCharging(false); + if (CinemachineShake.Instance != null) CinemachineShake.Instance.SetZoom(false); } public void ReleaseAttack() { if (!_isCharging || interaction.CurrentWeapon == null) return; - pAnim.TriggerThrow(); pAnim.SetCharging(false); - // 레이캐스트 조준 로직 - Ray ray = Camera.main.ScreenPointToRay(Input.mousePosition); - Plane groundPlane = new Plane(Vector3.up, transform.position); - float rayDistance; - Vector3 targetDirection = transform.forward; + // 던지기 물리 로직 생략 (기존 것 유지) - if (groundPlane.Raycast(ray, out rayDistance)) + if (CinemachineShake.Instance != null) { - Vector3 pointOnGround = ray.GetPoint(rayDistance); - targetDirection = (pointOnGround - transform.position).normalized; - targetDirection.y = 0; + CinemachineShake.Instance.HitSlow(0.15f, 0.3f); + CinemachineShake.Instance.SetZoom(false); + CinemachineShake.Instance.ShakeAttack(); } - if (targetDirection != Vector3.zero) transform.rotation = Quaternion.LookRotation(targetDirection); - - // 차징 레벨 계산 및 무기 던지기 - int lv = _chargeTimer >= 2f ? 3 : (_chargeTimer >= 1f ? 2 : 1); - float currentSpread = interaction.CurrentWeapon.Config.GetSpread(lv); - float throwForce = interaction.CurrentWeapon.Config.GetForce(lv); - - Vector3 finalThrowDir = Quaternion.Euler(0, Random.Range(-currentSpread, currentSpread), 0) * targetDirection; - interaction.CurrentWeapon.OnThrown(finalThrowDir, throwForce, lv, stats); - - // 상태 초기화 및 카메라 효과 interaction.ClearCurrentWeapon(); stats.ResetWeight(); _isCharging = false; _chargeTimer = 0f; - - CinemachineShake.Instance.SetZoom(false); - CinemachineShake.Instance.ShakeAttack(); // 강공격도 묵직하게 흔들어줌 } public void StartCharging() @@ -109,14 +123,9 @@ public class PlayerAttack : MonoBehaviour _isCharging = true; _chargeTimer = 0f; pAnim.SetCharging(true); - CinemachineShake.Instance.SetZoom(true); + if (CinemachineShake.Instance != null) CinemachineShake.Instance.SetZoom(true); } - public void CancelCharging() - { - _isCharging = false; - _chargeTimer = 0f; - if (pAnim != null) pAnim.SetCharging(false); - CinemachineShake.Instance.SetZoom(false); - } + public void StartWeaponCollision() { /* ... */ } + public void StopWeaponCollision() { /* ... */ } } \ No newline at end of file diff --git a/Assets/5.TestScript/CamShake.cs b/Assets/5.TestScript/CamShake.cs index 5ec73b64..d57d2c9b 100644 --- a/Assets/5.TestScript/CamShake.cs +++ b/Assets/5.TestScript/CamShake.cs @@ -21,33 +21,45 @@ public class CinemachineShake : MonoBehaviour { Instance = this; _impulseSource = GetComponent(); - - // ⭐ 가상 카메라가 연결 안 되어 있으면 자동으로 찾아보는 안전장치 if (_vCam == null) _vCam = GetComponent(); } - // 1. ⭐ [수정] 일반 공격 흔들림 (수직으로 묵직하게!) + // ⭐ 렉 없는 타격감: 시간을 0.1배속으로 늘림 (Hit-Slow) + public void HitSlow(float duration = 0.15f, float timeScale = 0.1f) + { + StartCoroutine(DoHitSlow(duration, timeScale)); + } + + private IEnumerator DoHitSlow(float duration, float timeScale) + { + Time.timeScale = timeScale; // 멈추지 않고 아주 느리게 + yield return new WaitForSecondsRealtime(duration); + Time.timeScale = 1.0f; + } + + // ⭐ 카메라 킥: 순간적으로 FOV를 확 줄여 충격을 줌 + public void CameraKick(float kickAmount = 8f) + { + if (_vCam == null) return; + _vCam.m_Lens.FieldOfView -= kickAmount; // 순간 줌인 + SetZoom(false); // AnimateZoom을 통해 부드럽게 복구 + } + public void ShakeAttack() { if (_impulseSource == null) return; - // Vector3.down 방향으로 힘을 주어 아래로 쾅! 찍는 느낌을 줍니다. - _impulseSource.GenerateImpulse(Vector3.down * 0.5f); + _impulseSource.GenerateImpulse(Vector3.down * 1.5f); } - // 2. ⭐ [수정] 힘 부족 시 도리도리 (수평으로 가볍게!) public void ShakeNoNo() { if (_impulseSource == null) return; - // Vector3.right 방향으로 힘을 주어 고개를 좌우로 흔드는 느낌을 줍니다. _impulseSource.GenerateImpulse(Vector3.right * 0.4f); - Debug.Log("[Shake] 힘 수치 부족! 도리도리 실행"); } public void SetZoom(bool isZooming) { - // ⭐ [에러 방지] 카메라가 없으면 코루틴을 실행하지 않음 if (_vCam == null) return; - if (_zoomCoroutine != null) StopCoroutine(_zoomCoroutine); float targetFOV = isZooming ? zoomedFOV : defaultFOV; _zoomCoroutine = StartCoroutine(AnimateZoom(targetFOV)); @@ -55,12 +67,9 @@ public class CinemachineShake : MonoBehaviour private IEnumerator AnimateZoom(float target) { - // ⭐ [에러 방지] 실행 중 카메라가 사라질 경우 대비 if (_vCam == null) yield break; - while (Mathf.Abs(_vCam.m_Lens.FieldOfView - target) > 0.1f) { - if (_vCam == null) yield break; _vCam.m_Lens.FieldOfView = Mathf.Lerp(_vCam.m_Lens.FieldOfView, target, Time.deltaTime * zoomSpeed); yield return null; } diff --git a/Assets/5.TestScript/PlayerMovement.cs b/Assets/5.TestScript/PlayerMovement.cs index 6540c007..a031dbe9 100644 --- a/Assets/5.TestScript/PlayerMovement.cs +++ b/Assets/5.TestScript/PlayerMovement.cs @@ -6,12 +6,11 @@ public class PlayerMovement : MonoBehaviour [SerializeField] private Stats stats; [SerializeField] private PlayerHealth health; [SerializeField] private PlayerAnimator pAnim; - [SerializeField] private PlayerAttack attackScript; // ⭐ 차징 상태 확인용 추가 + [SerializeField] private PlayerAttack attackScript; [Header("--- 차징 감속 설정 ---")] [Range(0.1f, 1f)] - [Tooltip("풀차징 시 원래 속도의 몇 %로 줄일지 설정 (0.3 = 30% 속도)")] - [SerializeField] private float minSpeedMultiplier = 0.3f; // + [SerializeField] private float minSpeedMultiplier = 0.3f; private Vector3 _moveDir; private bool _isSprinting; @@ -20,39 +19,37 @@ public class PlayerMovement : MonoBehaviour private void Update() { - // 💀 사망 시 모든 이동 및 애니메이션 중단 + // 1. 💀 사망 시 혹은 ⚔️ 공격 중일 때 이동 차단 if (health != null && health.IsDead) { if (pAnim != null) pAnim.UpdateMove(0f); return; } - // 1. 기본 이동 속도 결정 + // ⭐ [추가] 공격 중(콤보 및 후딜레이 포함)에는 이동 입력 무시 + if (attackScript != null && attackScript.IsAttacking) + { + if (pAnim != null) pAnim.UpdateMove(0f); // 제자리 대기 애니메이션 유도 + return; + } + + // 2. 이동 속도 계산 및 차징 감속 적용 float speed = _isSprinting ? stats.CurrentRunSpeed : stats.CurrentMoveSpeed; - // 2. ⭐ [추가] 차징 진행도에 따른 점진적 감속 로직 if (attackScript != null && attackScript.IsCharging) { - // ChargeProgress ($0 \sim 1$)에 따라 1.0(정상)에서 minSpeedMultiplier(최소)까지 부드럽게 감소 - // 예: 풀차징 시 원래 속도의 30%만 사용 float speedReduction = Mathf.Lerp(1.0f, minSpeedMultiplier, attackScript.ChargeProgress); speed *= speedReduction; } - // 3. 이동 처리 + // 3. 실제 이동 처리 transform.Translate(_moveDir * speed * Time.deltaTime, Space.World); - // 4. ⭐ 애니메이션 연동 + // 4. 애니메이션 연동 if (pAnim != null) { float animVal = _moveDir.magnitude > 0.1f ? (_isSprinting ? 1.0f : 0.5f) : 0f; - - // ⭐ 차징 중이면 걷는 애니메이션 속도도 살짝 늦춰서 묵직함을 더함 - if (attackScript != null && attackScript.IsCharging) - { - animVal *= 0.5f; - } - + if (attackScript != null && attackScript.IsCharging) animVal *= 0.5f; pAnim.UpdateMove(animVal); } }