fix: 드로그 패턴 애니메이션 재생 끊김 수정

- BT 재평가 중에도 패턴 실행 상태를 보존하도록 보스 패턴 액션과 런타임 상태를 조정했다.
- 스킬 컨트롤러에서 동일 프레임 종료 판정을 막아 패턴 내 다음 스킬이 즉시 잘리는 문제를 수정했다.
- 드로그 BT, 패턴/스킬 데이터, 애니메이션 클립과 컨트롤러를 현재 검증된 재생 구성으로 정리했다.
- 자연 발동 기준으로 콤보-기본기2 재생 시간을 재검증해 클립 길이와 실제 재생 간격이 맞는 것을 확인했다.
This commit is contained in:
2026-04-12 05:44:54 +09:00
parent 12a481b596
commit 9fd231626b
40 changed files with 598072 additions and 425361 deletions

View File

@@ -127,6 +127,10 @@ namespace Colosseum.Skills
private Dictionary<SkillData, float> cooldownTracker = new Dictionary<SkillData, float>();
private AnimatorOverrideController runtimeOverrideController;
private int cachedRecoveryStateHash;
private float currentClipStartTime = -1f;
private float currentClipExpectedDuration = 0f;
private string currentClipDebugName = string.Empty;
private int currentClipStartFrame = -1;
public bool IsExecutingSkill => currentSkill != null;
@@ -292,7 +296,13 @@ namespace Colosseum.Skills
// 애니메이션 종료 시 처리
if (stateInfo.normalizedTime >= 1f)
{
if (Time.frameCount <= currentClipStartFrame)
return;
// 같은 반복 차수 내에서 다음 클립이 있으면 재생
if (HasNextClipInSequence())
LogCurrentClipTiming("Advance");
if (TryPlayNextClipInSequence())
return;
@@ -308,6 +318,7 @@ namespace Colosseum.Skills
}
// 모든 클립과 단계가 끝나면 종료
LogCurrentClipTiming("Complete");
if (debugMode) Debug.Log($"[Skill] Animation complete: {currentSkill.SkillName}");
RestoreBaseControllerIfNeeded();
CompleteCurrentSkillExecution(SkillExecutionResult.Completed);
@@ -566,7 +577,8 @@ namespace Colosseum.Skills
? currentLoadoutEntry.GetResolvedAnimationSpeed()
: currentSkill.AnimationSpeed;
animator.speed = resolvedAnimationSpeed;
PlaySkillClip(currentPhaseAnimationClips[0], ShouldBlendIntoClip());
float enterTransitionDuration = ResolveSkillEnterTransitionDuration();
PlaySkillClip(currentPhaseAnimationClips[0], enterTransitionDuration > 0f, enterTransitionDuration);
}
TriggerImmediateSelfEffectsIfNeeded();
@@ -588,7 +600,7 @@ namespace Colosseum.Skills
return false;
currentClipSequenceIndex = nextIndex;
PlaySkillClip(currentPhaseAnimationClips[currentClipSequenceIndex], blendIn: false);
PlaySkillClip(currentPhaseAnimationClips[currentClipSequenceIndex], blendIn: false, transitionDuration: 0f);
if (debugMode)
{
@@ -598,6 +610,17 @@ namespace Colosseum.Skills
return true;
}
/// <summary>
/// 현재 시퀀스에 다음 클립이 남아 있는지 반환합니다.
/// </summary>
private bool HasNextClipInSequence()
{
if (currentSkill == null || currentPhaseAnimationClips == null)
return false;
return currentClipSequenceIndex + 1 < currentPhaseAnimationClips.Count;
}
/// <summary>
/// 반복 시전이 남아 있으면 다음 차수를 시작합니다.
/// </summary>
@@ -616,7 +639,7 @@ namespace Colosseum.Skills
/// <summary>
/// 스킬 클립으로 Override Controller 생성 후 재생
/// </summary>
private void PlaySkillClip(AnimationClip clip, bool blendIn)
private void PlaySkillClip(AnimationClip clip, bool blendIn, float transitionDuration)
{
if (baseSkillClip == null)
{
@@ -630,19 +653,21 @@ namespace Colosseum.Skills
return;
}
RecordCurrentClipTiming(clip);
if (debugMode)
{
Debug.Log($"[Skill] PlaySkillClip: {clip.name}, BaseClip: {baseSkillClip.name}");
Debug.Log($"[Skill] PlaySkillClip: {clip.name}, BaseClip: {baseSkillClip.name}, Blend={blendIn}, Transition={transitionDuration:F2}");
}
if (blendIn)
animator.CrossFadeInFixedTime(GetSkillStateHash(), GetSkillEnterTransitionDuration(), 0, 0f);
animator.CrossFadeInFixedTime(GetSkillStateHash(), transitionDuration, 0, 0f);
else
animator.Play(GetSkillStateHash(), 0, 0f);
// 클라이언트에 클립 동기화
if (IsServer && IsSpawned)
PlaySkillClipClientRpc(registeredClips.IndexOf(clip), blendIn);
PlaySkillClipClientRpc(registeredClips.IndexOf(clip), blendIn, transitionDuration);
}
/// <summary>
@@ -673,7 +698,7 @@ namespace Colosseum.Skills
/// 클라이언트: override 컨트롤러 적용 + 스킬 상태 재생 (원자적 실행으로 타이밍 문제 해결)
/// </summary>
[Rpc(SendTo.NotServer)]
private void PlaySkillClipClientRpc(int clipIndex, bool blendIn)
private void PlaySkillClipClientRpc(int clipIndex, bool blendIn, float transitionDuration)
{
if (baseSkillClip == null || animator == null || baseController == null) return;
if (clipIndex < 0 || clipIndex >= registeredClips.Count || registeredClips[clipIndex] == null)
@@ -685,7 +710,7 @@ namespace Colosseum.Skills
if (!ApplyOverrideClip(registeredClips[clipIndex]))
return;
if (blendIn)
animator.CrossFadeInFixedTime(GetSkillStateHash(), GetSkillEnterTransitionDuration(), 0, 0f);
animator.CrossFadeInFixedTime(GetSkillStateHash(), transitionDuration, 0, 0f);
else
animator.Play(GetSkillStateHash(), 0, 0f);
}
@@ -825,6 +850,7 @@ namespace Colosseum.Skills
lastCancelledSkillName = currentSkill.SkillName;
lastCancelReason = reason;
LogCurrentClipTiming("Cancelled");
Debug.Log($"[Skill] Cancelled: {currentSkill.SkillName} / reason={reason}");
RestoreBaseController();
@@ -1171,7 +1197,7 @@ namespace Colosseum.Skills
? currentLoadoutEntry.GetResolvedAnimationSpeed()
: currentSkill.AnimationSpeed;
animator.speed = resolvedAnimationSpeed;
PlaySkillClip(currentPhaseAnimationClips[0], blendIn: false);
PlaySkillClip(currentPhaseAnimationClips[0], blendIn: false, transitionDuration: 0f);
if (debugMode)
Debug.Log($"[Skill] 해제 단계 시작: {currentSkill.SkillName}");
@@ -1265,6 +1291,10 @@ namespace Colosseum.Skills
currentIterationIndex = 0;
loopHoldRequested = false;
cachedRecoveryStateHash = 0;
currentClipStartTime = -1f;
currentClipExpectedDuration = 0f;
currentClipDebugName = string.Empty;
currentClipStartFrame = -1;
shouldBlendIntoCurrentSkill = true;
shouldRestoreToIdleAfterCurrentSkill = true;
}
@@ -1288,6 +1318,38 @@ namespace Colosseum.Skills
sustainController?.HandleSkillExecutionEnded();
}
/// <summary>
/// 현재 클립의 예상 재생 시간을 기록합니다.
/// </summary>
private void RecordCurrentClipTiming(AnimationClip clip)
{
if (clip == null)
return;
currentClipStartTime = Time.time;
currentClipExpectedDuration = clip.length / Mathf.Max(0.0001f, animator != null ? animator.speed : 1f);
currentClipDebugName = clip.name;
currentClipStartFrame = Time.frameCount;
if (!debugMode)
return;
Debug.Log($"[SkillTiming] Start: skill={currentSkill?.SkillName ?? "<null>"}, clip={currentClipDebugName}, t={currentClipStartTime:F3}, expected={currentClipExpectedDuration:F3}");
}
/// <summary>
/// 현재 클립의 실제 경과 시간을 로그로 남깁니다.
/// </summary>
private void LogCurrentClipTiming(string phase)
{
if (!debugMode || string.IsNullOrEmpty(currentClipDebugName) || currentClipStartTime < 0f)
return;
float elapsed = Time.time - currentClipStartTime;
float delta = elapsed - currentClipExpectedDuration;
Debug.Log($"[SkillTiming] {phase}: skill={currentSkill?.SkillName ?? "<null>"}, clip={currentClipDebugName}, t={Time.time:F3}, elapsed={elapsed:F3}, expected={currentClipExpectedDuration:F3}, delta={delta:F3}");
}
/// <summary>
/// 적 스킬이 시전 중일 때 대상 추적 정책을 적용합니다.
/// </summary>
@@ -1460,29 +1522,46 @@ namespace Colosseum.Skills
/// <summary>
/// 현재 재생할 클립이 스킬 시작 블렌드 대상인지 반환합니다.
/// </summary>
private bool ShouldBlendIntoClip()
private float ResolveSkillEnterTransitionDuration()
{
if (isBossPatternBoundarySkill)
return false;
if (!shouldBlendIntoCurrentSkill)
return false;
return 0f;
if (isPlayingReleasePhase)
return false;
return 0f;
return currentClipSequenceIndex == 0 && currentIterationIndex == 1;
if (currentClipSequenceIndex != 0 || currentIterationIndex != 1)
return 0f;
if (!isBossPatternBoundarySkill)
return skillEnterTransitionDuration;
bool shouldBlendBossEntry = ShouldBlendBossPatternEntryFromCurrentState();
return shouldBlendBossEntry
? Mathf.Max(skillEnterTransitionDuration, bossPatternEnterTransitionDuration)
: 0f;
}
/// <summary>
/// 현재 스킬 진입에 사용할 전환 시간을 반환합니다.
/// 보스 패턴 첫 스킬이 이동 중 진입인지 판단합니다.
/// </summary>
private float GetSkillEnterTransitionDuration()
private bool ShouldBlendBossPatternEntryFromCurrentState()
{
if (isBossPatternBoundarySkill)
return bossPatternEnterTransitionDuration;
if (animator == null)
return false;
return skillEnterTransitionDuration;
if (animator.IsInTransition(0))
return true;
AnimatorStateInfo currentState = animator.GetCurrentAnimatorStateInfo(0);
int moveStateHash = Animator.StringToHash($"{BaseLayerName}.{MoveStateName}");
if (currentState.fullPathHash == moveStateHash)
return true;
UnityEngine.AI.NavMeshAgent agent = GetComponent<UnityEngine.AI.NavMeshAgent>();
return agent != null
&& agent.enabled
&& agent.velocity.sqrMagnitude > 0.0025f;
}
/// <summary>