fix: 보스 패턴 애니메이션 전환 안정화
- SkillController를 패턴 경계 기준으로 분기해 보스 첫 스킬은 즉시 시작하고 마지막 스킬만 Idle로 부드럽게 복귀하도록 조정 - 보스 패턴 실행 중 현재 스킬이 첫/마지막 스텝인지 BossBehaviorRuntimeState와 패턴 실행 경로에서 공유하도록 확장 - 패턴 내부 연속 클립 전환은 하드 전환으로 유지해 시작 프레임 스킵과 중간 Idle 복귀 문제를 줄이고 종료 전환 시간을 별도 노출
This commit is contained in:
@@ -252,6 +252,10 @@ public abstract partial class BossPatternActionBase : Action
|
||||
return Status.Failure;
|
||||
}
|
||||
|
||||
runtimeState.SetCurrentPatternSkillBoundary(
|
||||
startsFromIdle: IsFirstSkillStep(currentStepIndex),
|
||||
returnsToIdle: IsLastSkillStep(currentStepIndex));
|
||||
|
||||
GameObject skillTarget = activeTarget;
|
||||
if (step.Skill.JumpToTarget)
|
||||
{
|
||||
@@ -376,6 +380,7 @@ public abstract partial class BossPatternActionBase : Action
|
||||
if (isChargeWaiting)
|
||||
EndChargeWait(broken: false);
|
||||
|
||||
runtimeState?.SetCurrentPatternSkillBoundary(false, false);
|
||||
activePattern = null;
|
||||
activeTarget = null;
|
||||
currentStepIndex = 0;
|
||||
@@ -384,6 +389,36 @@ public abstract partial class BossPatternActionBase : Action
|
||||
waitEndTime = 0f;
|
||||
}
|
||||
|
||||
private bool IsFirstSkillStep(int stepIndex)
|
||||
{
|
||||
if (activePattern == null || activePattern.Steps == null)
|
||||
return false;
|
||||
|
||||
for (int i = 0; i < activePattern.Steps.Count; i++)
|
||||
{
|
||||
PatternStep candidate = activePattern.Steps[i];
|
||||
if (candidate != null && candidate.Type == PatternStepType.Skill && candidate.Skill != null)
|
||||
return i == stepIndex;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
private bool IsLastSkillStep(int stepIndex)
|
||||
{
|
||||
if (activePattern == null || activePattern.Steps == null)
|
||||
return false;
|
||||
|
||||
for (int i = activePattern.Steps.Count - 1; i >= 0; i--)
|
||||
{
|
||||
PatternStep candidate = activePattern.Steps[i];
|
||||
if (candidate != null && candidate.Type == PatternStepType.Skill && candidate.Skill != null)
|
||||
return i == stepIndex;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
private Status FinalizeResolvedPattern(BossPatternExecutionResult result)
|
||||
{
|
||||
runtimeState?.CompletePatternExecution(activePattern, result);
|
||||
|
||||
@@ -64,6 +64,9 @@ namespace Colosseum.Enemy
|
||||
protected float nextPatternReadyTime;
|
||||
protected BossPatternExecutionResult lastPatternExecutionResult;
|
||||
protected BossPatternData lastExecutedPattern;
|
||||
protected BossPatternData activePattern;
|
||||
protected bool currentPatternSkillStartsFromIdle;
|
||||
protected bool currentPatternSkillReturnsToIdle;
|
||||
protected GameObject lastReviveCaster;
|
||||
protected GameObject lastRevivedTarget;
|
||||
protected float lastReviveEventTime = float.NegativeInfinity;
|
||||
@@ -108,6 +111,21 @@ namespace Colosseum.Enemy
|
||||
/// </summary>
|
||||
public BossPatternData LastExecutedPattern => lastExecutedPattern;
|
||||
|
||||
/// <summary>
|
||||
/// 현재 패턴 실행 중인지 여부
|
||||
/// </summary>
|
||||
public bool IsExecutingPattern => activePattern != null && lastPatternExecutionResult == BossPatternExecutionResult.Running;
|
||||
|
||||
/// <summary>
|
||||
/// 현재 스킬 스텝이 패턴 시작에서 Idle과 이어져야 하는지 여부
|
||||
/// </summary>
|
||||
public bool CurrentPatternSkillStartsFromIdle => currentPatternSkillStartsFromIdle;
|
||||
|
||||
/// <summary>
|
||||
/// 현재 스킬 스텝이 패턴 종료에서 Idle로 돌아가야 하는지 여부
|
||||
/// </summary>
|
||||
public bool CurrentPatternSkillReturnsToIdle => currentPatternSkillReturnsToIdle;
|
||||
|
||||
/// <summary>
|
||||
/// EnemyBase 접근자
|
||||
/// </summary>
|
||||
@@ -204,8 +222,11 @@ namespace Colosseum.Enemy
|
||||
/// </summary>
|
||||
public void BeginPatternExecution(BossPatternData pattern)
|
||||
{
|
||||
activePattern = pattern;
|
||||
lastExecutedPattern = pattern;
|
||||
lastPatternExecutionResult = BossPatternExecutionResult.Running;
|
||||
currentPatternSkillStartsFromIdle = false;
|
||||
currentPatternSkillReturnsToIdle = false;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -215,11 +236,23 @@ namespace Colosseum.Enemy
|
||||
{
|
||||
lastExecutedPattern = pattern;
|
||||
lastPatternExecutionResult = result;
|
||||
activePattern = null;
|
||||
currentPatternSkillStartsFromIdle = false;
|
||||
currentPatternSkillReturnsToIdle = false;
|
||||
|
||||
if (pattern != null && IsTerminalPatternExecutionResult(result))
|
||||
StartCommonPatternInterval();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 현재 실행할 패턴 스킬이 패턴 시작/종료 경계인지 기록합니다.
|
||||
/// </summary>
|
||||
public void SetCurrentPatternSkillBoundary(bool startsFromIdle, bool returnsToIdle)
|
||||
{
|
||||
currentPatternSkillStartsFromIdle = startsFromIdle;
|
||||
currentPatternSkillReturnsToIdle = returnsToIdle;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 부활 스킬 사용 사실을 보스 AI에 알립니다.
|
||||
/// </summary>
|
||||
|
||||
@@ -210,6 +210,10 @@ namespace Colosseum.Enemy
|
||||
yield break;
|
||||
}
|
||||
|
||||
runtimeState.SetCurrentPatternSkillBoundary(
|
||||
startsFromIdle: IsFirstSkillStep(pattern, i),
|
||||
returnsToIdle: IsLastSkillStep(pattern, i));
|
||||
|
||||
GameObject stepTarget = currentTarget;
|
||||
if (step.Skill.JumpToTarget)
|
||||
{
|
||||
@@ -339,6 +343,8 @@ namespace Colosseum.Enemy
|
||||
if (isChargeWaiting)
|
||||
EndChargeWait(broken: false);
|
||||
|
||||
runtimeState?.SetCurrentPatternSkillBoundary(false, false);
|
||||
|
||||
if (currentPattern != null && runtimeState != null)
|
||||
{
|
||||
if (applyCooldown)
|
||||
@@ -355,6 +361,36 @@ namespace Colosseum.Enemy
|
||||
chargeTelegraphApplied = false;
|
||||
}
|
||||
|
||||
private static bool IsFirstSkillStep(BossPatternData pattern, int stepIndex)
|
||||
{
|
||||
if (pattern == null || pattern.Steps == null)
|
||||
return false;
|
||||
|
||||
for (int i = 0; i < pattern.Steps.Count; i++)
|
||||
{
|
||||
PatternStep candidate = pattern.Steps[i];
|
||||
if (candidate != null && candidate.Type == PatternStepType.Skill && candidate.Skill != null)
|
||||
return i == stepIndex;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
private static bool IsLastSkillStep(BossPatternData pattern, int stepIndex)
|
||||
{
|
||||
if (pattern == null || pattern.Steps == null)
|
||||
return false;
|
||||
|
||||
for (int i = pattern.Steps.Count - 1; i >= 0; i--)
|
||||
{
|
||||
PatternStep candidate = pattern.Steps[i];
|
||||
if (candidate != null && candidate.Type == PatternStepType.Skill && candidate.Skill != null)
|
||||
return i == stepIndex;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
private void ApplyPatternFlowState(BossPatternData pattern)
|
||||
{
|
||||
if (runtimeState == null || pattern == null)
|
||||
|
||||
@@ -50,6 +50,11 @@ namespace Colosseum.Skills
|
||||
public class SkillController : NetworkBehaviour
|
||||
{
|
||||
private const string SKILL_STATE_NAME = "Skill";
|
||||
private const string BaseLayerName = "Base Layer";
|
||||
private const string MoveStateName = "Move";
|
||||
private const string IdleStateName = "Idle";
|
||||
private const string BossIdlePhase1StateName = "Idle_Phase1";
|
||||
private const string BossIdlePhase3StateName = "Idle_Phase3";
|
||||
|
||||
[Header("애니메이션")]
|
||||
[SerializeField] private Animator animator;
|
||||
@@ -58,6 +63,16 @@ namespace Colosseum.Skills
|
||||
[Tooltip("Skill 상태에 연결된 기본 클립 (Override용). baseController의 Skill state에서 자동 발견됩니다.")]
|
||||
[SerializeField] private AnimationClip baseSkillClip;
|
||||
|
||||
[Header("애니메이션 전환")]
|
||||
[Tooltip("스킬 상태 진입 시 사용할 고정 전환 시간(초)")]
|
||||
[Min(0f)] [SerializeField] private float skillEnterTransitionDuration = 0.06f;
|
||||
[Tooltip("보스 패턴 첫 스킬 진입 시 사용할 고정 전환 시간(초)")]
|
||||
[Min(0f)] [SerializeField] private float bossPatternEnterTransitionDuration = 0.02f;
|
||||
[Tooltip("스킬 종료 후 기본 상태 복귀 시 사용할 고정 전환 시간(초)")]
|
||||
[Min(0f)] [SerializeField] private float skillExitTransitionDuration = 0.12f;
|
||||
[Tooltip("보스 패턴 마지막 스킬 종료 후 Idle 복귀 시 사용할 고정 전환 시간(초)")]
|
||||
[Min(0f)] [SerializeField] private float bossPatternExitTransitionDuration = 0.2f;
|
||||
|
||||
[Header("네트워크 동기화")]
|
||||
[Tooltip("이 이름이 포함된 클립이 자동 등록됩니다. 서버→클라이언트 클립 동기화에 사용됩니다.")]
|
||||
[SerializeField] private string clipAutoRegisterFilter = "_Player_";
|
||||
@@ -104,9 +119,14 @@ namespace Colosseum.Skills
|
||||
private readonly List<SkillEffect> currentLoopExitEffects = new();
|
||||
private readonly List<SkillEffect> currentReleaseStartEffects = new();
|
||||
private bool loopHoldRequested = false;
|
||||
private bool shouldBlendIntoCurrentSkill = true;
|
||||
private bool shouldRestoreToIdleAfterCurrentSkill = true;
|
||||
private bool isBossPatternBoundarySkill = false;
|
||||
|
||||
// 쿨타임 추적
|
||||
private Dictionary<SkillData, float> cooldownTracker = new Dictionary<SkillData, float>();
|
||||
private AnimatorOverrideController runtimeOverrideController;
|
||||
private int cachedRecoveryStateHash;
|
||||
|
||||
|
||||
public bool IsExecutingSkill => currentSkill != null;
|
||||
@@ -138,6 +158,8 @@ namespace Colosseum.Skills
|
||||
{
|
||||
baseController = animator.runtimeAnimatorController;
|
||||
}
|
||||
|
||||
EnsureRuntimeOverrideController();
|
||||
}
|
||||
|
||||
#if UNITY_EDITOR
|
||||
@@ -287,7 +309,7 @@ namespace Colosseum.Skills
|
||||
|
||||
// 모든 클립과 단계가 끝나면 종료
|
||||
if (debugMode) Debug.Log($"[Skill] Animation complete: {currentSkill.SkillName}");
|
||||
RestoreBaseController();
|
||||
RestoreBaseControllerIfNeeded();
|
||||
CompleteCurrentSkillExecution(SkillExecutionResult.Completed);
|
||||
}
|
||||
}
|
||||
@@ -380,6 +402,8 @@ namespace Colosseum.Skills
|
||||
currentSkill = skill;
|
||||
lastCancelReason = SkillCancelReason.None;
|
||||
lastExecutionResult = SkillExecutionResult.Running;
|
||||
CacheRecoveryState();
|
||||
ResolveSkillBoundaryTransitions();
|
||||
BuildResolvedEffects(currentLoadoutEntry);
|
||||
currentRepeatCount = currentLoadoutEntry.GetResolvedRepeatCount();
|
||||
currentIterationIndex = 0;
|
||||
@@ -542,7 +566,7 @@ namespace Colosseum.Skills
|
||||
? currentLoadoutEntry.GetResolvedAnimationSpeed()
|
||||
: currentSkill.AnimationSpeed;
|
||||
animator.speed = resolvedAnimationSpeed;
|
||||
PlaySkillClip(currentPhaseAnimationClips[0]);
|
||||
PlaySkillClip(currentPhaseAnimationClips[0], ShouldBlendIntoClip());
|
||||
}
|
||||
|
||||
TriggerImmediateSelfEffectsIfNeeded();
|
||||
@@ -564,7 +588,7 @@ namespace Colosseum.Skills
|
||||
return false;
|
||||
|
||||
currentClipSequenceIndex = nextIndex;
|
||||
PlaySkillClip(currentPhaseAnimationClips[currentClipSequenceIndex]);
|
||||
PlaySkillClip(currentPhaseAnimationClips[currentClipSequenceIndex], blendIn: false);
|
||||
|
||||
if (debugMode)
|
||||
{
|
||||
@@ -592,7 +616,7 @@ namespace Colosseum.Skills
|
||||
/// <summary>
|
||||
/// 스킬 클립으로 Override Controller 생성 후 재생
|
||||
/// </summary>
|
||||
private void PlaySkillClip(AnimationClip clip)
|
||||
private void PlaySkillClip(AnimationClip clip, bool blendIn)
|
||||
{
|
||||
if (baseSkillClip == null)
|
||||
{
|
||||
@@ -600,23 +624,25 @@ namespace Colosseum.Skills
|
||||
return;
|
||||
}
|
||||
|
||||
if (!ApplyOverrideClip(clip))
|
||||
{
|
||||
Debug.LogError("[SkillController] Skill override clip 적용에 실패했습니다.");
|
||||
return;
|
||||
}
|
||||
|
||||
if (debugMode)
|
||||
{
|
||||
Debug.Log($"[Skill] PlaySkillClip: {clip.name}, BaseClip: {baseSkillClip.name}");
|
||||
}
|
||||
|
||||
var overrideController = new AnimatorOverrideController(baseController);
|
||||
overrideController[baseSkillClip] = clip;
|
||||
animator.runtimeAnimatorController = overrideController;
|
||||
|
||||
// 애니메이터 완전 리셋 후 재생
|
||||
animator.Rebind();
|
||||
animator.Update(0f);
|
||||
animator.Play(SKILL_STATE_NAME, 0, 0f);
|
||||
if (blendIn)
|
||||
animator.CrossFadeInFixedTime(GetSkillStateHash(), GetSkillEnterTransitionDuration(), 0, 0f);
|
||||
else
|
||||
animator.Play(GetSkillStateHash(), 0, 0f);
|
||||
|
||||
// 클라이언트에 클립 동기화
|
||||
if (IsServer && IsSpawned)
|
||||
PlaySkillClipClientRpc(registeredClips.IndexOf(clip));
|
||||
PlaySkillClipClientRpc(registeredClips.IndexOf(clip), blendIn);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -624,22 +650,30 @@ namespace Colosseum.Skills
|
||||
/// </summary>
|
||||
private void RestoreBaseController()
|
||||
{
|
||||
if (animator != null && baseController != null)
|
||||
{
|
||||
animator.runtimeAnimatorController = baseController;
|
||||
animator.speed = 1f;
|
||||
}
|
||||
int recoveryStateHash = ResolveRecoveryStateHash();
|
||||
RestoreBaseAnimationState(recoveryStateHash);
|
||||
|
||||
// 클라이언트에 복원 동기화
|
||||
if (IsServer && IsSpawned)
|
||||
RestoreBaseControllerClientRpc();
|
||||
RestoreBaseControllerClientRpc(recoveryStateHash);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 현재 스킬이 Idle 복귀가 필요한 경계 스킬일 때만 기본 상태 복귀를 수행합니다.
|
||||
/// </summary>
|
||||
private void RestoreBaseControllerIfNeeded()
|
||||
{
|
||||
if (!shouldRestoreToIdleAfterCurrentSkill)
|
||||
return;
|
||||
|
||||
RestoreBaseController();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 클라이언트: override 컨트롤러 적용 + 스킬 상태 재생 (원자적 실행으로 타이밍 문제 해결)
|
||||
/// </summary>
|
||||
[Rpc(SendTo.NotServer)]
|
||||
private void PlaySkillClipClientRpc(int clipIndex)
|
||||
private void PlaySkillClipClientRpc(int clipIndex, bool blendIn)
|
||||
{
|
||||
if (baseSkillClip == null || animator == null || baseController == null) return;
|
||||
if (clipIndex < 0 || clipIndex >= registeredClips.Count || registeredClips[clipIndex] == null)
|
||||
@@ -648,22 +682,21 @@ namespace Colosseum.Skills
|
||||
return;
|
||||
}
|
||||
|
||||
var overrideController = new AnimatorOverrideController(baseController);
|
||||
overrideController[baseSkillClip] = registeredClips[clipIndex];
|
||||
animator.runtimeAnimatorController = overrideController;
|
||||
animator.Rebind();
|
||||
animator.Update(0f);
|
||||
animator.Play(SKILL_STATE_NAME, 0, 0f);
|
||||
if (!ApplyOverrideClip(registeredClips[clipIndex]))
|
||||
return;
|
||||
if (blendIn)
|
||||
animator.CrossFadeInFixedTime(GetSkillStateHash(), GetSkillEnterTransitionDuration(), 0, 0f);
|
||||
else
|
||||
animator.Play(GetSkillStateHash(), 0, 0f);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 클라이언트: 기본 컨트롤러 복원
|
||||
/// </summary>
|
||||
[Rpc(SendTo.NotServer)]
|
||||
private void RestoreBaseControllerClientRpc()
|
||||
private void RestoreBaseControllerClientRpc(int recoveryStateHash)
|
||||
{
|
||||
if (animator != null && baseController != null)
|
||||
animator.runtimeAnimatorController = baseController;
|
||||
RestoreBaseAnimationState(recoveryStateHash);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -874,7 +907,7 @@ namespace Colosseum.Skills
|
||||
if (TryStartReleasePhase())
|
||||
return true;
|
||||
|
||||
RestoreBaseController();
|
||||
RestoreBaseControllerIfNeeded();
|
||||
CompleteCurrentSkillExecution(SkillExecutionResult.Cancelled);
|
||||
return true;
|
||||
}
|
||||
@@ -1078,7 +1111,7 @@ namespace Colosseum.Skills
|
||||
if (TryStartReleasePhase())
|
||||
return;
|
||||
|
||||
RestoreBaseController();
|
||||
RestoreBaseControllerIfNeeded();
|
||||
CompleteCurrentSkillExecution(SkillExecutionResult.Completed);
|
||||
}
|
||||
|
||||
@@ -1138,7 +1171,7 @@ namespace Colosseum.Skills
|
||||
? currentLoadoutEntry.GetResolvedAnimationSpeed()
|
||||
: currentSkill.AnimationSpeed;
|
||||
animator.speed = resolvedAnimationSpeed;
|
||||
PlaySkillClip(currentPhaseAnimationClips[0]);
|
||||
PlaySkillClip(currentPhaseAnimationClips[0], blendIn: false);
|
||||
|
||||
if (debugMode)
|
||||
Debug.Log($"[Skill] 해제 단계 시작: {currentSkill.SkillName}");
|
||||
@@ -1231,6 +1264,9 @@ namespace Colosseum.Skills
|
||||
currentRepeatCount = 1;
|
||||
currentIterationIndex = 0;
|
||||
loopHoldRequested = false;
|
||||
cachedRecoveryStateHash = 0;
|
||||
shouldBlendIntoCurrentSkill = true;
|
||||
shouldRestoreToIdleAfterCurrentSkill = true;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -1368,5 +1404,177 @@ namespace Colosseum.Skills
|
||||
|
||||
currentTriggeredTargetsBuffer.Clear();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 기본 컨트롤러를 기반으로 런타임 OverrideController를 준비합니다.
|
||||
/// </summary>
|
||||
private bool EnsureRuntimeOverrideController()
|
||||
{
|
||||
if (animator == null || baseController == null || baseSkillClip == null)
|
||||
return false;
|
||||
|
||||
if (runtimeOverrideController == null || runtimeOverrideController.runtimeAnimatorController != baseController)
|
||||
runtimeOverrideController = new AnimatorOverrideController(baseController);
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 지정한 클립을 Skill 상태 override로 적용합니다.
|
||||
/// </summary>
|
||||
private bool ApplyOverrideClip(AnimationClip clip)
|
||||
{
|
||||
if (clip == null)
|
||||
return false;
|
||||
|
||||
if (!EnsureRuntimeOverrideController())
|
||||
return false;
|
||||
|
||||
// 같은 상태에 다른 스킬 클립을 연속 적용할 때는 새 override 인스턴스를 다시 붙여야
|
||||
// Unity가 바뀐 Motion을 안정적으로 반영합니다.
|
||||
runtimeOverrideController = new AnimatorOverrideController(baseController);
|
||||
runtimeOverrideController[baseSkillClip] = clip;
|
||||
animator.runtimeAnimatorController = runtimeOverrideController;
|
||||
animator.Update(0f);
|
||||
return true;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 현재 스킬이 패턴 경계에서 Idle과 블렌드해야 하는지 결정합니다.
|
||||
/// </summary>
|
||||
private void ResolveSkillBoundaryTransitions()
|
||||
{
|
||||
shouldBlendIntoCurrentSkill = true;
|
||||
shouldRestoreToIdleAfterCurrentSkill = true;
|
||||
isBossPatternBoundarySkill = false;
|
||||
|
||||
Colosseum.Enemy.BossBehaviorRuntimeState runtimeState = GetComponent<Colosseum.Enemy.BossBehaviorRuntimeState>();
|
||||
if (runtimeState == null || !runtimeState.IsExecutingPattern)
|
||||
return;
|
||||
|
||||
isBossPatternBoundarySkill = true;
|
||||
shouldBlendIntoCurrentSkill = runtimeState.CurrentPatternSkillStartsFromIdle;
|
||||
shouldRestoreToIdleAfterCurrentSkill = runtimeState.CurrentPatternSkillReturnsToIdle;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 현재 재생할 클립이 스킬 시작 블렌드 대상인지 반환합니다.
|
||||
/// </summary>
|
||||
private bool ShouldBlendIntoClip()
|
||||
{
|
||||
if (isBossPatternBoundarySkill)
|
||||
return false;
|
||||
|
||||
if (!shouldBlendIntoCurrentSkill)
|
||||
return false;
|
||||
|
||||
if (isPlayingReleasePhase)
|
||||
return false;
|
||||
|
||||
return currentClipSequenceIndex == 0 && currentIterationIndex == 1;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 현재 스킬 진입에 사용할 전환 시간을 반환합니다.
|
||||
/// </summary>
|
||||
private float GetSkillEnterTransitionDuration()
|
||||
{
|
||||
if (isBossPatternBoundarySkill)
|
||||
return bossPatternEnterTransitionDuration;
|
||||
|
||||
return skillEnterTransitionDuration;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 현재 스킬 종료에 사용할 전환 시간을 반환합니다.
|
||||
/// </summary>
|
||||
private float GetSkillExitTransitionDuration()
|
||||
{
|
||||
if (isBossPatternBoundarySkill && shouldRestoreToIdleAfterCurrentSkill)
|
||||
return bossPatternExitTransitionDuration;
|
||||
|
||||
return skillExitTransitionDuration;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 스킬 시작 전 기본 상태를 저장합니다.
|
||||
/// </summary>
|
||||
private void CacheRecoveryState()
|
||||
{
|
||||
if (animator == null)
|
||||
{
|
||||
cachedRecoveryStateHash = 0;
|
||||
return;
|
||||
}
|
||||
|
||||
AnimatorStateInfo currentState = animator.GetCurrentAnimatorStateInfo(0);
|
||||
cachedRecoveryStateHash = currentState.fullPathHash;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 스킬 상태 해시를 반환합니다.
|
||||
/// </summary>
|
||||
private static int GetSkillStateHash()
|
||||
{
|
||||
return Animator.StringToHash($"{BaseLayerName}.{SKILL_STATE_NAME}");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 스킬 종료 후 복귀할 상태 해시를 결정합니다.
|
||||
/// </summary>
|
||||
private int ResolveRecoveryStateHash()
|
||||
{
|
||||
if (animator == null)
|
||||
return 0;
|
||||
|
||||
if (isBossPatternBoundarySkill && shouldRestoreToIdleAfterCurrentSkill)
|
||||
{
|
||||
int bossIdleStateHash = ResolveBossIdleStateHash();
|
||||
if (bossIdleStateHash != 0)
|
||||
return bossIdleStateHash;
|
||||
}
|
||||
|
||||
if (cachedRecoveryStateHash != 0 && animator.HasState(0, cachedRecoveryStateHash))
|
||||
return cachedRecoveryStateHash;
|
||||
|
||||
int moveStateHash = Animator.StringToHash($"{BaseLayerName}.{MoveStateName}");
|
||||
if (animator.HasState(0, moveStateHash))
|
||||
return moveStateHash;
|
||||
|
||||
int idleStateHash = Animator.StringToHash($"{BaseLayerName}.{IdleStateName}");
|
||||
if (animator.HasState(0, idleStateHash))
|
||||
return idleStateHash;
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 보스 패턴 종료 시 현재 페이즈에 맞는 Idle 상태 해시를 반환합니다.
|
||||
/// </summary>
|
||||
private int ResolveBossIdleStateHash()
|
||||
{
|
||||
Colosseum.Enemy.BossBehaviorRuntimeState runtimeState = GetComponent<Colosseum.Enemy.BossBehaviorRuntimeState>();
|
||||
if (runtimeState == null)
|
||||
return 0;
|
||||
|
||||
int phase = runtimeState.CurrentPatternPhase;
|
||||
string stateName = phase >= 3 ? BossIdlePhase3StateName : BossIdlePhase1StateName;
|
||||
int stateHash = Animator.StringToHash($"{BaseLayerName}.{stateName}");
|
||||
return animator.HasState(0, stateHash) ? stateHash : 0;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 기본 스킬 클립 Override를 복원하고 지정한 상태로 부드럽게 복귀합니다.
|
||||
/// </summary>
|
||||
private void RestoreBaseAnimationState(int recoveryStateHash)
|
||||
{
|
||||
if (animator == null || baseController == null || baseSkillClip == null)
|
||||
return;
|
||||
|
||||
animator.speed = 1f;
|
||||
|
||||
if (recoveryStateHash != 0 && animator.HasState(0, recoveryStateHash))
|
||||
animator.CrossFadeInFixedTime(recoveryStateHash, GetSkillExitTransitionDuration(), 0, 0f);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user