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