fix: 드로그 패턴 애니메이션 재생 끊김 수정
- BT 재평가 중에도 패턴 실행 상태를 보존하도록 보스 패턴 액션과 런타임 상태를 조정했다. - 스킬 컨트롤러에서 동일 프레임 종료 판정을 막아 패턴 내 다음 스킬이 즉시 잘리는 문제를 수정했다. - 드로그 BT, 패턴/스킬 데이터, 애니메이션 클립과 컨트롤러를 현재 검증된 재생 구성으로 정리했다. - 자연 발동 기준으로 콤보-기본기2 재생 시간을 재검증해 클립 길이와 실제 재생 간격이 맞는 것을 확인했다.
This commit is contained in:
@@ -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>
|
||||
|
||||
Reference in New Issue
Block a user