diff --git a/Assets/_Game/Scripts/Enemy/EnemyBase.cs b/Assets/_Game/Scripts/Enemy/EnemyBase.cs index 5b013f90..30d680a0 100644 --- a/Assets/_Game/Scripts/Enemy/EnemyBase.cs +++ b/Assets/_Game/Scripts/Enemy/EnemyBase.cs @@ -216,6 +216,17 @@ namespace Colosseum.Enemy deltaPosition.z = 0f; } + // 패턴 마지막 스킬이 Idle로 복귀하는 짧은 블렌드 구간에서는 + // Idle 클립의 수평 루트모션이 섞여 뒤로 밀리는 현상을 막습니다. + bool shouldSuppressPostPatternIdleHorizontalRootMotion = + skillCtrl != null + && skillCtrl.IsSuppressingIdleRecoveryHorizontalRootMotion; + if (shouldSuppressPostPatternIdleHorizontalRootMotion) + { + deltaPosition.x = 0f; + deltaPosition.z = 0f; + } + bool shouldProjectRootMotionToFacing = skillCtrl != null && skillCtrl.ShouldProjectHorizontalRootMotionToFacing; if (shouldProjectRootMotionToFacing) deltaPosition = ProjectHorizontalDeltaOntoFacing(deltaPosition); diff --git a/Assets/_Game/Scripts/Skills/SkillController.cs b/Assets/_Game/Scripts/Skills/SkillController.cs index 40a62a27..9352fff2 100644 --- a/Assets/_Game/Scripts/Skills/SkillController.cs +++ b/Assets/_Game/Scripts/Skills/SkillController.cs @@ -136,6 +136,8 @@ namespace Colosseum.Skills private string currentClipDebugName = string.Empty; private int currentClipStartFrame = -1; private readonly List debugSkillStartPoints = new(); + private bool isSuppressingIdleRecoveryHorizontalRootMotion = false; + private int idleRecoveryStateHash = 0; public bool IsExecutingSkill => currentSkill != null; @@ -143,6 +145,11 @@ namespace Colosseum.Skills public bool UsesRootMotion => currentSkill != null && currentSkill.UseRootMotion; public bool IgnoreRootMotionY => currentSkill != null && currentSkill.IgnoreRootMotionY; /// + /// 스킬 종료 후 Idle 복귀 블렌드 중인지 반환합니다. + /// 이 구간에서는 Idle 클립의 수평 루트모션이 섞여 위치가 밀리지 않도록 별도 차단에 사용합니다. + /// + public bool IsSuppressingIdleRecoveryHorizontalRootMotion => isSuppressingIdleRecoveryHorizontalRootMotion; + /// /// 현재 스킬의 루트 위치를 실제 이동에 반영해도 되는지 반환합니다. /// 스킬 시작 직후 아직 Animator가 Skill 상태로 전이되지 않은 프레임에서는 /// 이전 상태의 루트 XZ가 누적되지 않도록 위치 적용을 잠시 차단합니다. @@ -336,7 +343,13 @@ namespace Colosseum.Skills private void Update() { - if (currentSkill == null || animator == null) return; + if (animator == null) + return; + + UpdateIdleRecoverySuppressionState(); + + if (currentSkill == null) + return; UpdateCastTargetTracking(); @@ -1730,6 +1743,24 @@ namespace Colosseum.Skills return Animator.StringToHash($"{BaseLayerName}.{SKILL_STATE_NAME}"); } + /// + /// Idle 복귀 블렌드가 끝나면 수평 루트모션 차단 플래그를 자동 해제합니다. + /// + private void UpdateIdleRecoverySuppressionState() + { + if (!isSuppressingIdleRecoveryHorizontalRootMotion || animator == null) + return; + + if (animator.IsInTransition(0)) + return; + + if (idleRecoveryStateHash != 0 && animator.GetCurrentAnimatorStateInfo(0).fullPathHash == idleRecoveryStateHash) + { + isSuppressingIdleRecoveryHorizontalRootMotion = false; + idleRecoveryStateHash = 0; + } + } + /// /// 현재 레이어가 실제 스킬 상태를 재생 중인지 확인합니다. /// 스킬 시작 직후 한 프레임 동안 이전 상태 정보가 남아 조기 종료되는 것을 방지합니다. @@ -1792,9 +1823,22 @@ namespace Colosseum.Skills return; animator.speed = 1f; + isSuppressingIdleRecoveryHorizontalRootMotion = false; + idleRecoveryStateHash = 0; if (recoveryStateHash != 0 && animator.HasState(0, recoveryStateHash)) + { + bool isIdleRecoveryState = + recoveryStateHash == ResolveBossIdleStateHash() + || recoveryStateHash == Animator.StringToHash($"{BaseLayerName}.{IdleStateName}"); + if (isIdleRecoveryState) + { + isSuppressingIdleRecoveryHorizontalRootMotion = true; + idleRecoveryStateHash = recoveryStateHash; + } + animator.CrossFadeInFixedTime(recoveryStateHash, GetSkillExitTransitionDuration(), 0, 0f); + } } } }